Grotti 2.5.1: Druckbericht, Statistik, Spulenakte und Protokoll-Fixes.
This commit is contained in:
1 parent
b277ca48aa
commit
efd0801cd1
30 files changed
+3393
-178
No files matched your search
+202
-10
@@ -37,7 +37,7 @@ IDEA_STATUS = {"offen", "spaeter", "erledigt"}
|
||||
PRINTER_FAMILIES = set(FAMILY_LABELS) | {""}
|
||||
STEER_ACTIONS = {
|
||||
"nozzle", "bed", "speed", "flow", "fan", "emergency_stop", "clear_error",
|
||||
"exclude_object",
|
||||
"exclude_object", "dryer_start", "dryer_stop", "slot_setzen",
|
||||
}
|
||||
|
||||
|
||||
@@ -580,6 +580,58 @@ def create_app(cfg):
|
||||
def protokoll():
|
||||
return page("protokoll.html", "Protokoll")
|
||||
|
||||
@app.get("/protokoll/<int:run_id>")
|
||||
def protokoll_detail(run_id):
|
||||
row = store.get_run_row(conn(), run_id)
|
||||
if not row:
|
||||
abort(404)
|
||||
return page("bericht.html", "Druckbericht")
|
||||
|
||||
@app.get("/statistik")
|
||||
def statistik_page():
|
||||
return page("statistik.html", "Statistik")
|
||||
|
||||
@app.get("/etikett/lauf/<int:run_id>")
|
||||
def etikett_lauf(run_id):
|
||||
row = store.get_run_row(conn(), run_id)
|
||||
if not row:
|
||||
abort(404)
|
||||
run = store.run_out(row)
|
||||
if not run.get("chargen_nr"):
|
||||
store.ensure_run_chargen(conn(), run_id, run.get("weight_used_g"))
|
||||
row = store.get_run_row(conn(), run_id)
|
||||
run = store.run_out(row)
|
||||
if not run.get("chargen_nr"):
|
||||
abort(404)
|
||||
labels = store.list_run_labels(conn(), run_id)
|
||||
mode = (request.args.get("mode") or "druck").strip()
|
||||
qty = max(1, min(50, int(request.args.get("qty") or 1)))
|
||||
if mode == "objekt":
|
||||
labels = store.ensure_run_labels(conn(), run_id, run.get("objects") or [], qty=qty)
|
||||
sheets = [lab for lab in labels if not lab.get("excluded")]
|
||||
else:
|
||||
sheets = [{"object_name": "", "chargen_nr": run["chargen_nr"], "excluded": False}]
|
||||
if not sheets:
|
||||
abort(404)
|
||||
spools = store.etikett_spools_for_run(conn(), run)
|
||||
base = grotti_config.public_base(cfg, request.url_root)
|
||||
target_g = float(run.get("weight_planned_g") or run.get("weight_g") or 0)
|
||||
for sheet in sheets:
|
||||
sheet["qr_url"] = "%s/protokoll/%s" % (base, run_id)
|
||||
sheet["target_g"] = target_g
|
||||
if sheet.get("object_name"):
|
||||
sheet["qr_url"] += "#objekt-%s" % quote(sheet["object_name"])
|
||||
return render_template(
|
||||
"etikett_lauf.html",
|
||||
page="etikett",
|
||||
title="Etikett %s" % run["chargen_nr"],
|
||||
token="",
|
||||
run=run,
|
||||
sheets=sheets,
|
||||
spools=spools,
|
||||
target_g=target_g,
|
||||
)
|
||||
|
||||
@app.get("/ideen")
|
||||
def ideen():
|
||||
return page("ideen.html", "Ideen")
|
||||
@@ -672,6 +724,9 @@ def create_app(cfg):
|
||||
item["queue_failed"] = queue.get("failed")
|
||||
item["queue_waiting"] = queue.get("waiting", 0)
|
||||
item["queue_waiting_next"] = queue.get("waiting_next", "")
|
||||
live_run = store.find_live_run(conn(), host) if host else None
|
||||
item["live_run_id"] = (live_run or {}).get("id")
|
||||
item["live_chargen"] = (live_run or {}).get("chargen_nr") or ""
|
||||
|
||||
by_id = {row["host"]: row for row in rows}
|
||||
if request.args.get("live", "1") == "0":
|
||||
@@ -873,7 +928,7 @@ def create_app(cfg):
|
||||
row = store.get_file_row(conn(), file_id)
|
||||
if not row:
|
||||
abort(404)
|
||||
png = library.preview_png(row)
|
||||
png = library.preview_png(row, conn=conn(), data_dir=cfg["data_dir"])
|
||||
if not png:
|
||||
abort(404)
|
||||
return png, 200, {"Content-Type": "image/png", "Cache-Control": "private, max-age=300"}
|
||||
@@ -883,8 +938,8 @@ def create_app(cfg):
|
||||
row = store.get_file_row(conn(), file_id)
|
||||
if not row:
|
||||
raise GrottiError("Die Datei fehlt.", "datei", 404)
|
||||
path = Path(row["stored_path"])
|
||||
if not path.is_file():
|
||||
path = library.resolve_stored_path(conn(), row, cfg["data_dir"])
|
||||
if path is None or not path.is_file():
|
||||
raise GrottiError("Die Datei liegt nicht mehr auf der Platte.", "datei", 404)
|
||||
cache_dir = Path(cfg["data_dir"]) / "toolpath"
|
||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -1506,11 +1561,28 @@ def create_app(cfg):
|
||||
|
||||
@app.get("/api/v1/spulen/by-code/<path:code>")
|
||||
def spool_by_code(code):
|
||||
row = store.get_spool_by_code(conn(), code)
|
||||
text = (code or "").strip()
|
||||
if text.upper().startswith("D-"):
|
||||
run = store.get_run_by_chargen(conn(), text)
|
||||
if not run:
|
||||
raise GrottiError("Kein Druck mit dieser Charge.", "charge", 404)
|
||||
return jsonify({"kind": "lauf", "run": store.run_out(run), "redirect": "/protokoll/%s" % run["id"]})
|
||||
row = store.get_spool_by_code(conn(), text)
|
||||
if not row:
|
||||
raise GrottiError("Keine Spule mit diesem Code.", "spule", 404)
|
||||
return jsonify(row)
|
||||
|
||||
@app.get("/api/v1/spulen/<int:spool_id>/akte")
|
||||
def spool_akte(spool_id):
|
||||
spool = store.get_spool_row(conn(), spool_id)
|
||||
if not spool:
|
||||
raise GrottiError("Die Spule fehlt.", "spule", 404)
|
||||
return jsonify({
|
||||
"spool": store.spool_out(spool),
|
||||
"runs": store.list_spool_runs(conn(), spool_id),
|
||||
"stats": store.spool_usage_stats(conn(), spool_id),
|
||||
})
|
||||
|
||||
@app.post("/api/v1/spulen")
|
||||
def spool_create():
|
||||
return jsonify(store.create_spool(conn(), spool_fields(body()))), 201
|
||||
@@ -1531,6 +1603,18 @@ def create_app(cfg):
|
||||
spool = store.get_spool_row(conn(), spool_id)
|
||||
if not spool:
|
||||
raise GrottiError("Die Spule fehlt.", "spule", 404)
|
||||
live_run = store.find_live_run(conn(), host)
|
||||
mm_now = float((live_run or {}).get("filament_used_mm") or 0)
|
||||
printing = bool(
|
||||
live_run and (live_run.get("state") or "") in ("in_progress", "printing", "paused")
|
||||
)
|
||||
if printing:
|
||||
try:
|
||||
snap = moonraker.query(host)
|
||||
if isinstance(snap.get("filament_used"), (int, float)):
|
||||
mm_now = float(snap["filament_used"])
|
||||
except Exception:
|
||||
pass
|
||||
# Alte Rolle am Platz / externe Rolle am Host freimachen
|
||||
if slot:
|
||||
old = conn().execute(
|
||||
@@ -1542,22 +1626,32 @@ def create_app(cfg):
|
||||
"SELECT * FROM spools WHERE printer_host = %s AND ace_slot IS NULL AND id <> %s",
|
||||
(host, spool_id),
|
||||
).fetchall()
|
||||
printer = store.printer_by_host(conn(), host) or {}
|
||||
if printing:
|
||||
closed = store.close_open_segments(conn(), live_run["id"], slot, mm_now)
|
||||
if not closed and old:
|
||||
# Erster Wechsel: alter Abschnitt von 0 bis jetzt
|
||||
for row in old:
|
||||
store.open_segment(conn(), live_run["id"], row["id"], slot, 0)
|
||||
store.close_open_segments(conn(), live_run["id"], slot, mm_now)
|
||||
for row in old:
|
||||
store.update_spool(conn(), row["id"], {"printer_host": "", "ace_slot": None})
|
||||
store.add_event(
|
||||
conn(),
|
||||
host,
|
||||
(store.printer_by_host(conn(), host) or {}).get("name") or host,
|
||||
printer.get("name") or host,
|
||||
"spule",
|
||||
"%s: „%s“ ins Regal%s" % (
|
||||
(store.printer_by_host(conn(), host) or {}).get("name") or host,
|
||||
printer.get("name") or host,
|
||||
row["name"],
|
||||
(" (Platz %s)" % slot) if slot else "",
|
||||
),
|
||||
"",
|
||||
(live_run or {}).get("filename") or "",
|
||||
run_id=(live_run or {}).get("id"),
|
||||
)
|
||||
updated = store.update_spool(conn(), spool_id, {"printer_host": host, "ace_slot": slot})
|
||||
printer = store.printer_by_host(conn(), host) or {}
|
||||
if printing:
|
||||
store.open_segment(conn(), live_run["id"], spool_id, slot, mm_now)
|
||||
store.add_event(
|
||||
conn(),
|
||||
host,
|
||||
@@ -1568,7 +1662,8 @@ def create_app(cfg):
|
||||
updated["name"],
|
||||
(" auf Platz %s" % slot) if slot else "",
|
||||
),
|
||||
"",
|
||||
(live_run or {}).get("filename") or "",
|
||||
run_id=(live_run or {}).get("id"),
|
||||
)
|
||||
return jsonify(updated)
|
||||
|
||||
@@ -1704,6 +1799,36 @@ def create_app(cfg):
|
||||
def run_list():
|
||||
return jsonify(store.list_runs(conn()))
|
||||
|
||||
@app.get("/api/v1/laeufe/<int:run_id>")
|
||||
def run_get(run_id):
|
||||
row = store.get_run_row(conn(), run_id)
|
||||
if not row:
|
||||
raise GrottiError("Der Lauf fehlt.", "lauf", 404)
|
||||
run = store.run_out(row)
|
||||
if not run.get("chargen_nr"):
|
||||
store.ensure_run_chargen(conn(), run_id)
|
||||
row = store.get_run_row(conn(), run_id) or row
|
||||
run = store.run_out(row)
|
||||
if run.get("chargen_nr") and run.get("objects"):
|
||||
store.ensure_run_labels(conn(), run_id, run.get("objects") or [], qty=1)
|
||||
file_row = store.get_file_row(conn(), run["file_id"]) if run.get("file_id") else None
|
||||
project = store.get_project(conn(), run["project_id"]) if run.get("project_id") else None
|
||||
samples = store.list_run_samples(conn(), run_id)
|
||||
# Anzeige auf ca. 1 Punkt / Minute verdichten
|
||||
if len(samples) > 180:
|
||||
step = max(1, len(samples) // 120)
|
||||
samples = samples[::step]
|
||||
return jsonify({
|
||||
"run": run,
|
||||
"spools": store.list_run_spools(conn(), run_id),
|
||||
"segments": store.list_run_segments(conn(), run_id),
|
||||
"samples": samples,
|
||||
"labels": store.list_run_labels(conn(), run_id),
|
||||
"events": store.list_run_events(conn(), run_id),
|
||||
"file": store.file_out(file_row) if file_row else None,
|
||||
"project": project,
|
||||
})
|
||||
|
||||
@app.post("/api/v1/laeufe/abgleich")
|
||||
def run_sync():
|
||||
library.sync_watch(conn(), cfg, force=False)
|
||||
@@ -1726,4 +1851,71 @@ def create_app(cfg):
|
||||
row = store.run_out(current)
|
||||
return jsonify(row)
|
||||
|
||||
@app.get("/api/v1/statistik")
|
||||
def statistik_api():
|
||||
since = request.args.get("since") or None
|
||||
until = request.args.get("until") or None
|
||||
host = (request.args.get("host") or "").strip()
|
||||
material = (request.args.get("material") or "").strip()
|
||||
brand = (request.args.get("brand") or "").strip()
|
||||
project_id = optional_id(request.args.get("project_id"))
|
||||
return jsonify(store.statistics(
|
||||
conn(),
|
||||
since=since,
|
||||
until=until,
|
||||
host=host,
|
||||
material=material or "",
|
||||
brand=brand or "",
|
||||
project_id=project_id,
|
||||
))
|
||||
|
||||
@app.get("/api/v1/statistik.csv")
|
||||
def statistik_csv():
|
||||
import csv
|
||||
import io
|
||||
from flask import Response
|
||||
|
||||
since = request.args.get("since") or None
|
||||
until = request.args.get("until") or None
|
||||
host = (request.args.get("host") or "").strip()
|
||||
material = (request.args.get("material") or "").strip()
|
||||
brand = (request.args.get("brand") or "").strip()
|
||||
runs = store.list_runs(
|
||||
conn(),
|
||||
limit=2000,
|
||||
since=since,
|
||||
until=until,
|
||||
host=host,
|
||||
material=material,
|
||||
brand=brand,
|
||||
)
|
||||
buf = io.StringIO()
|
||||
writer = csv.writer(buf, delimiter=";")
|
||||
writer.writerow([
|
||||
"id", "drucker", "datei", "zustand", "start", "ende", "dauer_s",
|
||||
"soll_g", "ist_g", "quelle", "filament_eur", "strom_eur", "urteil", "charge",
|
||||
])
|
||||
for run in runs:
|
||||
writer.writerow([
|
||||
run["id"],
|
||||
run.get("printer_name") or "",
|
||||
run.get("filename") or "",
|
||||
run.get("state") or "",
|
||||
run.get("started_at") or "",
|
||||
run.get("ended_at") or "",
|
||||
run.get("duration_s") or 0,
|
||||
run.get("weight_planned_g") or 0,
|
||||
run.get("weight_used_g") or 0,
|
||||
run.get("weight_source") or "",
|
||||
run.get("filament_cost") or 0,
|
||||
run.get("energy_cost") or 0,
|
||||
run.get("verdict") or "",
|
||||
run.get("chargen_nr") or "",
|
||||
])
|
||||
return Response(
|
||||
buf.getvalue(),
|
||||
mimetype="text/csv; charset=utf-8",
|
||||
headers={"Content-Disposition": "attachment; filename=grotti-laeufe.csv"},
|
||||
)
|
||||
|
||||
return app
|
||||
Reference in new issue
Block a user