feat(back-end): db connections implemented and created users

This commit is contained in:
2026-02-11 14:53:46 +01:00
parent e5183590c5
commit 8fac8ac892
11 changed files with 225 additions and 99 deletions

View File

@@ -92,7 +92,7 @@ jobs:
echo "ENV=dev" >> "$GITHUB_ENV" echo "ENV=dev" >> "$GITHUB_ENV"
fi fi
- name: Trigger deploy on Unraid (forced command key) - name: Setup SSH key
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
@@ -120,9 +120,39 @@ jobs:
# 5) Validazione: se fallisce qui, la chiave NON è valida/corrotta # 5) Validazione: se fallisce qui, la chiave NON è valida/corrotta
ssh-keygen -y -f ~/.ssh/id_ed25519 >/dev/null 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 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, # Aggiungiamo le opzioni di verbosità se dovesse fallire ancora,
# e assicuriamoci che l'input sia pulito # 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 }}"

View File

@@ -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

View File

@@ -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
) {}

View File

@@ -1,27 +1,15 @@
package com.printcalculator.model; package com.printcalculator.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import java.util.List;
public class QuoteResult { public class QuoteResult {
private double totalPrice; private double totalPrice;
private String currency; private String currency;
private PrintStats stats; private PrintStats stats;
@JsonIgnore
private CostBreakdown breakdown;
@JsonIgnore
private List<String> notes;
private double setupCost; 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.totalPrice = totalPrice;
this.currency = currency; this.currency = currency;
this.stats = stats; this.stats = stats;
this.breakdown = breakdown;
this.notes = notes;
this.setupCost = setupCost; this.setupCost = setupCost;
} }
@@ -36,14 +24,6 @@ public class QuoteResult {
public PrintStats getStats() { public PrintStats getStats() {
return stats; return stats;
} }
public CostBreakdown getBreakdown() {
return breakdown;
}
public List<String> getNotes() {
return notes;
}
public double getSetupCost() { public double getSetupCost() {
return setupCost; return setupCost;

View File

@@ -17,7 +17,15 @@ public class GCodeParser {
// ; estimated printing time = 1h 2m 3s // ; estimated printing time = 1h 2m 3s
// ; filament used [g] = 12.34 // ; filament used [g] = 12.34
// ; filament used [mm] = 1234.56 // ; 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_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*(.*)"); 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 + "'"); 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); Matcher timeMatcher = TIME_PATTERN.matcher(line);
if (timeMatcher.find()) { if (timeMatcher.find()) {
timeFormatted = timeMatcher.group(1).trim(); timeFormatted = timeMatcher.group(1).trim();
@@ -72,21 +96,60 @@ public class GCodeParser {
} }
private long parseTimeString(String timeStr) { private long parseTimeString(String timeStr) {
// Formats: "1d 2h 3m 4s" or "1h 20m 10s" // Formats: "1d 2h 3m 4s", "1h 20m 10s", "01:23:45", "12:34"
long totalSeconds = 0; String lower = timeStr.toLowerCase();
double totalSeconds = 0;
Matcher d = Pattern.compile("(\\d+)d").matcher(timeStr); boolean matched = false;
if (d.find()) totalSeconds += Long.parseLong(d.group(1)) * 86400;
Matcher h = Pattern.compile("(\\d+)h").matcher(timeStr); Matcher d = Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*d").matcher(lower);
if (h.find()) totalSeconds += Long.parseLong(h.group(1)) * 3600; if (d.find()) {
totalSeconds += Double.parseDouble(d.group(1)) * 86400;
matched = true;
}
Matcher m = Pattern.compile("(\\d+)m").matcher(timeStr); Matcher h = Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*h").matcher(lower);
if (m.find()) totalSeconds += Long.parseLong(m.group(1)) * 60; if (h.find()) {
totalSeconds += Double.parseDouble(h.group(1)) * 3600;
matched = true;
}
Matcher s = Pattern.compile("(\\d+)s").matcher(timeStr); Matcher m = Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*m").matcher(lower);
if (s.find()) totalSeconds += Long.parseLong(s.group(1)); 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;
} }
} }

View File

@@ -6,7 +6,6 @@ import com.printcalculator.entity.FilamentVariant;
import com.printcalculator.entity.PricingPolicy; import com.printcalculator.entity.PricingPolicy;
import com.printcalculator.entity.PricingPolicyMachineHourTier; import com.printcalculator.entity.PricingPolicyMachineHourTier;
import com.printcalculator.entity.PrinterMachine; import com.printcalculator.entity.PrinterMachine;
import com.printcalculator.model.CostBreakdown;
import com.printcalculator.model.PrintStats; import com.printcalculator.model.PrintStats;
import com.printcalculator.model.QuoteResult; import com.printcalculator.model.QuoteResult;
import com.printcalculator.repository.FilamentMaterialTypeRepository; import com.printcalculator.repository.FilamentMaterialTypeRepository;
@@ -18,10 +17,7 @@ import org.springframework.stereotype.Service;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Optional;
@Service @Service
public class QuoteCalculator { 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 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 totalPrice = subtotal.multiply(markupFactor).setScale(2, RoundingMode.HALF_UP);
BigDecimal markupAmount = totalPrice.subtract(subtotal); return new QuoteResult(totalPrice.doubleValue(), "CHF", stats, fixedFee.doubleValue());
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());
} }
private BigDecimal calculateMachineCost(PricingPolicy policy, BigDecimal hours) { private BigDecimal calculateMachineCost(PricingPolicy policy, BigDecimal hours) {

View File

@@ -74,4 +74,40 @@ class GCodeParserTest {
tempFile.delete(); 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();
}
} }

View File

@@ -13,6 +13,9 @@ services:
- ENERGY_COST_PER_KWH=${ENERGY_COST_PER_KWH} - ENERGY_COST_PER_KWH=${ENERGY_COST_PER_KWH}
- PRINTER_POWER_WATTS=${PRINTER_POWER_WATTS} - PRINTER_POWER_WATTS=${PRINTER_POWER_WATTS}
- MARKUP_PERCENT=${MARKUP_PERCENT} - MARKUP_PERCENT=${MARKUP_PERCENT}
- DB_URL=${DB_URL}
- DB_USERNAME=${DB_USERNAME}
- DB_PASSWORD=${DB_PASSWORD}
- TEMP_DIR=/app/temp - TEMP_DIR=/app/temp
- PROFILES_DIR=/app/profiles - PROFILES_DIR=/app/profiles
restart: always restart: always
@@ -31,4 +34,4 @@ services:
volumes: volumes:
backend_profiles_prod: backend_profiles_prod:
backend_profiles_int: backend_profiles_int:
backend_profiles_dev: backend_profiles_dev:

View File

@@ -26,11 +26,11 @@
min-width: 0; min-width: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
}
/* Make children (specifically app-card) stretch */ /* Stretch only the loading card so the spinner stays centered */
> * { .col-result > .loading-state {
flex: 1; flex: 1;
}
} }
/* Mode Selector (Segmented Control style) */ /* Mode Selector (Segmented Control style) */

