diff --git a/src/frontend/flask_app/blueprints/maker.py b/src/frontend/flask_app/blueprints/maker.py index da49040..6002a9d 100644 --- a/src/frontend/flask_app/blueprints/maker.py +++ b/src/frontend/flask_app/blueprints/maker.py @@ -5,6 +5,7 @@ import math import requests as http_requests from functools import wraps +from urllib.parse import quote # #105: percorso del file ricetta nell'URL del backend from flask import Blueprint, Response, abort, flash, jsonify, redirect, render_template, request, session, url_for from flask_babel import gettext as _ @@ -701,6 +702,46 @@ def api_dxf_compare_recipe(task_id: int): return jsonify(resp), 200 +# #105: parametri della ricetta DXF gia' salvata sul task, per riaprire il wizard compilato +@maker_bp.route("/api/vision/tasks//recipe/dxf-compare/source", methods=["GET"]) +@login_required +@role_required("Maker") +def api_dxf_compare_source(task_id: int): + """Proxy: parametri e file della ricetta DXF del task (404 se non ce n'e' una) (#105).""" + resp = api_client.get(f"/api/vision/tasks/{task_id}/recipe/dxf-compare/source") + # Codice e corpo del backend arrivano com'e': il 404 vuol dire «nessuna ricetta da DXF» + if resp.get("error"): + return jsonify(resp), resp.get("status_code", 500) + return jsonify(resp), 200 + + +# #105: un file della cartella ricetta (DXF, immagine di riferimento) come byte, non JSON +@maker_bp.route("/api/vision/tasks//recipe/files/", methods=["GET"]) +@login_required +@role_required("Maker") +def api_vision_recipe_file(task_id: int, path: str): + """Proxy: byte di un file della cartella ricetta del task, con content-type del backend (#105).""" + # Niente segmenti '..' o backslash: urllib3 normalizzerebbe l'URL fuori da questa rotta del backend + if "\\" in path or ".." in path.split("/"): + return jsonify({"error": True, "detail": _("File non trovato")}), 404 + try: + resp = http_requests.get( + f"{Config.API_SERVER_URL.rstrip('/')}/api/vision/tasks/{task_id}/recipe/files/{quote(path)}", + headers={"X-API-Key": session.get("api_key", "")}, + timeout=30, + ) + except http_requests.RequestException as exc: + return jsonify({ + "error": True, + "detail": _("Servizio non raggiungibile: %(detail)s", detail=str(exc)), + }), 502 + return Response( + resp.content, + status=resp.status_code, + content_type=resp.headers.get("content-type", "application/octet-stream"), + ) + + # #101: anteprima del DXF appena scelto nel wizard (contorni letti dal vision worker) @maker_bp.route("/api/vision/dxf-preview", methods=["POST"]) @login_required diff --git a/src/frontend/flask_app/static/js/dxf_recipe_wizard.js b/src/frontend/flask_app/static/js/dxf_recipe_wizard.js index f14fb3e..b618160 100644 --- a/src/frontend/flask_app/static/js/dxf_recipe_wizard.js +++ b/src/frontend/flask_app/static/js/dxf_recipe_wizard.js @@ -45,10 +45,102 @@ function dxfRecipeWizard(taskId, csrf, msgs) { runPxPerMm: null, // #104: scala usata nell'ultima Prova (null = calibrazione stazione) runBandMm: null, // #104: banda usata nell'ultima Prova runMinPointsPct: null, // #104: soglia usata nell'ultima Prova + existing: null, // #105: risposta di /source (ricetta DXF gia' salvata sul task); null = nessuna + existingInfo: false, // #105: «Ricetta esistente caricata...» dopo aver compilato il wizard + existingWarn: '', // #105: avviso (immagine non salvata, file illeggibili) + dxfFromRecipe: false, // #105: il DXF viene dalla ricetta (l' non puo' mostrarne il nome) + _existingApplied: false, // #105: il wizard e' gia' stato compilato dalla ricetta (non sovrascrivere le modifiche) + _existingReady: null, // #105: promessa della prima lettura di /source + _srcSeq: 0, // #105: una risposta di /source arrivata dopo una lettura piu' recente si scarta + _dxfPickSeq: 0, // #105: sale a ogni scelta manuale del DXF; i file della ricetta arrivati dopo si scartano + _imgPickSeq: 0, // #105: idem per l'immagine (_setImage) + + // #105: all'avvio legge se il task ha gia' una ricetta da DXF (serve per l'etichetta del bottone) + init() { + if (this.taskId) this._existingReady = this.loadExisting(); + }, async toggle() { this.open = !this.open; - if (this.open && !this.stations.length) await this.loadStations(); + if (this.open) { + this.openExisting(); // #105: compila il wizard dalla ricetta esistente, in parallelo alle stazioni + if (!this.stations.length) await this.loadStations(); + } + }, + + // #105: GET /source -> this.existing (null se 404 o errore) + async loadExisting() { + const seq = ++this._srcSeq; + let found = null; + try { + const resp = await fetch(`/maker/api/vision/tasks/${this.taskId}/recipe/dxf-compare/source`); + if (resp.ok) { + const body = await resp.json().catch(() => null); + if (body && typeof body === 'object' && !body.error) found = body; + } + } catch (e) { console.error('dxfRecipeWizard source', e); } + if (seq !== this._srcSeq) return; // lettura piu' recente in corso + this.existing = found; + }, + + // #105: all'apertura, una sola volta, riempie il wizard con la ricetta esistente + async openExisting() { + if (this._existingReady) await this._existingReady; + if (!this.existing || this._existingApplied) return; + await this.applyExisting(); + }, + + // #105: mm/px da px/mm, fino a 6 cifre significative e virgola decimale; '' se non valido + _mmPxFrom(pxPerMm) { + const v = Number(pxPerMm); + if (!Number.isFinite(v) || v <= 0) return ''; + return String(Number((1 / v).toPrecision(6))).replace('.', ','); + }, + + // #105: un file della cartella ricetta -> File; null se assente o illeggibile (in quel caso avvisa) + async _fetchRecipeFile(name) { + try { + const resp = await fetch(`/maker/api/vision/tasks/${this.taskId}/recipe/files/${encodeURIComponent(name)}`); + if (!resp.ok) throw new Error('HTTP ' + resp.status); + const blob = await resp.blob(); + return new File([blob], name, { type: blob.type || '' }); + } catch (e) { + console.error('dxfRecipeWizard file', name, e); + this.existingWarn = this.msgs.existingFilesFailed || 'Cannot read the recipe files'; + return null; + } + }, + + // #105: valori salvati nei campi, poi DXF e immagine di riferimento dalla cartella ricetta + async applyExisting() { + const e = this.existing; + if (!e) return; + this._existingApplied = true; + this.error = ''; + this.notice = ''; + this.existingWarn = ''; + const band = Number(e.band_mm), pct = Number(e.min_points_pct); + if (Number.isFinite(band) && band > 0) this.bandMm = band; + if (Number.isFinite(pct) && pct > 0) this.minPointsPct = pct; + this.dark = e.polarity === 'dark'; + const mm = this._mmPxFrom(e.px_per_mm); + if (mm) this.mmPx = mm; // il campo compare quando l'immagine e' da file + this.existingInfo = true; + // Sequenze lette prima di scaricare: se l'utente sceglie un file a mano nel frattempo, vince lui + const dxfSeq = this._dxfPickSeq, imgSeq = this._imgPickSeq; + const [dxf, img] = await Promise.all([ + e.dxf ? this._fetchRecipeFile(e.dxf) : null, + e.image ? this._fetchRecipeFile(e.image) : null, + ]); + if (dxf && dxfSeq === this._dxfPickSeq) { + this.dxfFile = dxf; + this.dxfFromRecipe = true; + this.result = null; + this.tested = false; + this.loadDxfPreview(); + } + if (img && imgSeq === this._imgPickSeq) this._setImage(img, 'file'); // 'file': il campo mm/px compare gia' compilato + if (!e.image && !this.existingWarn) this.existingWarn = this.msgs.noReferenceImage || 'Reference image not saved'; }, async loadStations() { @@ -109,6 +201,7 @@ function dxfRecipeWizard(taskId, csrf, msgs) { this.tested = false; // #104 this.resPx = null; // #104 this.imageSource = file ? (source || '') : ''; // #104 + this._imgPickSeq++; // #105: una scelta piu' recente batte i file della ricetta ancora in arrivo }, // #102: l' dell'anteprima e' caricata: legge i pixel; se non si carica il formato non e' mostrabile @@ -140,6 +233,8 @@ function dxfRecipeWizard(taskId, csrf, msgs) { }, onDxf(ev) { this.dxfFile = (ev.target.files && ev.target.files[0]) || null; + this._dxfPickSeq++; // #105: scelta a mano: il DXF della ricetta in arrivo si scarta + this.dxfFromRecipe = false; // #105 this.result = null; this.tested = false; // #104 this.loadDxfPreview(); // #101: anteprima appena si sceglie il file @@ -267,6 +362,10 @@ function dxfRecipeWizard(taskId, csrf, msgs) { this.notice = this.msgs.saved || 'Saved'; // Il componente visionRecipe ricarica cartella, file e uscite delle quote this.$dispatch('dxf-recipe-saved', { taskId: this.taskId }); + // #105: il task ha ora una ricetta (etichetta «Modifica»); il wizard e' gia' allineato, non si ricompila + this._existingApplied = true; + this.existingInfo = false; + await this.loadExisting(); } } } catch (e) { diff --git a/src/frontend/flask_app/templates/maker/task_editor.html b/src/frontend/flask_app/templates/maker/task_editor.html index d40126d..c74d52e 100644 --- a/src/frontend/flask_app/templates/maker/task_editor.html +++ b/src/frontend/flask_app/templates/maker/task_editor.html @@ -662,11 +662,16 @@