feat(back-end): db connections implemented and created users
This commit is contained in:
@@ -92,7 +92,7 @@ jobs:
|
||||
echo "ENV=dev" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Trigger deploy on Unraid (forced command key)
|
||||
- name: Setup SSH key
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -120,9 +120,39 @@ jobs:
|
||||
# 5) Validazione: se fallisce qui, la chiave NON è valida/corrotta
|
||||
ssh-keygen -y -f ~/.ssh/id_ed25519 >/dev/null
|
||||
|
||||
# ... (resto del codice uguale)
|
||||
ssh-keyscan -H "${{ secrets.SERVER_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null
|
||||
|
||||
- name: Write DB env on server
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ "${{ env.ENV }}" == "prod" ]]; then
|
||||
DB_URL="${{ secrets.DB_URL_PROD }}"
|
||||
DB_USER="${{ secrets.DB_USERNAME_PROD }}"
|
||||
DB_PASS="${{ secrets.DB_PASSWORD_PROD }}"
|
||||
elif [[ "${{ env.ENV }}" == "int" ]]; then
|
||||
DB_URL="${{ secrets.DB_URL_INT }}"
|
||||
DB_USER="${{ secrets.DB_USERNAME_INT }}"
|
||||
DB_PASS="${{ secrets.DB_PASSWORD_INT }}"
|
||||
else
|
||||
DB_URL="${{ secrets.DB_URL_DEV }}"
|
||||
DB_USER="${{ secrets.DB_USERNAME_DEV }}"
|
||||
DB_PASS="${{ secrets.DB_PASSWORD_DEV }}"
|
||||
fi
|
||||
|
||||
cat > /tmp/pc.env <<EOF
|
||||
DB_URL=${DB_URL}
|
||||
DB_USERNAME=${DB_USER}
|
||||
DB_PASSWORD=${DB_PASS}
|
||||
EOF
|
||||
|
||||
ssh -i ~/.ssh/id_ed25519 -o BatchMode=yes "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" \
|
||||
"setenv ${{ env.ENV }}" < /tmp/pc.env
|
||||
|
||||
- name: Trigger deploy on Unraid (forced command key)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Aggiungiamo le opzioni di verbosità se dovesse fallire ancora,
|
||||
# e assicuriamoci che l'input sia pulito
|
||||
ssh -i ~/.ssh/id_ed25519 -o BatchMode=yes "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "${{ env.ENV }}"
|
||||
ssh -i ~/.ssh/id_ed25519 -o BatchMode=yes "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "deploy ${{ env.ENV }}"
|
||||
|
||||
10
Makefile
10
Makefile
@@ -1,10 +0,0 @@
|
||||
.PHONY: install s
|
||||
install:
|
||||
@echo "Installing Backend dependencies..."
|
||||
cd backend && pip install -r requirements.txt || pip install fastapi uvicorn trimesh python-multipart numpy
|
||||
@echo "Installing Frontend dependencies..."
|
||||
cd frontend && npm install
|
||||
|
||||
start:
|
||||
@echo "Starting development environment..."
|
||||
./start.sh
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.printcalculator.model;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public record CostBreakdown(
|
||||
BigDecimal materialCost,
|
||||
BigDecimal machineCost,
|
||||
BigDecimal energyCost,
|
||||
BigDecimal subtotal,
|
||||
BigDecimal markup
|
||||
) {}
|
||||
@@ -1,27 +1,15 @@
|
||||
package com.printcalculator.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import java.util.List;
|
||||
|
||||
public class QuoteResult {
|
||||
private double totalPrice;
|
||||
private String currency;
|
||||
private PrintStats stats;
|
||||
|
||||
@JsonIgnore
|
||||
private CostBreakdown breakdown;
|
||||
|
||||
@JsonIgnore
|
||||
private List<String> notes;
|
||||
|
||||
private double setupCost;
|
||||
|
||||
public QuoteResult(double totalPrice, String currency, PrintStats stats, CostBreakdown breakdown, List<String> notes, double setupCost) {
|
||||
public QuoteResult(double totalPrice, String currency, PrintStats stats, double setupCost) {
|
||||
this.totalPrice = totalPrice;
|
||||
this.currency = currency;
|
||||
this.stats = stats;
|
||||
this.breakdown = breakdown;
|
||||
this.notes = notes;
|
||||
this.setupCost = setupCost;
|
||||
}
|
||||
|
||||
@@ -36,14 +24,6 @@ public class QuoteResult {
|
||||
public PrintStats getStats() {
|
||||
return stats;
|
||||
}
|
||||
|
||||
public CostBreakdown getBreakdown() {
|
||||
return breakdown;
|
||||
}
|
||||
|
||||
public List<String> getNotes() {
|
||||
return notes;
|
||||
}
|
||||
|
||||
public double getSetupCost() {
|
||||
return setupCost;
|
||||
|
||||
@@ -17,7 +17,15 @@ public class GCodeParser {
|
||||
// ; estimated printing time = 1h 2m 3s
|
||||
// ; filament used [g] = 12.34
|
||||
// ; filament used [mm] = 1234.56
|
||||
private static final Pattern TIME_PATTERN = Pattern.compile(";\\s*estimated printing time.*=\\s*(.*)", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern TOTAL_ESTIMATED_TIME_PATTERN = Pattern.compile(
|
||||
";\\s*.*total\\s+estimated\\s+time\\s*[:=]\\s*([^;]+)",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern MODEL_PRINTING_TIME_PATTERN = Pattern.compile(
|
||||
";\\s*.*model\\s+printing\\s+time\\s*[:=]\\s*([^;]+)",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern TIME_PATTERN = Pattern.compile(
|
||||
";\\s*(?:estimated\\s+printing\\s+time|estimated\\s+print\\s+time|print\\s+time).*[:=]\\s*(.*)",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern FILAMENT_G_PATTERN = Pattern.compile(";\\s*filament used \\[g\\]\\s*=\\s*(.*)");
|
||||
private static final Pattern FILAMENT_MM_PATTERN = Pattern.compile(";\\s*filament used \\[mm\\]\\s*=\\s*(.*)");
|
||||
|
||||
@@ -43,6 +51,22 @@ public class GCodeParser {
|
||||
System.out.println("DEBUG: Found potential time line: '" + line + "'");
|
||||
}
|
||||
|
||||
Matcher totalTimeMatcher = TOTAL_ESTIMATED_TIME_PATTERN.matcher(line);
|
||||
if (totalTimeMatcher.find()) {
|
||||
timeFormatted = totalTimeMatcher.group(1).trim();
|
||||
seconds = parseTimeString(timeFormatted);
|
||||
System.out.println("GCodeParser: Found total estimated time: " + timeFormatted + " (" + seconds + "s)");
|
||||
continue;
|
||||
}
|
||||
|
||||
Matcher modelTimeMatcher = MODEL_PRINTING_TIME_PATTERN.matcher(line);
|
||||
if (modelTimeMatcher.find()) {
|
||||
timeFormatted = modelTimeMatcher.group(1).trim();
|
||||
seconds = parseTimeString(timeFormatted);
|
||||
System.out.println("GCodeParser: Found model printing time: " + timeFormatted + " (" + seconds + "s)");
|
||||
continue;
|
||||
}
|
||||
|
||||
Matcher timeMatcher = TIME_PATTERN.matcher(line);
|
||||
if (timeMatcher.find()) {
|
||||
timeFormatted = timeMatcher.group(1).trim();
|
||||
@@ -72,21 +96,60 @@ public class GCodeParser {
|
||||
}
|
||||
|
||||
private long parseTimeString(String timeStr) {
|
||||
// Formats: "1d 2h 3m 4s" or "1h 20m 10s"
|
||||
long totalSeconds = 0;
|
||||
|
||||
Matcher d = Pattern.compile("(\\d+)d").matcher(timeStr);
|
||||
if (d.find()) totalSeconds += Long.parseLong(d.group(1)) * 86400;
|
||||
// Formats: "1d 2h 3m 4s", "1h 20m 10s", "01:23:45", "12:34"
|
||||
String lower = timeStr.toLowerCase();
|
||||
double totalSeconds = 0;
|
||||
boolean matched = false;
|
||||
|
||||
Matcher h = Pattern.compile("(\\d+)h").matcher(timeStr);
|
||||
if (h.find()) totalSeconds += Long.parseLong(h.group(1)) * 3600;
|
||||
Matcher d = Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*d").matcher(lower);
|
||||
if (d.find()) {
|
||||
totalSeconds += Double.parseDouble(d.group(1)) * 86400;
|
||||
matched = true;
|
||||
}
|
||||
|
||||
Matcher m = Pattern.compile("(\\d+)m").matcher(timeStr);
|
||||
if (m.find()) totalSeconds += Long.parseLong(m.group(1)) * 60;
|
||||
Matcher h = Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*h").matcher(lower);
|
||||
if (h.find()) {
|
||||
totalSeconds += Double.parseDouble(h.group(1)) * 3600;
|
||||
matched = true;
|
||||
}
|
||||
|
||||
Matcher s = Pattern.compile("(\\d+)s").matcher(timeStr);
|
||||
if (s.find()) totalSeconds += Long.parseLong(s.group(1));
|
||||
Matcher m = Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*m").matcher(lower);
|
||||
if (m.find()) {
|
||||
totalSeconds += Double.parseDouble(m.group(1)) * 60;
|
||||
matched = true;
|
||||
}
|
||||
|
||||
return totalSeconds;
|
||||
Matcher s = Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*s").matcher(lower);
|
||||
if (s.find()) {
|
||||
totalSeconds += Double.parseDouble(s.group(1));
|
||||
matched = true;
|
||||
}
|
||||
|
||||
if (matched) {
|
||||
return Math.round(totalSeconds);
|
||||
}
|
||||
|
||||
long daySeconds = 0;
|
||||
Matcher dayPrefix = Pattern.compile("(\\d+)\\s*d").matcher(lower);
|
||||
if (dayPrefix.find()) {
|
||||
daySeconds = Long.parseLong(dayPrefix.group(1)) * 86400;
|
||||
}
|
||||
|
||||
Matcher hms = Pattern.compile("(\\d{1,2}):(\\d{2}):(\\d{2})").matcher(lower);
|
||||
if (hms.find()) {
|
||||
long hours = Long.parseLong(hms.group(1));
|
||||
long minutes = Long.parseLong(hms.group(2));
|
||||
long seconds = Long.parseLong(hms.group(3));
|
||||
return daySeconds + hours * 3600 + minutes * 60 + seconds;
|
||||
}
|
||||
|
||||
Matcher ms = Pattern.compile("(\\d{1,2}):(\\d{2})").matcher(lower);
|
||||
if (ms.find()) {
|
||||
long minutes = Long.parseLong(ms.group(1));
|
||||
long seconds = Long.parseLong(ms.group(2));
|
||||
return daySeconds + minutes * 60 + seconds;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.printcalculator.entity.FilamentVariant;
|
||||
import com.printcalculator.entity.PricingPolicy;
|
||||
import com.printcalculator.entity.PricingPolicyMachineHourTier;
|
||||
import com.printcalculator.entity.PrinterMachine;
|
||||
import com.printcalculator.model.CostBreakdown;
|
||||
import com.printcalculator.model.PrintStats;
|
||||
import com.printcalculator.model.QuoteResult;
|
||||
import com.printcalculator.repository.FilamentMaterialTypeRepository;
|
||||
@@ -18,10 +17,7 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class QuoteCalculator {
|
||||
@@ -101,22 +97,7 @@ public class QuoteCalculator {
|
||||
BigDecimal markupFactor = BigDecimal.ONE.add(policy.getMarkupPercent().divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP));
|
||||
BigDecimal totalPrice = subtotal.multiply(markupFactor).setScale(2, RoundingMode.HALF_UP);
|
||||
|
||||
BigDecimal markupAmount = totalPrice.subtract(subtotal);
|
||||
|
||||
CostBreakdown breakdown = new CostBreakdown(
|
||||
materialCost.setScale(2, RoundingMode.HALF_UP),
|
||||
machineCost.setScale(2, RoundingMode.HALF_UP),
|
||||
energyCost.setScale(2, RoundingMode.HALF_UP),
|
||||
subtotal.setScale(2, RoundingMode.HALF_UP),
|
||||
markupAmount.setScale(2, RoundingMode.HALF_UP)
|
||||
);
|
||||
|
||||
List<String> notes = new ArrayList<>();
|
||||
notes.add("Policy: " + policy.getPolicyName());
|
||||
notes.add("Machine: " + machine.getPrinterDisplayName());
|
||||
notes.add("Material: " + variant.getVariantDisplayName());
|
||||
|
||||
return new QuoteResult(totalPrice.doubleValue(), "CHF", stats, breakdown, notes, fixedFee.doubleValue());
|
||||
return new QuoteResult(totalPrice.doubleValue(), "CHF", stats, fixedFee.doubleValue());
|
||||
}
|
||||
|
||||
private BigDecimal calculateMachineCost(PricingPolicy policy, BigDecimal hours) {
|
||||
|
||||
@@ -74,4 +74,40 @@ class GCodeParserTest {
|
||||
|
||||
tempFile.delete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void parse_colonFormattedTime_returnsCorrectStats() throws IOException {
|
||||
File tempFile = File.createTempFile("test_colon", ".gcode");
|
||||
try (FileWriter writer = new FileWriter(tempFile)) {
|
||||
writer.write("; generated by OrcaSlicer\n");
|
||||
writer.write("; print time: 01:02:03\n");
|
||||
writer.write("; filament used [g] = 7.5\n");
|
||||
}
|
||||
|
||||
GCodeParser parser = new GCodeParser();
|
||||
PrintStats stats = parser.parse(tempFile);
|
||||
|
||||
assertEquals(3723L, stats.printTimeSeconds());
|
||||
assertEquals("01:02:03", stats.printTimeFormatted());
|
||||
|
||||
tempFile.delete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void parse_totalEstimatedTimeInline_returnsCorrectStats() throws IOException {
|
||||
File tempFile = File.createTempFile("test_total", ".gcode");
|
||||
try (FileWriter writer = new FileWriter(tempFile)) {
|
||||
writer.write("; generated by OrcaSlicer\n");
|
||||
writer.write("; model printing time: 5m 17s; total estimated time: 5m 21s\n");
|
||||
writer.write("; filament used [g] = 2.0\n");
|
||||
}
|
||||
|
||||
GCodeParser parser = new GCodeParser();
|
||||
PrintStats stats = parser.parse(tempFile);
|
||||
|
||||
assertEquals(321L, stats.printTimeSeconds());
|
||||
assertEquals("5m 21s", stats.printTimeFormatted());
|
||||
|
||||
tempFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ services:
|
||||
- ENERGY_COST_PER_KWH=${ENERGY_COST_PER_KWH}
|
||||
- PRINTER_POWER_WATTS=${PRINTER_POWER_WATTS}
|
||||
- MARKUP_PERCENT=${MARKUP_PERCENT}
|
||||
- DB_URL=${DB_URL}
|
||||
- DB_USERNAME=${DB_USERNAME}
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
- TEMP_DIR=/app/temp
|
||||
- PROFILES_DIR=/app/profiles
|
||||
restart: always
|
||||
@@ -31,4 +34,4 @@ services:
|
||||
volumes:
|
||||
backend_profiles_prod:
|
||||
backend_profiles_int:
|
||||
backend_profiles_dev:
|
||||
backend_profiles_dev:
|
||||
|
||||
@@ -26,11 +26,11 @@
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Make children (specifically app-card) stretch */
|
||||
> * {
|
||||
flex: 1;
|
||||
}
|
||||
/* Stretch only the loading card so the spinner stays centered */
|
||||
.col-result > .loading-state {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Mode Selector (Segmented Control style) */
|
||||
|
||||
@@ -191,6 +191,11 @@ export class UploadFormComponent implements OnInit {
|
||||
|
||||
const item = this.items().find(i => i.file === file);
|
||||
if (item) {
|
||||
const vars = this.currentMaterialVariants();
|
||||
if (vars && vars.length > 0) {
|
||||
const found = vars.find(v => v.colorName === item.color);
|
||||
if (found) return found.hexColor;
|
||||
}
|
||||
return getColorHex(item.color);
|
||||
}
|
||||
return '#facf0a';
|
||||
|
||||
@@ -49,6 +49,18 @@ interface BackendResponse {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface BackendQuoteResult {
|
||||
totalPrice: number;
|
||||
currency: string;
|
||||
setupCost: number;
|
||||
stats: {
|
||||
printTimeSeconds: number;
|
||||
printTimeFormatted: string;
|
||||
filamentWeightGrams: number;
|
||||
filamentLengthMm: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Options Interfaces
|
||||
export interface MaterialOption {
|
||||
code: string;
|
||||
@@ -159,7 +171,7 @@ export class QuoteEstimatorService {
|
||||
// @ts-ignore
|
||||
if (environment.basicAuth) headers['Authorization'] = 'Basic ' + btoa(environment.basicAuth);
|
||||
|
||||
return this.http.post<BackendResponse>(`${environment.apiUrl}/api/quote`, formData, {
|
||||
return this.http.post<BackendResponse | BackendQuoteResult>(`${environment.apiUrl}/api/quote`, formData, {
|
||||
headers,
|
||||
reportProgress: true,
|
||||
observe: 'events'
|
||||
@@ -206,7 +218,9 @@ export class QuoteEstimatorService {
|
||||
|
||||
// Calculate Results
|
||||
let setupCost = 10;
|
||||
|
||||
let setupCostFromBackend: number | null = null;
|
||||
let currencyFromBackend: string | null = null;
|
||||
|
||||
if (request.nozzleDiameter && request.nozzleDiameter !== 0.4) {
|
||||
setupCost += 2;
|
||||
}
|
||||
@@ -214,18 +228,27 @@ export class QuoteEstimatorService {
|
||||
const items: QuoteItem[] = [];
|
||||
|
||||
finalResponses.forEach((res, idx) => {
|
||||
if (res && res.success) {
|
||||
const originalItem = request.items[idx];
|
||||
items.push({
|
||||
fileName: res.fileName,
|
||||
unitPrice: res.data.cost.total,
|
||||
unitTime: res.data.print_time_seconds,
|
||||
unitWeight: res.data.material_grams,
|
||||
quantity: res.originalQty, // Use the requested quantity
|
||||
material: request.material,
|
||||
color: originalItem.color || 'Default'
|
||||
});
|
||||
if (!res) return;
|
||||
const originalItem = request.items[idx];
|
||||
const normalized = this.normalizeResponse(res);
|
||||
if (!normalized.success) return;
|
||||
|
||||
if (normalized.currency && currencyFromBackend == null) {
|
||||
currencyFromBackend = normalized.currency;
|
||||
}
|
||||
if (normalized.setupCost != null && setupCostFromBackend == null) {
|
||||
setupCostFromBackend = normalized.setupCost;
|
||||
}
|
||||
|
||||
items.push({
|
||||
fileName: res.fileName,
|
||||
unitPrice: normalized.unitPrice,
|
||||
unitTime: normalized.unitTime,
|
||||
unitWeight: normalized.unitWeight,
|
||||
quantity: res.originalQty, // Use the requested quantity
|
||||
material: request.material,
|
||||
color: originalItem.color || 'Default'
|
||||
});
|
||||
});
|
||||
|
||||
if (items.length === 0) {
|
||||
@@ -234,7 +257,8 @@ export class QuoteEstimatorService {
|
||||
}
|
||||
|
||||
// Initial Aggregation
|
||||
let grandTotal = setupCost;
|
||||
const useBackendSetup = setupCostFromBackend != null;
|
||||
let grandTotal = useBackendSetup ? 0 : setupCost;
|
||||
let totalTime = 0;
|
||||
let totalWeight = 0;
|
||||
|
||||
@@ -249,8 +273,8 @@ export class QuoteEstimatorService {
|
||||
|
||||
const result: QuoteResult = {
|
||||
items,
|
||||
setupCost,
|
||||
currency: 'CHF',
|
||||
setupCost: useBackendSetup ? setupCostFromBackend! : setupCost,
|
||||
currency: currencyFromBackend || 'CHF',
|
||||
totalPrice: Math.round(grandTotal * 100) / 100,
|
||||
totalTimeHours: totalHours,
|
||||
totalTimeMinutes: totalMinutes,
|
||||
@@ -274,6 +298,31 @@ export class QuoteEstimatorService {
|
||||
});
|
||||
}
|
||||
|
||||
private normalizeResponse(res: any): { success: boolean; unitPrice: number; unitTime: number; unitWeight: number; setupCost?: number; currency?: string } {
|
||||
if (res && typeof res.totalPrice === 'number' && res.stats && typeof res.stats.printTimeSeconds === 'number') {
|
||||
return {
|
||||
success: true,
|
||||
unitPrice: res.totalPrice,
|
||||
unitTime: res.stats.printTimeSeconds,
|
||||
unitWeight: res.stats.filamentWeightGrams,
|
||||
setupCost: res.setupCost,
|
||||
currency: res.currency
|
||||
};
|
||||
}
|
||||
|
||||
if (res && res.success && res.data) {
|
||||
return {
|
||||
success: true,
|
||||
unitPrice: res.data.cost.total,
|
||||
unitTime: res.data.print_time_seconds,
|
||||
unitWeight: res.data.material_grams,
|
||||
currency: 'CHF'
|
||||
};
|
||||
}
|
||||
|
||||
return { success: false, unitPrice: 0, unitTime: 0, unitWeight: 0 };
|
||||
}
|
||||
|
||||
private mapMaterial(mat: string): string {
|
||||
const m = mat.toUpperCase();
|
||||
if (m.includes('PLA')) return 'pla_basic';
|
||||
|
||||
Reference in New Issue
Block a user