Produktion: Format-Warteschlangen, Sammeldruck, Webhook, Preis-a/e
Produktions-Board: - Massenauswahl (+ "Alle auswählen") mit Sammel-Druck und Sammel-Status - Warteschlange je Maschine nach Format gebündelt; Formatabfrage (machines.current_format) statt automatischem Anschluss - resiliente Ladeabfragen (Board bleibt ohne neue Felder nutzbar) Agent (skrift-agent): - druckt strikt ein Dokument nach dem anderen (Idle-Gate) -> behebt Verlust von Folgejobs während eines laufenden Drucks - druckt nur Jobs des eingelegten Formats (current_format), kein Auto-Anschluss - Heartbeat gedrosselt (nur bei Statuswechsel / alle 60 s) Neues Modul "Manueller Druck" (skrift-neuauftrag): - interner Auftrag ohne Zahlung, Vorschau, Platzhalter/Excel-Import, Felder bleiben nach dem Absenden stehen Backend/Endpunkte (skrift-orders): - jobs tragen format/format_label; /enqueue-bulk für Sammeldruck - Webhook /inbound/:kind (JetFormBuilder) mit Token + tolerantem Body-Parser - Kleinmengen-Aufschlag je Produkt über Faktoren a (Deckel) und e (Exponent) - Fix: norm()-Shadowing in calculatePrice (norm is not a function) Bootstrap: - jobs.format/format_label, machines.current_format - products.price_factor_a/e; Multiplikator-Formel auf a/e umgestellt inkl. verhaltensneutraler Migration bestehender Formel/Produkte skrift-app (Konfigurator): - Zeilenumbrüche im Textfeld erlaubt; Umschlag-Individualtext als Textfeld - Platzhalter-Tabelle im Text-Schritt immer verfügbar (auch Sammelversand ohne Kuvert) inkl. Excel-Upload nur für Platzhalterwerte - Danke-Seite: Button zurück zu skrift.de - z-index-Fix: Platzhalter-Tabelle überlagert die Vorschau nicht mehr WordPress-Preisrechner: a/e-Faktoren in PHP- und JS-Rechnung Directus: SKRIFT_WEBHOOK_TOKEN in compose/.env.example Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -168,29 +168,83 @@ def main():
|
||||
print(f"[Agent] gestartet für Maschine {machine_id} gegen {cfg['directus_url']}")
|
||||
|
||||
KIND = {"brief": "schriftstueck", "umschlag": "umschlag", "signatur": "signatur"}
|
||||
# Sicherheitsfenster: Sehen wir nach dem Senden binnen dieser Zeit kein
|
||||
# „Printing" (Job war sehr kurz), gilt er als fertig – verhindert Deadlock.
|
||||
start_grace = max(60, poll * 6)
|
||||
|
||||
# Kontext des aktuell an die Maschine gesendeten Jobs (oder None). Der
|
||||
# nächste Job wird ERST gesendet, wenn die Maschine wieder „Idle" ist –
|
||||
# sonst würde ein neuer write_svg (clear=1) den laufenden Druck verwerfen.
|
||||
laufend = None
|
||||
|
||||
# Heartbeat gedrosselt: nur schreiben, wenn sich der Status ändert oder
|
||||
# spätestens alle 60 s – sonst flutet der PATCH bei jedem Poll das Log.
|
||||
beat_state = {"status": None, "ts": 0.0}
|
||||
|
||||
def beat(status):
|
||||
now = time.monotonic()
|
||||
if status != beat_state["status"] or now - beat_state["ts"] >= 60:
|
||||
_heartbeat(dx, machine_id, status)
|
||||
beat_state["status"] = status
|
||||
beat_state["ts"] = now
|
||||
|
||||
while True:
|
||||
try:
|
||||
maschine = dx.get(f"/items/machines/{machine_id}",
|
||||
fields="base_url,mid,username,password,active")
|
||||
fields="base_url,mid,username,password,active,current_format")
|
||||
if not maschine or not maschine.get("active", True):
|
||||
_heartbeat(dx, machine_id, "offline")
|
||||
beat("offline")
|
||||
time.sleep(poll)
|
||||
continue
|
||||
|
||||
plotter = Plotter(maschine)
|
||||
mstatus = plotter.status()
|
||||
# Bei Fehler (z. B. Papierende) automatisch fortsetzen.
|
||||
if mstatus and str(mstatus.get("status", "")).lower() in ("error", "fehler"):
|
||||
plotter.recover()
|
||||
_heartbeat(dx, machine_id, "bereit")
|
||||
st = plotter.status() or {}
|
||||
s = str(st.get("status", "")).strip().lower()
|
||||
|
||||
job = _naechster_job(dx, machine_id)
|
||||
if not job:
|
||||
# Fehler (z. B. Papierende): fortsetzen, aber nichts Neues senden.
|
||||
if s in ("error", "fehler"):
|
||||
plotter.recover()
|
||||
beat("fehler")
|
||||
time.sleep(poll)
|
||||
continue
|
||||
|
||||
_druck(dx, plotter, job, KIND)
|
||||
# Maschine druckt noch → warten (auf keinen Fall einen neuen Job senden).
|
||||
if s == "printing":
|
||||
if laufend is not None:
|
||||
laufend["gestartet"] = True
|
||||
beat("druckt")
|
||||
time.sleep(poll)
|
||||
continue
|
||||
|
||||
# Nur bei sicherem „Idle" handeln. Unbekannt/leer → abwarten.
|
||||
if s != "idle":
|
||||
beat("bereit" if s else "offline")
|
||||
time.sleep(poll)
|
||||
continue
|
||||
|
||||
beat("bereit")
|
||||
|
||||
# Ein zuvor gesendeter Job ist fertig gedruckt (Idle nach Printing) →
|
||||
# abschließen. Das start_grace fängt sehr kurze Jobs ab, bei denen wir
|
||||
# das „Printing" nie zu Gesicht bekommen.
|
||||
if laufend is not None:
|
||||
if laufend.get("gestartet") or (time.monotonic() - laufend["ts"] > start_grace):
|
||||
_abschluss(dx, laufend)
|
||||
laufend = None
|
||||
else:
|
||||
time.sleep(poll)
|
||||
continue
|
||||
|
||||
# Nächsten Job nur senden, wenn nichts (mehr) läuft.
|
||||
if laufend is None:
|
||||
current_format = (maschine.get("current_format") or "").strip()
|
||||
# Format-Warteschlange: nur passendes Format, kein Auto-Anschluss.
|
||||
if current_format:
|
||||
job = _naechster_job(dx, machine_id, current_format)
|
||||
if job:
|
||||
laufend = _sende(dx, plotter, job, KIND)
|
||||
|
||||
time.sleep(poll)
|
||||
|
||||
except requests.HTTPError as e:
|
||||
print(f"[Agent] HTTP-Fehler: {e}")
|
||||
@@ -207,12 +261,14 @@ def _heartbeat(dx, machine_id, status):
|
||||
pass
|
||||
|
||||
|
||||
def _naechster_job(dx, machine_id):
|
||||
def _naechster_job(dx, machine_id, current_format):
|
||||
jobs = dx.get(
|
||||
"/items/jobs",
|
||||
**{
|
||||
"filter[machine][_eq]": machine_id,
|
||||
"filter[status][_eq]": "queued",
|
||||
# Nur Jobs des aktuell eingelegten Formats.
|
||||
"filter[format][_eq]": current_format,
|
||||
"sort": "-priority,date_created",
|
||||
"limit": 1,
|
||||
"fields": "id,type,output_ref,params,numbers,"
|
||||
@@ -224,7 +280,13 @@ def _naechster_job(dx, machine_id):
|
||||
return jobs[0] if jobs else None
|
||||
|
||||
|
||||
def _druck(dx, plotter, job, KIND):
|
||||
def _sende(dx, plotter, job, KIND):
|
||||
"""Sendet einen Job an die Maschine und markiert ihn als „printing".
|
||||
|
||||
Gibt einen Kontext zum späteren Abschluss zurück (oder None bei Fehler).
|
||||
Es wird NICHT auf das Druckende gewartet – das übernimmt die Hauptschleife
|
||||
(erst wenn die Maschine wieder „Idle" ist, wird der nächste Job gesendet).
|
||||
"""
|
||||
job_id = job["id"]
|
||||
order = job.get("order") or {}
|
||||
order_id = order.get("id")
|
||||
@@ -239,7 +301,7 @@ def _druck(dx, plotter, job, KIND):
|
||||
tpl[k] = params[k]
|
||||
nummern = parse_nummern(job.get("numbers"))
|
||||
typ = job.get("type") or "brief"
|
||||
print(f"[Agent] Job {job_id}: {typ} für {order_nr}")
|
||||
print(f"[Agent] Job {job_id}: {typ} für {order_nr} → an Maschine senden")
|
||||
|
||||
dx.patch(f"/items/jobs/{job_id}", {"status": "printing", "error": None})
|
||||
# Produktionsstatus fürs Kanban: sobald gedruckt wird → „im_druck".
|
||||
@@ -271,27 +333,44 @@ def _druck(dx, plotter, job, KIND):
|
||||
|
||||
plotter.login()
|
||||
plotter.write(dateien, tpl)
|
||||
|
||||
dx.patch(f"/items/jobs/{job_id}", {"status": "printed"})
|
||||
# Indikator am Auftrag setzen (Schriftstück bzw. Umschlag gedruckt) und
|
||||
# bei Vollständigkeit automatisch in die Kuvertierung schieben.
|
||||
if order_id:
|
||||
feld = "kuvert_gedruckt" if typ == "umschlag" else "brief_gedruckt"
|
||||
try:
|
||||
dx.patch(f"/items/orders/{order_id}", {feld: True})
|
||||
stand = dx.get(f"/items/orders/{order_id}",
|
||||
fields="brief_gedruckt,kuvert_gedruckt,needs_envelope,production_status") or {}
|
||||
braucht_kuvert = bool(stand.get("needs_envelope"))
|
||||
fertig = stand.get("brief_gedruckt") and (stand.get("kuvert_gedruckt") or not braucht_kuvert)
|
||||
if fertig and stand.get("production_status") == "im_druck":
|
||||
dx.patch(f"/items/orders/{order_id}", {"production_status": "kuvertieren"})
|
||||
print(f"[Agent] Auftrag {order_nr} vollständig gedruckt → Kuvertierung.")
|
||||
except Exception:
|
||||
pass
|
||||
print(f"[Agent] Job {job_id} gedruckt ({len(dateien)} Dateien).")
|
||||
print(f"[Agent] Job {job_id} an Maschine übergeben ({len(dateien)} Dateien) – warte auf Fertigstellung.")
|
||||
return {"job_id": job_id, "order_id": order_id, "order_nr": order_nr,
|
||||
"typ": typ, "gestartet": False, "ts": time.monotonic()}
|
||||
except Exception as e:
|
||||
dx.patch(f"/items/jobs/{job_id}", {"status": "failed", "error": str(e)[:1000]})
|
||||
print(f"[Agent] Job {job_id} fehlgeschlagen: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _abschluss(dx, ctx):
|
||||
"""Schließt einen fertig gedruckten Job ab: Status, Indikatoren, Kuvertierung.
|
||||
|
||||
Wird erst aufgerufen, wenn die Maschine nach dem Druck wieder „Idle" meldet.
|
||||
"""
|
||||
job_id = ctx["job_id"]
|
||||
order_id = ctx.get("order_id")
|
||||
order_nr = ctx.get("order_nr")
|
||||
typ = ctx.get("typ")
|
||||
try:
|
||||
dx.patch(f"/items/jobs/{job_id}", {"status": "printed"})
|
||||
except Exception as e:
|
||||
print(f"[Agent] Konnte Job {job_id} nicht auf 'printed' setzen: {e}")
|
||||
# Indikator am Auftrag setzen (Schriftstück bzw. Umschlag gedruckt) und
|
||||
# bei Vollständigkeit automatisch in die Kuvertierung schieben.
|
||||
if order_id:
|
||||
feld = "kuvert_gedruckt" if typ == "umschlag" else "brief_gedruckt"
|
||||
try:
|
||||
dx.patch(f"/items/orders/{order_id}", {feld: True})
|
||||
stand = dx.get(f"/items/orders/{order_id}",
|
||||
fields="brief_gedruckt,kuvert_gedruckt,needs_envelope,production_status") or {}
|
||||
braucht_kuvert = bool(stand.get("needs_envelope"))
|
||||
fertig = stand.get("brief_gedruckt") and (stand.get("kuvert_gedruckt") or not braucht_kuvert)
|
||||
if fertig and stand.get("production_status") == "im_druck":
|
||||
dx.patch(f"/items/orders/{order_id}", {"production_status": "kuvertieren"})
|
||||
print(f"[Agent] Auftrag {order_nr} vollständig gedruckt → Kuvertierung.")
|
||||
except Exception:
|
||||
pass
|
||||
print(f"[Agent] Job {job_id} fertig gedruckt.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user