View File

@@ -191,6 +191,11 @@ export class UploadFormComponent implements OnInit {
const item = this.items().find(i => i.file === file); const item = this.items().find(i => i.file === file);
if (item) { 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 getColorHex(item.color);
} }
return '#facf0a'; return '#facf0a';

View File

@@ -49,6 +49,18 @@ interface BackendResponse {
error?: string; error?: string;
} }
interface BackendQuoteResult {
totalPrice: number;
currency: string;
setupCost: number;
stats: {
printTimeSeconds: number;
printTimeFormatted: string;
filamentWeightGrams: number;
filamentLengthMm: number;
};
}
// Options Interfaces // Options Interfaces
export interface MaterialOption { export interface MaterialOption {
code: string; code: string;
@@ -159,7 +171,7 @@ export class QuoteEstimatorService {
// @ts-ignore // @ts-ignore
if (environment.basicAuth) headers['Authorization'] = 'Basic ' + btoa(environment.basicAuth); 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, headers,
reportProgress: true, reportProgress: true,
observe: 'events' observe: 'events'
@@ -206,7 +218,9 @@ export class QuoteEstimatorService {
// Calculate Results // Calculate Results
let setupCost = 10; let setupCost = 10;
let setupCostFromBackend: number | null = null;
let currencyFromBackend: string | null = null;
if (request.nozzleDiameter && request.nozzleDiameter !== 0.4) { if (request.nozzleDiameter && request.nozzleDiameter !== 0.4) {
setupCost += 2; setupCost += 2;
} }
@@ -214,18 +228,27 @@ export class QuoteEstimatorService {
const items: QuoteItem[] = []; const items: QuoteItem[] = [];
finalResponses.forEach((res, idx) => { finalResponses.forEach((res, idx) => {
if (res && res.success) { if (!res) return;
const originalItem = request.items[idx]; const originalItem = request.items[idx];
items.push({ const normalized = this.normalizeResponse(res);
fileName: res.fileName, if (!normalized.success) return;
unitPrice: res.data.cost.total,
unitTime: res.data.print_time_seconds, if (normalized.currency && currencyFromBackend == null) {
unitWeight: res.data.material_grams, currencyFromBackend = normalized.currency;
quantity: res.originalQty, // Use the requested quantity
material: request.material,
color: originalItem.color || 'Default'
});
} }
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) { if (items.length === 0) {
@@ -234,7 +257,8 @@ export class QuoteEstimatorService {
} }
// Initial Aggregation // Initial Aggregation
let grandTotal = setupCost; const useBackendSetup = setupCostFromBackend != null;
let grandTotal = useBackendSetup ? 0 : setupCost;
let totalTime = 0; let totalTime = 0;
let totalWeight = 0; let totalWeight = 0;
@@ -249,8 +273,8 @@ export class QuoteEstimatorService {
const result: QuoteResult = { const result: QuoteResult = {
items, items,
setupCost, setupCost: useBackendSetup ? setupCostFromBackend! : setupCost,
currency: 'CHF', currency: currencyFromBackend || 'CHF',
totalPrice: Math.round(grandTotal * 100) / 100, totalPrice: Math.round(grandTotal * 100) / 100,
totalTimeHours: totalHours, totalTimeHours: totalHours,
totalTimeMinutes: totalMinutes, 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 { private mapMaterial(mat: string): string {
const m = mat.toUpperCase(); const m = mat.toUpperCase();
if (m.includes('PLA')) return 'pla_basic'; if (m.includes('PLA')) return 'pla_basic';