bff6d70b17
Aplicatie Flask + SQLite pentru productie alimentara: cantarirea componentelor unui produs finit si tiparirea etichetei prin QZ Tray. - produse finite (retete) formate din componente cantarite - designer vizual de etichete (mm/px, zona sigura, campuri variabile, cod QR cu continut din mai multe bucati) - legare produs finit -> sablon de eticheta - navigare pe doua taburi: Cantarire / Setari - administrare: componente, produse finite, sabloane, campuri, istoric - backup, resetare baza de date, checklist de pregatire, /healthz - CLI de administrare si serviciu systemd pentru productie - vendor/syncpulse-scale-bridge (GPL-3.0-or-later) pentru citirea cantarului
136 lines
4.5 KiB
Python
136 lines
4.5 KiB
Python
"""Pagina de cantarire si setarile aplicatiei."""
|
|
from __future__ import annotations
|
|
|
|
from flask import (Blueprint, current_app, flash, jsonify, redirect, render_template, request,
|
|
send_file, url_for)
|
|
|
|
from .. import maintenance
|
|
from ..db import all_settings, get_setting, set_setting
|
|
from ..repositories import (
|
|
get_open_package,
|
|
get_recipe,
|
|
get_template,
|
|
list_components,
|
|
list_label_fields,
|
|
list_recipes,
|
|
list_templates,
|
|
package_state,
|
|
package_stats,
|
|
recent_packages,
|
|
)
|
|
from ..scale import get_scale
|
|
|
|
bp = Blueprint("main", __name__)
|
|
|
|
|
|
@bp.get("/")
|
|
def dashboard():
|
|
scale = get_scale(current_app)
|
|
profiles = scale.profiles()
|
|
active_profile = get_setting("active_scale_profile")
|
|
if active_profile not in {p["name"] for p in profiles}:
|
|
active_profile = profiles[0]["name"] if profiles else ""
|
|
|
|
open_package = get_open_package()
|
|
initial = package_state(open_package["id"]) if open_package else None
|
|
|
|
settings = all_settings()
|
|
template_id = settings.get("default_template_id") or ""
|
|
if template_id and get_template(int(template_id)) is None:
|
|
template_id = ""
|
|
|
|
recipes = [get_recipe(item["id"]) for item in list_recipes()]
|
|
recipes = [recipe for recipe in recipes if recipe is not None]
|
|
|
|
return render_template(
|
|
"dashboard.html",
|
|
recipes=recipes,
|
|
components=list_components(),
|
|
initial=initial,
|
|
recent=recent_packages(6),
|
|
stats=package_stats(),
|
|
profiles=profiles,
|
|
active_profile=active_profile,
|
|
scale_ready=bool(profiles),
|
|
default_operator=settings.get("default_operator", ""),
|
|
default_template_id=template_id,
|
|
auto_print=settings.get("auto_print", "1") == "1" and current_app.config["AUTO_PRINT"],
|
|
poll_ms=700,
|
|
scale_error=scale.error,
|
|
setup=maintenance.readiness_summary(),
|
|
)
|
|
|
|
|
|
@bp.get("/healthz")
|
|
def healthz():
|
|
"""Verificare simpla pentru monitorizare / reverse proxy."""
|
|
status = maintenance.database_status()
|
|
summary = maintenance.readiness_summary()
|
|
healthy = status["integrity"] == "ok"
|
|
payload = {
|
|
"ok": healthy,
|
|
"version": "0.1.0",
|
|
"database": {
|
|
"path": status["path"],
|
|
"size": status["size"],
|
|
"integrity": status["integrity"],
|
|
"schema_version": status["schema_version"],
|
|
"counts": status["counts"],
|
|
},
|
|
"setup": {
|
|
"ready": summary["ready"],
|
|
"missing": [item["label"] for item in summary["missing"]],
|
|
},
|
|
}
|
|
return jsonify(payload), (200 if healthy else 503)
|
|
|
|
|
|
@bp.get("/setari")
|
|
def settings_page():
|
|
scale = get_scale(current_app)
|
|
return render_template(
|
|
"settings.html",
|
|
profiles=scale.profiles(),
|
|
scale_available=scale.available,
|
|
scale_error=scale.error,
|
|
templates=list_templates(),
|
|
components=list_components(include_inactive=True),
|
|
recipes=list_recipes(include_inactive=True),
|
|
fields_count=len(list_label_fields(include_inactive=True)),
|
|
db_status=maintenance.database_status(),
|
|
backups=maintenance.list_backups()[:5],
|
|
setup=maintenance.readiness_summary(),
|
|
)
|
|
|
|
|
|
@bp.get("/setari/backup")
|
|
def settings_backup():
|
|
"""Descarca un backup proaspat al bazei (inainte de o actualizare)."""
|
|
path = maintenance.backup_database("manual")
|
|
maintenance.prune_backups()
|
|
return send_file(path, as_attachment=True, download_name=path.name)
|
|
|
|
|
|
@bp.post("/setari")
|
|
def settings_save():
|
|
scale = get_scale(current_app)
|
|
valid_profiles = {p["name"] for p in scale.profiles()}
|
|
active = (request.form.get("active_scale_profile") or "").strip()
|
|
if active and active not in valid_profiles:
|
|
flash("Profilul de cantar selectat nu exista.", "error")
|
|
return redirect(url_for("main.settings_page"))
|
|
|
|
template_id = (request.form.get("default_template_id") or "").strip()
|
|
if template_id and (not template_id.isdigit() or get_template(int(template_id)) is None):
|
|
flash("Sabloul implicit selectat nu exista.", "error")
|
|
return redirect(url_for("main.settings_page"))
|
|
|
|
for key in ("company_name", "company_address", "company_phone", "default_operator"):
|
|
set_setting(key, (request.form.get(key) or "").strip()[:200])
|
|
set_setting("active_scale_profile", active)
|
|
set_setting("default_template_id", template_id)
|
|
set_setting("auto_print", "1" if request.form.get("auto_print") else "0")
|
|
|
|
flash("Setarile au fost salvate.", "success")
|
|
return redirect(url_for("main.settings_page"))
|