From 67a89d17945da6f5d278f55302a096b710c5ba5d Mon Sep 17 00:00:00 2001 From: Dominic Gundel Date: Sun, 18 Oct 2026 20:10:32 +0200 Subject: [PATCH] =?UTF-8?q?Grotti=203.3.0:=20Projekte=20mit=20Ordner,=20Da?= =?UTF-8?q?tei-L=C3=B6schen/Verschieben,=20Material-Archiv,=20UI-Polish.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 33 +++ docs/Anleitung-ACE.md | 18 +- docs/Anleitung-Auftraege.md | 1 + docs/Anleitung-Dateien.md | 21 +- docs/Anleitung-Einrichtung.md | 2 +- docs/Anleitung-Einstellungen.md | 2 +- docs/Anleitung-Material.md | 10 +- docs/Anleitung-Oberflaeche.md | 5 +- docs/Anleitung-Orca.md | 49 +++- docs/Anleitung-Projekte.md | 33 +++ docs/Anleitung-Wand.md | 5 +- docs/Anleitung-Warteschlange.md | 2 +- docs/community/10-druckbericht-strom.png | Bin 0 -> 199016 bytes docs/internal/Abraeumen.md | 2 +- docs/internal/Dateien-Metadaten.md | 2 +- docs/internal/Features-UI.md | 2 +- docs/internal/Ops-Queue-Alerts.md | 2 +- grotti/acm.py | 173 +++++++++++- grotti/jobs.py | 66 ++++- grotti/library.py | 281 ++++++++++++++++++-- grotti/migrations/012_spool_archived.sql | 2 + grotti/migrations/013_project_folder.sql | 2 + grotti/parse.py | 89 ++++++- grotti/print_queue.py | 50 +++- grotti/schema.sql | 4 + grotti/store.py | 320 ++++++++++++++++++++--- grotti/usage.py | 20 +- grotti/version.py | 2 +- grotti/watcher.py | 15 +- scripts/orca_senden.py | 188 ++++++++++++- tests/test_jobs_spool.py | 41 +++ tests/test_material_routing.py | 80 ++++++ tests/test_orca_senden.py | 84 ++++++ tests/test_paint_index.py | 171 ++++++++++++ web/app.py | 165 +++++++++++- web/static/grotti.css | 209 ++++++++++++++- web/templates/bericht.html | 44 ++-- web/templates/dateien.html | 289 +++++++++++++++----- web/templates/doku.html | 8 +- web/templates/layout.html | 6 +- web/templates/material.html | 123 ++++++++- web/templates/projekte.html | 311 ++++++++++++++++++++++ web/templates/protokoll.html | 22 +- web/templates/statistik.html | 69 ++--- web/templates/wand.html | 153 ++++++++--- 45 files changed, 2856 insertions(+), 320 deletions(-) create mode 100644 docs/Anleitung-Projekte.md create mode 100644 docs/community/10-druckbericht-strom.png create mode 100644 grotti/migrations/012_spool_archived.sql create mode 100644 grotti/migrations/013_project_folder.sql create mode 100644 tests/test_material_routing.py create mode 100644 tests/test_paint_index.py create mode 100644 web/templates/projekte.html diff --git a/CHANGELOG.md b/CHANGELOG.md index a7d7165..14bfed2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,39 @@ Alle Änderungen an Grotti. Format nach [Keep a Changelog](https://keepachangelo ## [Unreleased] +## [3.3.0] – 2026-10-18 + +### Neu + +- **Projekte** `/projekte`: Arbeitsmappe mit Ordner unter dem Slice-Pfad (`projects.folder`); G-code-Zuordnung per Sync; Upload weiterer Dateien (STEP/STL/PDF/…); Nav unter Verwalten. +- **Dateien:** Uploads in Slice-Ordner verschieben (werden Watch); Löschen für Upload und Watch (nur unter Slice/`eingang`); kompaktere Bibliothek-Filter. +- **Material:** Filter Sorte/Farbe; leere/archivierte Spulen ausgeblendet (Filter **Archiv**); Buttons **Leer**, **Archivieren**, **Wiederherstellen** (`archived_at`). +- **Wand:** kleines Kamera-Snapshot-Thumb auf jeder Karte (nie Stream); Menüpunkt Kameras entfernt, `/kameras` → Wand. + +### Geändert + +- **Technik-Cockpit:** kompakte Fact-Chips; Steuerung in Gruppen Heizung / Druck / ACE-Plätze. +- **Protokoll:** kurze Dateinamen, festere Spaltenbreiten. +- **Statistik:** kompakte Filter inkl. Farbe, Auto-Laden, KPI Primär/Sekundär. +- **Orca-Login:** Session 30 Tage (überlebt App-Fenster schließen); kein zweites Chrome mit demselben Profil (sonst erneutes Anmelden). +- **Druckbericht:** Kosten-Torte und Tabelle inkl. Filament (nicht nur Strom); Summe = Material + Energie. +- **Dateien:** Starten / Einreihen / Ablauf mit einem Klick (kein Zwei-Klick mehr). API weiter mit `confirm: true`. +- **Senden:** Zwei Start-Modi statt drei — **Dieser Drucker** (frei → sofort, sonst Warteschlange) und **Nächster passender** (Grotti wählt nach Sorte). +- **Material-Routing:** Match auf Katalog-Sorte aus Orca-`filament_settings_id` (PLA Meta ≠ Matte ≠ Silk); falsche Variante wartet mit Grund. Farbe bleibt soft. Bei Auto-Wahl baut Grotti die ACE-Map aus den Spulen am Claim-Drucker. Bestehende Dateien: **Dateien neu lesen**. +- **Wand:** Bei Filament-Pause Hinweis, die neue Rolle unter Material dem Drucker zuzuweisen — sonst zählt der Verbrauch weiter zur alten Spule. +- Anleitung Material/ACE: Rollenwechsel während Pause klargestellt. + +### Behoben + +- **Orca/Anycubic öffnet zwei Browserfenster:** Nachbearbeitung läuft oft 2× — Debounce (~25 s) öffnet nur noch eines. Deep-Link lädt fehlende `paint_index` nach (ACE zeigt sonst fälschlich T0/T1). +- **Dateien-Tab hängt nach Orca:** 3D-Pfad erst nach ACE/Senden (Idle); Debounce ignoriert Anycubic-Zeitstempel im Dateinamen; erneuter Export aktualisiert den offenen Tab statt ein zweites Fenster. +- **Orca/Anycubic → Desktop-Fenster:** `orca_senden` öffnet Grotti als Chrome/Edge-App (`browser: app`). Anycubic-Dialog nur vermeiden durch **G-code exportieren**, nicht Remote Print / Aufgabe starten. +- Anleitung Orca: Grotti unter **Gerät** per Device-UI-URL (`http://127.0.0.1:8770/`); Upload-zum-Host weiter meiden. +- **ACE-Map mit Lücken-T (Bambu/Anycubic):** UI behält Slicer-`Tn` (z. B. T1+T10). Beim Senden verdichtet Grotti auf T0…n und schreibt passende `.acm` — sonst gklib **10011700 invalid filament id** bzw. Hang bei Map-Mismatch. Senden prüft Map gegen T-Befehle. Bestehende Dateien: **Dateien neu lesen**. +- **10011700 trotz dichter UI-Map:** Verdichtung hängt am G-code (`T1`/`T10`), nicht daran, ob die Map schon 0/1 zeigt — sonst blieb T10 auf dem Drucker. Grotti neu starten, Clip erneut senden. +- **Abräumen ließ Lauf offen:** Urteil und Platte waren quittiert, der Live-Lauf blieb `in_progress` (Moonraker-History oft ohne Endzeit). Abräumen und Watcher (`printing`→`complete`) schließen den Lauf jetzt als `completed` inkl. Materialabzug. +- **Rollenwechsel während Pause:** Einlegen hängt Abschnitte/Ereignisse auch an offene History-Jobs (nicht nur `live:`). Send-Spule und Lauf-Spule werden mitgezogen; Abzug bevorzugt die Rolle am Drucker statt eines veralteten Send-Hints. + ## [3.2.0] – 2026-10-18 ### Neu diff --git a/docs/Anleitung-ACE.md b/docs/Anleitung-ACE.md index 7850feb..5ea6563 100644 --- a/docs/Anleitung-ACE.md +++ b/docs/Anleitung-ACE.md @@ -12,27 +12,41 @@ Stand **18.10.2026**. Mehrfarb-Drucke brauchen vor dem Start eine Zuordnung Date Es gibt **kein freies Spulen-Dropdown** beim Start: Grotti nimmt die Rolle(n), die am Ziel-Drucker liegen. Material-Mismatch oder zu wenig Rest → Warnung vor dem Start. +### Rollenwechsel während Pause + +Während Druck oder Pause die neue Spule unter Material dem Drucker zuweisen und speichern. Grotti schließt den Abschnitt der alten Rolle und öffnet einen neuen — im Druckbericht erscheinen dann zwei Spulenzeilen und Ereignisse. Nur Filament am Gerät laden ohne Zuweisung: Abzug und Protokoll bleiben bei der alten Rolle. + +Wenn Moonraker den Druck nach Filament-Pause als neuen History-Job führt, übernimmt Grotti die neu zugewiesene Rolle für den fortgesetzten Teil (Send- und Lauf-Spule werden beim Einlegen aktualisiert). + Etiketten mit QR: [Material und mehr](Anleitung-Material.md). ## ACE-Map beim Senden Unter **Dateien → Optionen → ACE-Zuordnung**: -- Jede Farbe aus der Datei (`paint_index`) einem **ACE-Platz** zuordnen -- Liegt unter Material eine Spule auf dem Platz, zeigt die Liste Name, Farbe und Restgramm mit +- Jede Farbe aus der Datei (`Tn` / Slicer-`paint_index`) einem **ACE-Platz** zuordnen +- Bei Bambu-/Anycubic-Dateien sind die Nummern oft **nicht 0–3** (z. B. T1 und T10 bei 16 Slots) — Grotti zeigt `T…`, verdichtet beim Senden aber auf T0…n (sonst Fehler **invalid filament id**) +- Liegt unter Material eine Spule auf dem Platz, zeigt die Liste Name, Farbe und Restgramm mit — Vorauswahl bevorzugt die **Katalog-Sorte** aus dem Orca-Profil (Meta/Matte …) - Ohne passende Map schreibt der Drucker oft **„cannot parse the file“** und startet nicht - Grotti legt die Sidecar `*.gcode.acm` an und schickt sie mit +Bei **Nächster passender** (kein Drucker gewählt) baut Grotti die Map erst beim Claim aus den Spulen am gewählten Drucker. Passt die Sorte nicht → Wartegrund in der Queue. + Externe Spule / keine ACE-Slots: keine `.acm` nötig. +Nach einem Grotti-Update mit ACE-Farben: unter Dateien **neu lesen**, damit alte Einträge die echten `Tn` bekommen. + ## Typische Fehler | Symptom | Check | | --- | --- | | „cannot parse the file“ | ACE-Zuordnung vollständig? Farben richtig? | +| „T… fehlt in der Map“ | Datei neu lesen, Zuordnung erneut setzen (Slicer-T ≠ ACE-Platz) | +| **invalid filament id** (10011700) | Alte Grotti-Version ohne T-Verdichtung, oder Map nicht neu gesetzt. Grotti neu starten, Datei neu lesen, nochmal senden. | | Material-Warnung | Eingelegte Sorte ≠ Datei. Der Start geht trotzdem. PA12-CF steht in Grotti so, an den ACE geht PA6-CF. | | Rest zu gering | Andere Spule oder leichteres Teil | | ACE blinkt nicht / kein Wechsel | Spule und Platz prüfen, ggf. Druck neu starten | +| Drucker hängt kurz nach Multi-Color-Start | Oft falsche Map (T10 gemappt als Platz 0). Neu lesen + Zuordnung. | ## Weiter diff --git a/docs/Anleitung-Auftraege.md b/docs/Anleitung-Auftraege.md index 1ffb672..8c09a6c 100644 --- a/docs/Anleitung-Auftraege.md +++ b/docs/Anleitung-Auftraege.md @@ -14,6 +14,7 @@ Im Auftrag **Lieferschein** öffnen: Positionen, Chargen `D-…`, druckbar. Dana ## Weiter +- [Projekte](Anleitung-Projekte.md) (Arbeitsmappe / Konstruktion — nicht dasselbe wie Auftrag) - [Warteschlange](Anleitung-Warteschlange.md) - [Material](Anleitung-Material.md) - [Protokoll / Bericht](Anleitung-Oberflaeche.md) diff --git a/docs/Anleitung-Dateien.md b/docs/Anleitung-Dateien.md index 8fef208..96c59e8 100644 --- a/docs/Anleitung-Dateien.md +++ b/docs/Anleitung-Dateien.md @@ -1,31 +1,33 @@ # Dateien senden -Stand **17.10.2026**. Seite `/dateien` — Bibliothek links, Senden rechts. +Stand **18.10.2026**. Seite `/dateien` — Bibliothek links, Senden rechts. ![Dateien: Bibliothek und Senden](../web/static/doku/dateien-flow.png) ## Bibliothek -- Suche nach Name, Material und Projekt +- Kompakte Filterleiste: Suche, Maschine, Projekt, Material, Ordner, Sortierung - Filter: Maschine, Projekt, Material, Ordner (Unterordner unter dem Slice-Ordner; Uploads als „Upload“) - Sortierung: Neueste, Name, Material, Ordner, Druckzeit, Gewicht — bei Material/Ordner mit Gruppenköpfen - Auswahl der Filter bleibt im Browser merken - Vorschaubild, Material, Zeit, Gewicht, Schichthöhe - Info-Popup mit Slice-Metadaten (Kopf und Footer) - **Hochladen** per Knopf oder Drag & Drop -- Mit gesetztem Slice-Ordner ([Einstellungen](Anleitung-Einstellungen.md)): **Ordner anlegen**, Watch-Dateien per Auswahl **verschieben** (Datei bleibt in der Bibliothek dieselbe, inkl. Projekt) +- Mit gesetztem Slice-Ordner ([Einstellungen](Anleitung-Einstellungen.md)): **Ordner anlegen**; Watch- **und** Upload-Dateien per Auswahl **verschieben** (Upload wird zur Watch-Datei im Slice-Ordner) +- **Löschen** (Papierkorb, Zwei-Klick) für jede Bibliotheksdatei — entfernt Eintrag und Datei auf der Platte (nur unter Slice-Ordner oder `eingang`) - **Ordner neu lesen**, wenn unter Einstellungen ein Slice-Ordner gesetzt ist -Ohne Slice-Ordner geht nur manueller Upload — das reicht für den Alltag. Uploads liegen flach unter „Upload“ und lassen sich in der Bibliothek nicht in Slice-Ordner schieben. +Ohne Slice-Ordner geht nur manueller Upload; Verschieben in Ordner braucht den Slice-Pfad. + +Projekte mit eigenem Ordner: [Projekte](Anleitung-Projekte.md). ## Senden: drei Schritte 1. **Datei** wählen (Klick in der Liste). 2. **Drucker** wählen — Kacheln zeigen Live-Zustand und eingelegte Rolle. Passt die Maschine nicht, ist die Kachel blass. -3. **Wann:** - - **Jetzt** — sofort senden und starten (Zwei-Klick) - - **Wenn frei** — in die Warteschlange für diesen Drucker - - **Nächster freier** — erster passender freier Drucker +3. **Start:** + - **Dieser Drucker** — frei und Platte klar → sofort starten; sonst Warteschlange für diesen Drucker + - **Nächster passender** — Grotti wählt den nächsten freien Drucker mit passender Sorte (z. B. Sunlu PLA Meta, nicht Matte) Auswahl merkt sich der Browser (`localStorage`). @@ -44,7 +46,7 @@ Große Dateien brauchen oft **eine Minute**. In der Sidebar unter **Senden** sie ## Nach dem Start - Fortschritt auf der [Wand](Anleitung-Wand.md) und in der Sidebar **Läuft jetzt** -- Bei „Wenn frei“: Eintrag unter [Warteschlange](Anleitung-Warteschlange.md) +- Bei Warteschlange / Nächster passender: Eintrag unter [Warteschlange](Anleitung-Warteschlange.md) - Später unter **Protokoll** mit **Nochmal** (inkl. gespeicherter ACE-Map) ## Aus Orca @@ -53,6 +55,7 @@ Nach dem G-code-Export kann Orca die Datei direkt nach Grotti laden und diese Se ## Weiter +- [Projekte](Anleitung-Projekte.md) - [Orca an Grotti](Anleitung-Orca.md) - [ACE und Spulen](Anleitung-ACE.md) - [Warteschlange](Anleitung-Warteschlange.md) diff --git a/docs/Anleitung-Einrichtung.md b/docs/Anleitung-Einrichtung.md index dadffd4..56e44c4 100644 --- a/docs/Anleitung-Einrichtung.md +++ b/docs/Anleitung-Einrichtung.md @@ -54,7 +54,7 @@ Dann öffnen. Beim ersten Start legst du den Admin-Benu ## Sicherheit -- Druckstart nur mit Bestätigung (Zwei-Klick in der UI bzw. `confirm: true` in der API). +- Druckstart über die API nur mit `confirm: true` (Dateien-UI: ein Klick). - Keine freie G-code-Konsole; Firmware-Neustart über Grotti ist gesperrt. - Benutzer und API-Tokens unter Einstellungen. Für Zugriff aus dem Internet: Reverse Proxy mit TLS, nicht den Entwickler-Server öffentlich freigeben. - `config.json` und `data/` bleiben lokal, nicht im Git. diff --git a/docs/Anleitung-Einstellungen.md b/docs/Anleitung-Einstellungen.md index bdd5b36..d95a1df 100644 --- a/docs/Anleitung-Einstellungen.md +++ b/docs/Anleitung-Einstellungen.md @@ -12,7 +12,7 @@ Stand **18.10.2026**. Seite `/einstellungen` — Drucker, Automatik, Anzeige, Te ## Automatik -Checkbox **Automatik-Warteschlange** (`auto_queue`). Aus = keine neuen „wenn frei“-Starts. Laufende Abläufe und manuelle „Jetzt“-Starts bleiben möglich. +Checkbox **Automatik-Warteschlange** (`auto_queue`). Aus = keine neuen Queue-Starts. Laufende Abläufe und manuelle Sofort-Starts bleiben möglich. ## Slice-Ordner diff --git a/docs/Anleitung-Material.md b/docs/Anleitung-Material.md index f7f3c3f..d99269a 100644 --- a/docs/Anleitung-Material.md +++ b/docs/Anleitung-Material.md @@ -1,6 +1,6 @@ # Material und mehr -Stand **18.10.2026**. Spulen, Etiketten, Aufträge, Protokoll, Statistik, Kameras, Aktivität. +Stand **18.10.2026**. Spulen, Etiketten, Aufträge, Protokoll, Statistik, Aktivität. ![Material: Spulen](../web/static/doku/material-spule.png) @@ -10,7 +10,7 @@ Zwei Reiter: **Spulen** und **Sorten**. Oben die Karte **Bedarf aus Warteschlange**, wenn wartende Jobs Filament brauchen: Queue-Gewicht vs. Spulenrest je Sorte/Farbe. Fehlmengen erscheinen auch in der Sidebar und als Chip in der Warteschlange. -**Spulen:** Filter Alle / Am Drucker / Regal / Wenig Rest. Formular bei Auswahl oder „Neue Spule“. Ort = Drucker · ACE/extern oder Regal. Scan-Feld im Seitenkopf (QR vom Etikett). Unter der ausgewählten Spule die **Akte**: verdruckte Gramm, Ausschuss, Leer-in. +**Spulen:** Filter Alle / Am Drucker / Regal / Wenig Rest / Archiv; zusätzlich Sorte und Farbe. Leere und archivierte Spulen sind standardmäßig ausgeblendet — **Archiv** zeigt sie. Formular bei Auswahl oder „Neue Spule“. **Leer** setzt Rest auf 0, legt die Rolle ins Regal und archiviert; **Archivieren** / **Wiederherstellen** ohne Restzwang. Ort = Drucker · ACE/extern oder Regal. Scan-Feld im Seitenkopf (QR vom Etikett). Unter der ausgewählten Spule die **Akte**: verdruckte Gramm, Ausschuss, Leer-in. **Sorten:** Profile (Dichte, Preis, Maschinen). „Spule damit anlegen“ wechselt zum Spulen-Reiter. @@ -26,9 +26,9 @@ Unter **Aufträge** Kunden, Positionen und Fortschritt. Dateien lassen sich eine ## Protokoll und Statistik -Abgeschlossene Läufe: Dauer, Ist-Gramm, Kosten, Urteil. Zeile öffnet den **Druckbericht** (Soll/Ist, Spulen, Temperaturverlauf, Strom-Aufschlüsselung mit Tortendiagramm, Charge). **Nochmal** startet denselben Job inkl. gespeicherter ACE-Map und Bettnetz-Option. +Abgeschlossene Läufe: Dauer, Ist-Gramm, Kosten, Urteil. Zeile öffnet den **Druckbericht** (Soll/Ist, Spulen, Temperaturverlauf, Kosten-Aufschlüsselung Filament + Strom mit Tortendiagramm, Charge). **Nochmal** startet denselben Job inkl. gespeicherter ACE-Map und Bettnetz-Option. -Nach Filament-Ende: neue Spule vom Regal dem Drucker/ACE-Platz zuweisen, am Gerät laden, an der Wand **Weiter**. +Nach Filament-Ende: neue Spule unter **Material** dem Drucker/ACE-Platz zuweisen (Ort setzen und speichern — das legt den Rollenwechsel im Lauf an), am Gerät laden, an der Wand **Weiter**. Nur physisch nachlegen reicht nicht. Die Zuweisung gilt auch, wenn Moonraker den Druck nach der Pause als neuen Job zählt. Unter **Statistik** KPIs, Balkendiagramme (Druckzeit je Drucker, Verbrauch je Material), Tabellen und CSV-Export. Zeitraum per Knopf oder Von/Bis. @@ -38,7 +38,7 @@ Abzug nach dem Druck nimmt das Ist aus `filament_used` (Abbruch nur anteilig). M ## Kameras -`/kameras` — alle Kameras nebeneinander mit Live-Zustand. Oben **Snapshot** oder **Stream** wählbar (gilt für diesen Browser, auch im Cockpit). Stream ist flüssiger, hält aber je sichtbarer Kamera eine Verbindung. +Auf der **Druckerwand** erscheint je Drucker ein kleines Snapshot-Bild (ca. alle 3 s), wenn eine Webcam erreichbar ist. Klick öffnet das Cockpit — dort gilt weiter die Einstellung Snapshot/Stream unter Einstellungen → Dieser Browser. Die frühere Seite `/kameras` leitet auf die Wand um. ## Aktivität diff --git a/docs/Anleitung-Oberflaeche.md b/docs/Anleitung-Oberflaeche.md index 4fecdff..db71bd7 100644 --- a/docs/Anleitung-Oberflaeche.md +++ b/docs/Anleitung-Oberflaeche.md @@ -4,7 +4,7 @@ Stand **18.10.2026**. Wie die App aufgebaut ist — Menü, Jetzt-Liste, Desktop ## Navigation -Links (Desktop), gruppiert: **Live** (Drucker, Dateien, Kameras, Warteschlange, Aufträge), **Rückschau** (Protokoll, Statistik, Aktivität), **Verwalten** (Material, Wartung). Darüber im Inhaltsbereich die **Topbar**: Suche, Live-Chips, Einstellungen, Doku und Benutzer-Menü (Abmelden, Version). +Links (Desktop), gruppiert: **Live** (Drucker, Dateien, Warteschlange, Aufträge), **Rückschau** (Protokoll, Statistik, Aktivität), **Verwalten** (Projekte, Material, Wartung). Darüber im Inhaltsbereich die **Topbar**: Suche, Live-Chips, Einstellungen, Doku und Benutzer-Menü (Abmelden, Version). Unter ca. 760 px: untere Leiste (Drucker, Dateien, Warteschlange, **Jetzt**, Mehr) plus **Mehr**-Sheet (inkl. Aktivität und Abmelden). Das Seitenmenü lässt sich zur Icon-Rail einklappen. @@ -36,7 +36,7 @@ Unter **Einstellungen → Dieser Browser**: - **Regal (Touch)** — größere Bedienelemente, Sidebar oft eingeklappt; auf schmalem Viewport die untere Leiste. Alle Menüpunkte bleiben in der Sidebar sichtbar. - **Darstellung** — Dunkel, Hell oder System - **Zeitzone** — Browser oder fest (z. B. Europe/Berlin) für Uhrzeiten in Protokoll und Bericht -- **Kamera** — Snapshot (schonend, alle paar Sekunden) oder Stream (flüssig, hält eine Verbindung je Kamera). Auch umschaltbar unter Kameras. +- **Kamera** — Snapshot (schonend, alle paar Sekunden) oder Stream (flüssig, hält eine Verbindung). Gilt für das Cockpit; Wand-Thumbs sind immer Snapshot. Shortcut in der URL: `?shelf=1` bzw. `?shelf=0`. Nur dieser Browser (`localStorage`). @@ -46,3 +46,4 @@ Drucker-Cockpit: Alltag (Job, ETA, Rolle, Steuerung der Aktionen) oben; **Techni - [Druckerwand](Anleitung-Wand.md) - [Dateien senden](Anleitung-Dateien.md) +- [Projekte](Anleitung-Projekte.md) diff --git a/docs/Anleitung-Orca.md b/docs/Anleitung-Orca.md index 6f8ea86..83b9cc4 100644 --- a/docs/Anleitung-Orca.md +++ b/docs/Anleitung-Orca.md @@ -1,6 +1,6 @@ # Orca an Grotti -Stand **17.10.2026**. Nach dem Export in OrcaSlicer landet der G-code in der Bibliothek und die Seite [Dateien senden](Anleitung-Dateien.md) öffnet sich mit der Datei schon gewählt. Drucker, ACE-Platz und Einreihen wählst du dort wie gewohnt. +Stand **18.10.2026**. Nach dem Export landet der G-code in der Bibliothek und Grotti öffnet sich als **eigenes Fenster** (Chrome/Edge-App auf dem Desktop). Drucker, ACE-Platz und Einreihen wählst du dort. ## Einmal einrichten @@ -11,11 +11,13 @@ Stand **17.10.2026**. Nach dem Export in OrcaSlicer landet der G-code in der Bib ```json { "url": "http://127.0.0.1:8770", - "token": "grt_…" + "token": "grt_…", + "browser": "app" } ``` -`url` ist die Adresse, unter der du Grotti im Browser öffnest. +`url` ist die Adresse, unter der du Grotti im Browser öffnest. +`browser`: `app` (Standard) = eigenes Desktop-Fenster; `tab` = klassischer Browser-Tab. 4. Im **Prozessprofil** (Druckqualität, z. B. „0.20mm Standard“): **Sonstiges → Nachbearbeitungsskripte**, eine Zeile: @@ -25,13 +27,31 @@ D:\Projekte\Grotti\.venv\Scripts\python.exe D:\Projekte\Grotti\scripts\orca_send Gilt für **OrcaSlicer** und **AnycubicSlicerNext**. Pfade anpassen. Der Slicer hängt den Export-Pfad selbst an. -Unter **Gerät / Drucker** keinen Print-Host (Moonraker) eintragen — sonst geht der Job am Grotti-Senden vorbei. +### Gerät-Reiter = Grotti (nur OrcaSlicer) -## Ablauf +Damit unter **Gerät** die Grotti-Oberfläche erscheint: -1. In Orca slicen und **G-code exportieren** (Platte schreiben). Als Ziel den Slice-Ordner wählen — **Unterordner** (z. B. `slice\Gridfinity\…`) gehen: Grotti legt die Datei dort ab und liest den Watch neu. Export außerhalb des Slice-Ordners landet als Upload (ohne Ordner-Verschieben). -2. Der Browser öffnet `/dateien?file=…` mit der neuen Datei. -3. Drucker wählen, unter Optionen Farbe → ACE-Platz, **Jetzt** / **Wenn frei** / **Nächster freier**, dann **Starten**. +1. Orca → Drucker-Dropdown → **Physikalischer Drucker** (Verbindung/Stecker-Symbol). +2. **Host-Typ:** z. B. `Octo/Klipper` (egal, solange du nicht „Drucken“ an den Host schickst). +3. **Hostname / API:** leer lassen — oder nur eine Dummy-Adresse. **Nicht** die Moonraker-IP eines Kobras eintragen. +4. **Device UI / Web-UI-URL:** `http://127.0.0.1:8770/` (Grotti muss laufen). +5. Speichern → Reiter **Gerät** öffnen → einmal bei Grotti anmelden. + +Wichtig: Den Orca-Button **Drucken / Upload zum Host** nicht nutzen — der spricht Moonraker/OctoPrint und umgeht ACE-Zuordnung in Grotti. Weiterhin **G-code exportieren** + Nachbearbeitungsskript. + +In **AnycubicSlicerNext** steckt unter Gerät/Arbeitsfläche die Anycubic-Cloud — dort lässt sich Grotti so nicht einbinden. + +## Ablauf (wichtig bei Anycubic) + +**Nicht** „Remote Print“ / „Aufgabe starten“ / Cloud-Dialog — der Anycubic-Dialog lässt sich von Grotti nicht abschalten. + +Stattdessen: + +1. Slicen, dann **G-code exportieren** / **Platte schreiben** (Ziel: Slice-Ordner; Unterordner ok). +2. Das Nachbearbeitungsskript lädt die Datei nach Grotti und öffnet das Grotti-Fenster. +3. Drucker wählen, unter Optionen Farbe → ACE-Platz, **Dieser Drucker** oder **Nächster passender**, dann **Starten**. + +Erstes App-Fenster: einmal bei Grotti anmelden (eigenes Profil unter `data/grotti-app-profile`). Die Session hält **30 Tage** und überlebt das Schließen des Fensters. Läuft das Fenster noch, startet Orca kein zweites Chrome (sonst leeres Profil → erneutes Login) — die neue Datei wird im offenen Fenster ausgewählt. Test ohne Orca: @@ -43,11 +63,20 @@ D:\Projekte\Grotti\.venv\Scripts\python.exe D:\Projekte\Grotti\scripts\orca_send | Symptom | Check | | --- | --- | +| Anycubic-Dialog trotz Grotti | Du hast Remote Print / Aufgabe starten genutzt → nur **G-code exportieren** | | Meldungsfenster „Config fehlt“ | `data/orca-senden.json` anlegen | | „Grotti nicht erreichbar“ | Server läuft? `url` stimmt? | | „Token fehlt oder stimmt nicht“ | Token neu anlegen, in der JSON eintragen | -| Browser ohne Datei / Login | Einmal anmelden; Deep-Link behält `?file=` nach dem Login | -| Skript läuft nicht | Export wirklich speichern; Zeile im **Druckerprofil**; Python-Pfad prüfen | +| App-Fenster will Login | Einmal anmelden (Session 30 Tage). Profil: `data/grotti-app-profile`. Zweites Chrome-Fenster schließen, nur eines offen lassen. Grotti neu starten nach Update. | +| Browser-Tab statt Fenster | In `orca-senden.json` `"browser": "app"` setzen (oder Chrome/Edge fehlt) | +| Zwei Fenster / Tab hängt | Debounce (~25 s); erneuter Export aktualisiert den offenen Tab. 3D-Pfad lädt verzögert | +| ACE zeigt T0/T1 statt T1/T10 | Seite neu laden oder **Ordner neu lesen**; Deep-Link zieht `paint_index` nach | +| Skript läuft nicht | Export wirklich speichern; Zeile im **Prozessprofil**; Python-Pfad prüfen | + +## Was Grotti nicht kann + +- Den **Anycubic-Cloud-/Aufgabe-Dialog** unterdrücken (liegt im Slicer, nicht im Skript). +- Sich in Anycubics Reiter **Arbeitsfläche** einbetten. In **Orca** dagegen: Device-UI-URL → Grotti unter **Gerät** (siehe oben). ## Weiter diff --git a/docs/Anleitung-Projekte.md b/docs/Anleitung-Projekte.md new file mode 100644 index 0000000..6a90e03 --- /dev/null +++ b/docs/Anleitung-Projekte.md @@ -0,0 +1,33 @@ +# Projekte + +Stand **18.10.2026**. Arbeitsmappen unter `/projekte` — getrennt von Kunden-[Aufträgen](Anleitung-Auftraege.md). + +## Wozu + +Ein **Projekt** bündelt Konstruktion und Druckdateien in einem Ordner unter dem [Slice-Pfad](Anleitung-Einstellungen.md), z. B. `Projekte/2609_002`. + +| | Projekt | Auftrag | +| --- | --- | --- | +| Zweck | Konstruktion + Slice-Dateien | Kunde, Menge, Lieferschein | +| Ort | Ordner auf der Platte | nur in der Datenbank | +| Seite | `/projekte` | `/auftraege` | + +## Anlegen + +**Neues Projekt** — Name, Status, optional Ordner (Standard: `Projekte/`). Mit gesetztem Slice-Ordner legt Grotti den Ordner an. + +G-code und 3MF in diesem Ordner (oder dorthin [verschoben](Anleitung-Dateien.md)) werden dem Projekt zugeordnet und erscheinen in der Druck-Bibliothek. + +## Weitere Dateien + +Im Projekt-Detail: Upload von STEP, STL, PDF, SolidWorks usw. — liegen im selben Ordner, erscheinen **nicht** in der Druck-Bibliothek. Download und Löschen pro Datei. + +## Löschen + +Projekt löschen (Zwei-Klick) entfernt den Datensatz und den Ordner **inkl. Inhalt** (`force`). Druckdateien in der Bibliothek verlieren nur die Projekt-Zuordnung, wenn sie woanders liegen; Dateien im Projektordner sind mit dem Ordner weg. + +## Weiter + +- [Dateien](Anleitung-Dateien.md) +- [Aufträge](Anleitung-Auftraege.md) +- [Einstellungen](Anleitung-Einstellungen.md) diff --git a/docs/Anleitung-Wand.md b/docs/Anleitung-Wand.md index 91e9df9..7568e7c 100644 --- a/docs/Anleitung-Wand.md +++ b/docs/Anleitung-Wand.md @@ -9,6 +9,7 @@ Stand **18.10.2026**. Startseite `/` — eine Karte je Drucker. Klick öffnet da Pro Drucker siehst du: - Zustand (Chip), Fertig-Uhrzeit, Fortschrittsbalken +- Kleines Kamerabild (Snapshot ca. alle 3 s), wenn eine Webcam erreichbar ist — Klick öffnet das Cockpit - Restzeit, Schicht, Laufzeit - Temperaturen Düse/Bett (mit Ziel) - Dateiname, bei ACE oft Farb-Swatches @@ -34,11 +35,11 @@ URL mit Hash: `/#` (z. B. nach Klick auf die Karte). Inhalt: -- **Kamera** — nur wenn eine Webcam erreichbar ist; sonst bleibt der Platz weg. Modus Snapshot/Stream unter Einstellungen → Dieser Browser (oder Kameras-Seite) +- **Kamera** — nur wenn eine Webcam erreichbar ist; sonst bleibt der Platz weg. Modus Snapshot/Stream unter Einstellungen → Dieser Browser (gilt nur im Cockpit; die Wand-Thumbs sind immer Snapshot) - Status, Temps inkl. Bauraum wo verfügbar, Lüfter - **Steuerung:** Zieltemperaturen, Speed, Flow, Fan — fest vorgegebene Befehle, keine freie Konsole. Mit ACE zusätzlich Trockner (Temp, Dauer, Start/Stopp) und pro bestücktem Platz **an den Drucker** (Sorte und Farbe aus der Grotti-Spule) - **E-Stop** nur bei printing/paused/heating (Zwei-Klick). Danach oft Strom aus/an -- **Verlauf** Temperatur und ED des aktuellen oder letzten Drucks; Link zum Druckbericht (dort Strom nach Grundlast/Düse/Bett) +- **Verlauf** Temperatur und ED des aktuellen oder letzten Drucks; Link zum Druckbericht (dort Kosten: Filament + Grundlast/Düse/Bett) - **3D-Pfad** der laufenden oder gewählten Datei; Objekte ausschließen (mit Bestätigung) - **Bettnetz**-Heatmap, wenn vorhanden diff --git a/docs/Anleitung-Warteschlange.md b/docs/Anleitung-Warteschlange.md index a1fbc0a..3820d1b 100644 --- a/docs/Anleitung-Warteschlange.md +++ b/docs/Anleitung-Warteschlange.md @@ -1,6 +1,6 @@ # Warteschlange und Ablauf -Stand **18.10.2026**. Aufträge „wenn frei“, Priorität, und der Kobra-3-Ablauf (drucken → abkühlen → schieben → tasten). +Stand **18.10.2026**. Aufträge in der Warteschlange (Dieser Drucker / Nächster passender), Priorität, und der Kobra-3-Ablauf (drucken → abkühlen → schieben → tasten). ![Warteschlange](../web/static/doku/warteschlange.png) diff --git a/docs/community/10-druckbericht-strom.png b/docs/community/10-druckbericht-strom.png new file mode 100644 index 0000000000000000000000000000000000000000..d446188fbda165b0de87bca23638cfdafdc21aef GIT binary patch literal 199016 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 #;y^ zj%YN>COjsvNVOwZ>JOm(vJuN0Cyh3ayQ(U_o>}WAJY(1=vjj=Ut}|HrA21i9`FM(kZC&L`=MgOr9;}`NAR%;VrY8pwWu- z0ZO^Qwa1YfK2;FJEhJHhOOXqO%Zz`%dWqwh3@CH{OChxvoU5& zwJs~mf99?I`u!Y7kQ?#V)`5U14PLe3<-Q8jrp@!RarLpAd$s$+UFTSBw}^!k?m4Mt zsa{D~k-*QPtX0+J$9c5rN=G=~dSXH}a#M zu9t#B5_Qn4n7a#%^r0nvc@2>qEgOdSRL>RNb*2_Bzh}G|cIRY3c zs5gbGc!jFpB@u=&Q$mqvA(#%5_txwu(oh~*1g@zlD>2FSp};`wWr@qhdk4&{7L^1S z(JK(g$`+lL{hi$vW+{Uhqkg!QmhsRrqyKVVh}G34ToM5Ry0r4qzFOp5nZ;bZa zR-Y%uD18H5@&rEGm?Z>fYEIffG3s`jzm722L}9nc00KK!ft%eQ&N9K==&SQ zoJeDtAZS*i`UX9b&aGRVChdG>1av!I?&a=|f8nhS%E0XK#^+1r$UpKfeLViwHDI9K zb;)TvDQIgLZd&duTS?*MDEjAnkbkNyn95aK5ETmmZ>ABXuosG%A9lmPg$mOYVu1q- zFqg5%g!fAmjv@^H8-XEwLDSeUOO93kdyQa@dHDF|#!hZV{q6@al-YgM3$ zs%yXXMKIkNyPL*Gb2eMd!`nKXre!Fm8daSFbe#$QKU2%3)e zHc7vw5Fh8*;bNP#^03PSnl=%wtQ` zwsw{a@+SfqC>I+|@nVMe?^^k?AKUx~4xQTG$3&&50C|e>DrxkJYP`*w)ar?5s;<~a z5Kt5EuLT^;lCJMA7!{7iauC$`UHXglF}!0}Kn-sW(4AubZ@|0uB_J_;p3m3?Qmv{5dK+SSsXOKd4NK%?xc9+RyN)BQjBb-$J+WK{Q&TW_A zlCMwqZ3aQ`V(B5g=ei6WsTHp<;l9LGgc-dINy0$8e|PIEpO*=vhw8 z_TP@d)&Vwe%&rRwiS-NJM4rEIM}B-|_-m-d4jYvh<+IphhK~E7O&i_HJEhd<^7S*_ z<_bD#hdAJzqPpjGA%AU>x2gcK89O zMv%IUji`3&V4Gw}XeYv8-S5dXH2hFtky=j87LtRspKT;fpC?_D4LL#)RcKT#mXYhaG!HDlMCkR^XpXE_V{}u|?hJqv| zMPpTlmkp5ezVH(qD~#uHRYgeedPiftn>epfx9+gNQ>rv|ET5e(q_ccl>bqNFaCf&o zwZXJMW1X7R{(QFwIsY@8l@(Pu@f>YCP_lZ%0#@@(o_wrIUNk;)>e|`puZ)t9;-fPG zK_e}3|1l2wvR6M#KOy!r?`?$nauUKCkdEVeN#k^MNZUV`KCPnBjjQz*Fu-Tyyvmeg zwp?@lI&HsW5~O=)Vv1a9Ob$q>E_mg!svnDf?0)7_C);D8)GwOecYLTSR>-25L~1`u zfJskYQWnUW5l%eoa-DR0dv^l;u^nx~OQ6HzxZBtbE#hs}N$r@*0t9zcI@@%ya=Uz* zh7x*ubgG`O8zjZ@T`>D{6s+=?F1ve$fA9G)WaRd+)1s|>g=@>hLKdt-#|s9yu0NCH zC=ThpU3uG3N~zvbI~!5goTuJI5B>g3@Uz25kW1Sdn)3*P39{G=P;y1fniTQ{{M_ze zb4kR$SZu@|^G*Ci%@6_01=|EKqD5~^-J-f}z<=?uQr=IN!|N80k-yCUB+Ko-6z|J% zyF-*>^KxVQ3k8HiA?fmSYx8Sff%2-&p;m&A^)Ocf>!B$Q@~hVR{HZ0C!UjFpng2!^ zs37pMbDR+>+axg{m@!LnH}Z-BjjyKbyi{dw?UKi8a8)12aM4PtSGnPE3F?H@JHmT^ zLtfA*1hTadR`rBHRzUm%<{#~!h8JU*>&w=XbbIae>%xhOpGYNb=2Qp}>cmku0%+C6 z<~FdP(G9y5jV%DNsA2Hx;g>k)v;hOkfJqFtMOiK?a&+3XS*mt?5O^5b4edenU#yf; z;>`Ps`f+g~fkK^5GuOqkU^0-9=*cS~KQz(6;4|&Q?+_fK35ngYrvhrmXf!Mp;wHE;&SA%kDIU)okZxHS~_hj+hVm)>~QibPYgr!F`1eyPNX%r|O+{|eRm zxZ14rQ78uf(13}G1yn^ucqzkTbDj|7mLEOag!A%HVBn>?hiEdnIPWaACr`5u8A9wcdL zdSyFqIc@j*WkzpJEeFbu^(P*F!}smJtQh^a4armZtoiC@c+Ax*hr@{WVdm^TJptRu zxH#flo*QE^0@xfA^IKuTWuy76lKk>tZO1i~3CX^^vjLDDA48ixA5#@GZdM87RAYUl zykTXD8S7IaGVIyPlwQCNeC~=YuNY~a06Tk^y8u$&lUV8&w|6|va%T0SjgRf5(zHu$ zeo7Z8-!%_TB|ksb@4dCA@jfECvMP49Ec2>b+&1UpPz$B&5WOq20MG3SCfxAj_o@s5 zJi1?eor8F_`A@;7-ul29hgPsmQ;GhQ$g_)H^?%zw6B&Gcb<6gHTgt!6&7HlhnrZ0w z%)yklsE1szu3lZ1dR-|*qG@;q^k;7LmOfupKF10o@Wh6Z+cWBKMH#+;w)Ham?Q^va zyx!ec>{CYfFlReuXY?g^&^q*C@4d8Nck(3cM{_fNEw2o199us3RA<+)RWDI`OFD>r zE|&&-Qy7fg?fH$rCFD{CM(`Cn+e|0_u9pdAh`u&>eM!`1F(6sow3ZtY#cf{=TlqQRFiQGE^5Q|I5;fgpFprwH3Ux*t{dC{M6q}a0MbqpA zGvC#$nMl&|fX?Ym)ruZM{dA9q>B|;W^Z9ILGdlb!D-#*=Q_E)Ltd84r?n^kkkL%5x zG;~vr+jG^5<}_*%30>Y=*3bRm4F1UQ9N_y)CJX5gHukK|$>2o3_ZKusNnU(#vl|xq zk|X-_O1mvs|1%bulBpSOTteklz7J=^PXMydT{MWfN#bzH<1yywv&iST-oMy6T_VOu zmXd^TYa8DDt+?Vuk^ZO59*glcg$+1H+xN@+0ye6VXF(|%-19@G(wf^wMrbODdq>o` zGNrfAAIOjQP7?jw z8iO5|x$P+k|BHX~r=9f`Xvb99WX2=ZR{MlJJ1Q3Jcc9J1TqE*4SxN&NjS)y(dF(yD z_I7zjuMn)pgEdA3o-Q**OcNIGuiyn^{K6EQYc~UfeEy~2|C28vz?2}VXwhkL z$b<9^<@a;-l^P8ds+8M1zUT0MKNKRjkS zZtkePskJi#lx=#vRdJAyB!GFjj#?phL=_Q#lei`8@i24>YE@zR^OZP5XIc?mX7DMq z!S^Fwfd@-Zr^)y(AmfIH5FR>n-S-Anf%6!dC%$`EWm~w)MYwufZ}*b`4L-!hIeRAH z{%@h@fJ!^@Mc=$k-$m`7dB$BKp{We|oZ^o#H!umIK7I9-O61{2O48gD zE|xP`G*t0PbK0mpx|0yis*mylt6Vevh4!clWv{1;$c4(t#OqwjI+Q#`1A~U`n7YJY zy;y;zW>#5VJlAb(#2SHE1lhiKJuj7iOf6#o45~^NErP$FLtSMb8}w|&WXFy^*kCN) z2Ur6<(mgQJ6p)4v=`W3o_mO%@Lzx*Uep`%m3EB;rADPEeE6CgW)Zg#bYVLXSL|d4J zB!z5#?Mp16X-llD2`}>B#ZpB+4u;%y3!sujj zL8LV;P~k=eNAIZ9WZOH;0C}A3+jV|Yg^f{TJ?3yE*e4Cov)&Qi0^Qe-j2|Uzu%qM0 zL3yY2zIJt~9npnd@qb)^Ky3lXUqO40Auf6~RZRP9q zU7^)bz<7o0xU4jJr)O}LRXVE`G@#t_h-FAyPtjmYQ33o%X7+4{N*^C$sj4G&nOvdF z-t^~7?hpI)Y4isQ=>L7S3xR6pLOkGL0q#{ZiKw4BR$DP<+(9-ceRFohoiN2x>4|~G z{`m_NmI6ZLOSALyoYwnmvqM|lRkGe;JqpIZi7%|{egi!rMi&aX)|YI~RyB{aIU{7$ z!9~G&aUjQv;}VZdOih_kgF9V-(hNgN(iqJ$s;DuWrP*;JT-9dOJ-Cbt#}AvT}BF?)twa|8lYY9dQoVVq_odmgS$F z51}-CHNAg}*29o~kvOhfz*DS+v-%`|oEGRWMaP6@giRN=9kHy?F9-dr}bN;xIB%L>H zY;YnXcB^%4I!YAqa>h9$$0-qsKXP+CVUbWT+_xN5`xly7UVo7bKp8i(s)os&RAvS5 zrDAuVJV+V|s2YKxQDh9_s4vOJ)+dKtwn{f+J~EsyKhpK&0ik8+1jUtm@*N2ANEM=N zxPZEEF|}G!0Tk)oedY!IAw?-7jE($ml>K)E)9GNoBAd_<)qY(W;xy^ifXKsy=1Qe% z6q>^p%fc@XS!u1c&cmg(#?@%MkBhsLfsHk3>U8baI(+}UYePx#ud?%q0x1Q$WvR{V z^L2HD=)wIo#f=+5r{ncN z$nsgy?Zbm=QQ)=-U6a-ugb%qiwJ@mZz|!?=R8jhEJr%`W_%SX0NLZR1Zf2?QPt~5W zWw3`Jtx@2}r~o7`IQ8Y%Y5i4k{juLg=XGX8kJzEI1i@y!p#5N2AFZ2jTHVex<*nml zFh(;YpVb~VZ%rQf!{zhsKm90Ok$*%U$s7$`HZi`UZOwc%^tV!UN*>g>2o>*;ohVl( zm=kWCbvQp&H!FUhjyC@(BP@<|CscFF%X~|Ff!~<4sYqRK+7RmX8YWe7 zLVWd4GeH&)RJmM@b~{oL=P;euS-wCw50V$5ma*|NQ|xJ|m{y>mZ6I0+^Elgi!w`?wbNTVoQFX57`!c#K6l@VwH^7buYidYQv~(dASRCTSLzA($ z`tZ4u0G$P*N+G`vCa5nFvxKo zt@_mCo-{2k%iC$%inaa$UqwyP?=(UWHeJ@EAdGsT22_z9t7l{tQud9t-pG0i>^o6m za&08vkwP-R$9I(?@gEU`$}C-^l+jhL~-)ofpRDkkk|R*Vjo{ax~|H`%3_|g z@(ApITG;>lWpbMXhM2GD>^~EY))ICyW-ZC(X!|mP$)u?yxEdXpS=im|UEBQb3=Or} zLI)s$gpO_ZuWe|9kT3EK1QS}T*N`G@ztYq3iO>l5yns9#;RY^U*i)Ui11;+7K-{<0 z5TJR7Rh-3OZ45n&%NdWUE0L_~zrHHU{j)Q~P5Ft8)g^W(D7HURMi$*o=4s*#{^yo+AW_>pVZs#X?f#2 z9;|C!wM|*I_IFGlOeD`sycCj?&5g_$wJ>eJ@G3bZrR(hpBA9j7i$)Hn4FMU?5AGuY zXe0$o&gu0AK1V>TtCHIduSxCI`Sc{$`{8qwS+REc*Rgph*{8R2JwK4Zrz{wp?jfJE zS40axZO_E9s~M}KaKa_`*Ek}(L}((16aJ|I?YlwwiB_EpCYLkg=k36M@K0lfm`P&1 z=;T*1S;u+;=#aXyjv7!n+3)j~S|vW~UVh!5=Rq%4QxxHBq5!TZJATF!;8qiBfQimJ z*4P|W{2?DdS`u%&*|as zxGRx&F@a7#tM^ntM~KI)m;6SE&Cysjo?ce=n$Jf_(uF8;;6bvF#B|1ze9ynHWANR> zKT15vX_gf_Dr2c>F9WREJRUUfHFUfu?RtR40w;Gcq%hvD%aP|l5m3PAE9kqP4`q1i zUQDfKf-@5jlL52ge9(N%|9_%KF%MT8cW>SglFt!B3`SAZokq`w%7T|mph@~I+2b_r z+x{AYY^R5G-!Z*=yVWG_ohuWclj3xKTUKf&93oMSPpCkha(rl7L?NI~g)X)^YX(Yu zATZoazYJ>CgWF;4R5LMwih&q3RE1D_5c+LodNbanf{9j>Ks$$VTNFs+4A^`3#_kjNxr^G8?vA^0^*NwrdYHqq(=%7SFpu zg>Jbe*tvNYNjq|Pe;4F+b$vD641(0^K1E5cUa;Df$NxcDcdlZJ=XoS!UQ-0#0%Y@u zSAlr0+Sow6^+vi{kT2)%_&WLr9DaQH-Y2P7zEVhgu|Z>$ABW}Zv?v*GIj8V$TWnSk z;uvmo!ZXGh<_=Q6u5ggx!U3lym!!kK1u2N zsh|%XdgoQAPWN+HPIy7Lc<;NoPp=eh`>E6D)gCufct+gE+pRrz>(B{pJ+La!+@qFO zlTk}Pa6k_Vv~<#1P_btS+aT_<`H9b%65glLp zahq9hze-&9#Sj(QpIWnG_HkK>%mK8AjZ{l_c5G0A`06mZ+a|$~MUvBf-GrW6o5O{9 zsayi@USEXwrM};E-2^Jqm-J4xKdi51g;@2x+D~yT*I9@wO3YfA3{AG8%&?K9r<2-A zNd&Z5P>eL2f|0x{0zZ)cgLeF<&^LFTvGlKlm~sl5%BgkMoG(P^1XoPdEK}s=YQZ`` z>GO*r+5l860yRs~SKJiK5~oQ;3#h|29&iR=@F{;+raV&HSF^mo*JQ|`RX1}qobMRa z3GyY;A6DHh7QXSo}&NC=!rtuAFEyI9{qN5kB2VBbg>DJoV&- zpU*MpB--pg_)GlZP((meRLjzZY^#@bygA>H(mmgkcnBn$nkTR!RwCBs9I&M+B$?JFzw6_AD_)Vyz- zGR4?wQzx;XO5d(p-_Zlod!FCg3d2Tybc6{B84PlhEjlVAi*A`oiViN-Qntf z!4|KEQ8vfvPNd?=wZz84Dk2Q$gM9&p^GOpP90685(d#9p*43z5%^X;zcSfjAVKPU# zl{-xkUwx~F!f8P1HZchRgcj9KcjLz>c@d+f*gj&Jh@26GdM~41X-cB>0oj7p3YY=; zQ|TdUSvV9Id@*X$bhRdHX_rAK#EIE+%WO3T21{@_TeFRkP_JePed0K+UI&izHj%Sw z*u_LI13c+!(&VCqt2`8)g#?|^R1FifNmbpEaBV&2Z(j5=#l$S|l0B9(Z9m4MIva$w zl*B~;FaPdt-EX5jcYZFemF4f=O~kUVyC_R|RaIAs8L+pvL_&fB6SK;8vpqXS-KU5y z-L$$~9Zv%0smf~jE1n+grrpEL1U+1Z8RZm~qoW+6O06sv7Mu zs&8oy`T~g}G^zkD$y|?<8B7SmKM%TspeY{k0*AHSnJHLB{cRAT%At1SxZv&{TBSBM zLyIn1X?@V>E`Qec*Tv*$R3-@g^__452Px{R9wCU86K7ss@17pqHA~r+?2H}!YJW=i zi%Ub6oU62&Ui2$15rv)V1~@Ml6Z8%W+K{bkKHQ?$vcPpW#I0}_u0cQ~3VJkWqy^W3 z(>c|}J#nRwb|Mmai(Kk@2knV=g0T7!taL?x%|&n^znvW&nlwM|XS2?azmgM^*+|KQ z13VZNn1hW`5E-$o8_++)pR6W@KycOE>n3^W{;wARjs#<~I6;-cBOlF;_?)v=Zx{`R zt5~caucV6~s6#U0G%vz+E|K6-x3uxrHre*u{6-=E3ZfjAmYT;<4zV$?)M~f6-j&4}7 zWco9?o|vI%ZiZQgWY)=^b$1_M8y6=F>J!}Mo+ur?N#ti-M)Tdp%+{1Y{UjVRJWLer zE-Yjw4^o-cp*cnA2bP}FkCaNpV<^4G5i*vzF0B?M22*cU-MxmHP!tXAZyAlR8D$Lb zj{F%1`^&4fK9$@t1tdg|A4z>E3oqrW7W@uZ%Mq{2>ffXF7hCIX-5llfw87#daEBJ@ z2G5h@x6C%|pVYK4RFg6j=89CEoN}98V_2mJMJ#z)L!*`bEB?a@dl8Tioyj3vNfDRY z?d|Ppu*PYizAXg}t*Tp0sul?46En5FwLySx|LN+U7YoZOYsQ(K_G*F^`$Co?jSO8| z+ocd|YA2*The5u$+IJVcq8s?f=0!#s>ZFm0%&v_M>jxE$z8E_RV^yBf^w?WK+00b1 z#mYtJO7E~ju93L6>mtIT=~VWw5Mn`rnQ6vu&}uVXNt%hK5>U2f*~X!Vn#Y;jxrEBG z@${UTxjqqXAE2e%I9BRf^X&0b`(p3VtfG+g92zTmQYQ&#tqc2Jnaq5)*(fcBx`Fll zRBQ=Nr0f*v5E7H@Wz*MG-p6IBiRgKFMW^csP`L6&M(drZAak7 z)AZJ0SV>e!F;jH)hzs|&@6U!1%XtebxoB4;U-cSL2zhNDr{4?->?NFW6X@|0ZWno} zuVZc|NB(lgTGJM6M^rUZm!UK8-5z`-Cpie6uaz;J_7amA`5c#{WLJGXk`bRi6QGO0 z(8Z*xWjCZST(+o4Lu2EO?k-w%Otygfyw}{Y7{Bb~WH`HjinOO|O}r6dLod}Zv)P)O-{$U zJUs2Nunvle0b`Jjr!#p4b#<_bO2#Z2uCAu;?xl~3XQP<5mm6&z)zwoCW(F@WznF?G zq34cz_iw!JuLJ!eA|mv8#FM&;o9pYpt^VW{6e6Oc%2%+`RS!ah*uXt~>P?F)m@cOp zi~N#{zU%#Yr>T${iB&jq186myS383ac7-{i^t8mA9JZ78fD)@6cB`9Qfh?Y;X3k}k z(Yw99`~k2l-K{T$*!suMgyYI4q86;BbgkD*YS~9tA{7?fj5sGj?5)jj-v1A144q6& z3_Q%W$20KDmTYlzQDs!-B4i2=xVl;u)gWpG&Y+R`@b00nGLpdRryEn@6BDgm?D9i8 z|MTStfU1AsU#Dk-GXrPj@RC_BXhXd$$+!U@;@j={z$E&Y^LQ_^XABOkA8C5am7w#_ z!e#}XBy}ATi{IXfz~+YbT54_hp6I&|kq%Guo2ASpgPQau)HDvKr+-R^EH7F%9WPt& z+s$T1C^A0{1MnrDMRWxl>gjx`zz2`khlTg&PrB>fL)PIMzLdpHQQ8Xj8r{wICa3x( za7b$AINuneS8X0o%I;ZY`ozd(*ZWLZex9}m^W`D!n4*!t;V|%sw_Ufxd6aMg#r)w_ znYpjzlfOn{{ac{;rsb8fq6ycU%-$Z=c*ZWSd;4O#YTg~5>+&BXB&h3fZU;k+UR^I| zy0e&R1qm@vcMl5$^~y?VWTwvVcd-USA1*GJwlq^1q!19@?@w;i3Q}@t&s_PbY3m4I zSD$se+Fk9|mt?Tei^V0L1w5WlZsR&&Ypy$=zrRV_&-kMf+^@x@xW^4kETgd~2YBDI z%jCaH(H3`&lIW}>6ip~DX5Oe2M#^` z_1uv!Eq*+B9L@pr(5$bvdb{iR&ghx+mOZtTo#+jk>#aB*4d)t|!|TuK9Ys6Sl1q!x(nF`sNgYveGUiRE6jN464M9 z&!Rqr`9u=RWMe+T@ej*{a&)alCO_|}-h-Lbm1IyUkll}+&r0A^W z=Ru!#w|T}F?~g?lIfdxP>{|1Wu~nVPJHqWYzkS&8>hoBOanE+%yYS-AGreK&<9f{u z!T!nC=NB};9&`PxIggFKKA(-hR&Ku57sMbkT#lcwO*v8aC}htb(0ox~sY)52^H4}< z^K$B4`j@N5N3$d63GA%ey$zxbEj`&*`6N^?q?k2YUFQB(e9udZ9c@sT(7qr(rzCV5 zox7TjateLrukJ7H7-;+m0AN-J8d7C@bZYa){HB_2?XzNTb9oT>aUa6liqyb2YxtRJ zawTB?x|~fYf6-so^R1yF2rFaCY5US{au?c>A(fxQ=Zgz|C-9fV`E3SfwHKV z=2xs5nVx=n@Bt|bcfHik%v=+J1s|ibuaGI4n}{y6uAGq{B7w;_8fI{Htt!a>nac&n znUG^oJwTV`ait$#yXh}(VY`f_xLl(c@Qd3e)vCq|a=(yhG^oK}a#?+Jt-ImAg z%GSu}?m~mA^KChSegcp|FCDLF0Ggghn+{RL5eql-?z^(i+e^U85c%LxvQ&c+mnx?TyI;8N*SkxZ$LfB2@yNKTE2{~bcIC;AViBQ`M?sI?|ncd-$RP_nSn#Zh6U$ORuRgz54HTHU>OHM8qB#2pUSq`FE0BR%fN=~!HK|G~H(!`qdw7!s3nH@DN}cRKJHcsL$tO5Q zddA#&Xflm;L<8gfI3+_NNMKsM7rnyiLk+mH)24rD<~CK`J*bUl>jQNU{!ea_y>CG- zjnV^~dU*nHbQ1oO+dK}(^Z&?t$M8s_C0sk0SQFc}?PQWnl8Mu?ZQHhO+qP}nw(Xqk zz0djneAm^#yVrVqt?KHkdhVzDqio&-0EyI)RGt(Jn?IZDmBg*ep1WO8Ys@=43G8nJ=DEq=qr-!H$BMRR<%>?mBZv1} zx}ydr+Jj$;Vw{JnO!2E&cKik<&t$SctTFYtrz6;BWr@N=sggNTP2@{!F}0qc zM}DPyVR1`vt|K;Zt~aHnPd-g{`!jiBN27t{Z!*SUgW(L=e;NCG7fijGccCuP?eHvx z2dfciO{t)ebRQsF_$J_G&pUFhtC)nqU2CZqkbexd#v_paNX?LxI>!~^1c0$dm6R~# zgrd#2HP-(n@j9jB!JA%M(jubtd|Oa1PZtO+C@6p|!)CeH@8`knYi~(aE&quuJqQSR zF689odoHS6QNo6xKTdxBSi?U_G$-e(Y_jhu{df}aRcW<#i7Z3xkIRVgZ-5uxPpz`C zu~LXIJT#Prk+HVk9uf|&f+q7vzX2veSc~6o(Vynp88K)5*7u8Yi;!Orz5k7%DTJ6GD7hKgI+*DT!`>_^S2Fe+sKP;+wC=fbnX7Vdi1-)MN~Su zSr8l)&wF|`*$@q-!fMo~U496hdk%l)-;DoP3m{m29%-ZX-tudv&cXP&Em`tN3?pLh zRJMM-nwZDYJU$kw^in(5^}}@YVrg8iF3(nMEoQpKRje%buhw#ecVZt&Ro} zf@!H-cfQz~q&c?WxS(0Edm97BTjGhqcfT@i4Fj<;5)p_{A|)XOG%j|4?>U?^k1P^w zb9~H9u2IQWj~Bf9Z%#P6&eRvK?k;~*Ty(TC)88Itb$?QOV7pMwzWMVl>8VvxNVz)f z6~jD)xFWxCVA|WTQjUhYI($`gm#&SHr}QMfn%L+w~^fr>-om<^`KQ zKf|Y=k?aQzd@%5m^}c<{LRh?fj2*n<%;3{W;Saz4-2!tEVDa@R^w@{+>XL&=&HXYm z<-gq5T2ok9i1&G0eCzLZb8k#+9L{?q zn4gG7Wu~UsxkK2Kb`J))xi*@)Gl)AJARD;b7$SgCavIsa3~mG@X1VbUyL{|ugS$lH zlXCS%6r3!LC-rCSZQVyRr6#4tC*Jh?zt*p{He9Vq`unb_3u0HJiR3#J%8BbVF?{}}zi11*x zf+XtnKEFPDYD`2__SU0(Y2 zspJkN<}dI3L5)2$dO31koH$O)RlNWHO)xXh@9*hRS?x&_GI4O^h#}C^SMxlF}9i}9a1Mo0wt@~nXcg(-6M~p{@!K`wgsYL z_P5}Egp`dWFTPf5_su;PGIn!frrhRCm*FPjG~Wk*KCOpXeYKpN7 zWJQOBv59T}Rz;Y4BEYMk(j$`LZgAL~55=Mw#k|TW^C@0=jjOfWZRD-powVH20*nn< zx{4___R7EVAYRolMb6h}nhKAu^e6}G8`&sMjPn|hrnP2=A-mX*8|#>f6(Cl|6wtDz z3>Z zvn{8;cAl|No!^riZ}&P<2nc1&qhf>HH1L8(Noozzbj3*x@NtpHrQPm=U(@I`+!20s z)wHAGzgT0RQ@a*A(~Fkm6W%h5s-I}ya|cd8b~?$g>{T1TY|MxTmKyy4W>qLP1K^eK z&rG~$@wuV0Q%9vAQCDMKBXP%|H8sOCxkqWXoILlr{WReo{oVX|j|NUI?0K39nrXjg z#n?VPf^5IyT#+H#FcS)xr<&P&vz+a%9=ol*y0S+9wVgineu#9|9%lyKt*gW^v2{?@ zk~qWjkJ#@PTYY>8CHh7twvZGd7n@vVL9iwc?BXTiaIx zCndUl%U-sl#AuJd|Cs|m{ZvS?Os1Q*ci^6u>%g20ZC;6_)h3;UfQJ zdLFM*0`nF<{vQ%Qv&slvDjP5x^a~>t(9@jeN?wn-$&oz7jeiE&n;9wKoee3I@Vqqa zA*DI&%aNEF-^yu@!Vxp-WJT0KeG=iUD6scpY_E6Nu1_^OntR@%hGvo7D-2ILW~6@XZqgyjG=_>QMWr%aU9Ow$T44#cjCIh(+mVn3I@qXq zHb68t=fXA;n8K>6tfsR(Q^a#fFmZ4&u+>R_84fbUj8XsYZ?|X7l;#eKaXnYdf)7N{ z)a=a2puxq}G(K+9)g@FT1zS?k*@^G#1JT!4DB1ae*f*KZM@^&ic%G?yH>nlEruFdT zE`YGyN~twFRasbAZ>J6F3F^N9<3WZoIXS_nrluw?-!aJ9BZw2?brxJ{W>H0x!j)~XH0ThH?NjnJUT49 z^_>2hc2%CWdn!)35i^Y$9Uk{Nw)hWxUb_=jsL`TypZZmNE?$nS2wP(v9F|tu@V^J~ z)EbVI(#V>1CD}Y#%njupI-{eD_I*eUnF_6_i-Wn9wWTLw>?gTs)Y|9t7y3+jWh$Xd zZ1+6ZQtxy2fNCy|lj~g4lm|G1zdwxhb}O2SeCC+1;ClkO!Z)3l;tYw^?Gx+zF8ns+ zQ{_eD%)(&WO^7StSi$2GZgjeg3K=s76_AlG6i@NpYb^u*`2-X_ z6%qy8wrf*XYjplJx7p$lQ_Gyo8^YSK0=iT65}HLGp+X8_PE(dtm~n9xBvJzMk=`hc z5y=Wp8MOw3EHZ)@xrOc3=?dvQ=f7;hUEuR2cy+2g zu~)fgV_c~~K%o!ayy@c6*HmLNQXLA&M>ivm#dfk*)+vSRE`o)f6 zIf8#&gc@`;b%B7@9DiyCdhECAoqDUQPaS(@Y5h4$K@1uhf7;H;z+B* zm2IlXKy0USdnGQ1%l*=$@?OD>vPki|B7$7dbeFan{1&jzg`eZO`)Q9_zfFCXw#B*T zEwd`l?ePHr`N>GUe3dJ_V%8x41COz{$H z@bYfTR=v@~_I*wed7_aA9<4jtV}O`qgH#4~d|ENkl&De-eY(6P;E*J1tV|8S>B>#v z9&eRa=H_&tEx<~^p{6QIN^P6jn%51don*8Bk5&uk`SmSQ%gye@ZLQ#V0)@p1!Bkx~v5T2Lw;Ji`F){w-abUSHS`2)^Ix(xGPY_y*F|EJd4rS=imH zBY~`fJSbR=uojQUd-9ESzhaKgLbdj_=kia_;Y()p6z=NuRA)C1&Jpn9&hC_(!)|>r zf;81Uzf-LpCMgJ_yns{Y!cdC*P(apHMFU`8adG^*ebd zLrE8Qcrh#oMi%$z)`BfZhYpKr28=1^{iamJmNI7GkEkQK6lXrGK@E7zc)_gB5jz>r zGkBxJA#G5o_RqF{OvD{HK}ZsHzG6aW;7o#Z|Hnti&W_L1<>tSC|9&c5VPVh><4FBR ze}4apCYPm3T?tupj6#Jr^4!bI6M7M(K*7XL9~fqyMf@q3^YU;B5|S~@Xs1_7i12w` zC}?PKn9SVJX}TF08Xg@O2v34YkV{KjLPOu-^R`j{4vCN3gMi05+M6Tf;VJmCXf$V5 zNZ{h(QRFU37~WS`X+VM;>*Mcpo=8e!!w6DWU2QNk1e3wTn;IJ0+Ww{#FJk~FK!M&3 z?X}>zAq>+ImJ?~;V*;6th}lybUw*hYg->{GDI;Nwnw%|NF=DsPD#nbp`F@w;$A_cT zEkcj!qMXtiHg3jb*S|sQes5`uELEwL7?0@qnC|2;f>d#k9+gsX%|QCY*M+)e%4eUQ zSh(5d8V*&uSA%jy2D9~in5KP|I4I>2GgJRwXq$yEJ;U2p{D)dODd@ZIHCdQ)!cP4x z__g0S1Jh!c5yU5LGboCvOY}Gat;h%obrb$+AL(+$rs7M-Rg-J=wAVdZwt~$GkITC~ zU#xi^!kZhT4ZtkV_ho9FUsB&NSjb2J(4#osrY*V7x$BBRAF z%c6H)+gV;D%b-F;e#^JRkft)c z1Gscorw|eH70xvr5Bk_NMdPg>r;7i13fB1EN)cALy#%EVSMrsQD$)QE6qCdNB_2TX z_@`O{+y`x6P&1X~hpyS&C)=jEu?sa-AIDk6Q(*&NHCr&5%HMxgua@($c3)!%qW{@; zak$qbGNdTs3n2U+NeTpmX^J$J64R!rX6jkM%K9GYeo|EdYDn#}8$4;c@Kc9CyY5!D zXXf5Hd15^qNX3sxZ7Cs_i^I=;p;_4GzffJN)c^iPq55ZGKYg0ZYic2cW`C(N9mx2e zWmnon7UQnJC_8EZg>k+)z*0%zcz={{+w%4BuWad2*}b^a)$_B(FcdR{^=)Ugxf8I# zSmmWRPBf8C3$Ol4o;firbCmVx=O2;;5K70r8L)ob3QKe3g16Jt83Dq|CN!XQs1RWF zX2D4LRHiUFQE}PDG6`j~GKq6?v}&PMAL!)Gl)`f*xF+S`uck{E8(MUMuUJbM=&lvM zrL3sV8n3!z5Mz$I%cN?xu`L@5J~Dy|23?av7PwwX8|Ue4UPG z0%c)_*RQi;#K$zv2gk*pPHj_-u9S&c%NL8X@l8`L9iERG}?C2em4BamW(ctT$dOJ?R3h zCT}8o?A*daa`G`p_J@iFP!&nsm?GlR8xs#}5>&tuSo&+1>DKMa(sUjT_f6li=-{lq zoPve22M2c#)Nw-Lx*%dXpj(nAO@a`<`xDQa!m~nUioEY|;m`0d|4H7Ny(sgfm1>e_RG_5K@FKEJ!9c z`udk^jV~0Hkq+5$ZDn0&O`*xni(x7FjHjq zY(T@n2nqZ97`@2^!{Y@7iOS+h$QUQ>`#vQlCGood$Q6-N=+Mpuc8h9iW?>)+eh@A# z;>B7SBlGTdZWH%djrxhx2d>`*iG$d`Ho4MaMmorkWrC>#JuDMu^twF}_KF)Umz@Ie zTC_E6KE>Bc#HWb8ULJ?vp}(TJSDTGMGJbRVZsXq_L!NorRxWiAlbJ^(7bg@3f(0fbk&x0WSi3he>X+XWM{P;a zKjVyaxbiYtm#8lGA0P54CISqc1Z(}|kB?oFmn$U7jLqB0k^f8Hr#KvD+3CgZFcvue{q^+IdmQm>Q>^O!Z;-l(Hg zNu4LcSj&%Fb#uKdw_cz1DXTJ>3+rBQ!WpfMcvKGxLB%|DmZy6>4-USZoIZ=DpQApw zbKDjJ4We}MkqIkU%R0s|ZZ2~05yWa=9UTrGAv`#|QotbgG-R2idaziH+i>;O$;QtDyaMN^mnEYX zCdvy3t{sx>@Cj_r9^COEP8_PU3Kop)VfYY<2K{U0a*Z<|x<_x-fe zm$+*xCgYc~^p%WU2&{wIQ4X!L_#C~rR#isDKbT5AS!{3`GPB>Wdqtw=JYRPq%-19= zq-Nca$LSr}gmPwr75UsI3j@O4Ug5crl-IO1l3?DtvMveB5D&EBgGr+)>nf7=j)!&4 zztcoTec!;6Vbo!w`*%smh6;;{1S$)Dnv8sW&x>Yr+uJe(zXfy$r>5|X^ZND>5p`>8 z83l+9eu05O2k!l}DRW(byZb!!^zuAB@nZ^*%+zq;(erT8P=DY23PYb^VrPFIMaHMF z7g#J(Z^rw4N1&pjVq#!OkRY8ArAzK5Qi#efnsG&?8#fb4h2r3=k4j zQHhD320e<6BZ!EI(A3=2wZgtAeqq*+YP}05WYuP_srS{2jy5H}+R^nIb=s+2h_N>@j>XJv)QcLM-hqZso(WjjE z(%4z9d?58{@~{&a{$T5sgicdfePj$(kL8`y-2X#fA9LMNMTTiL~1G* zD=;sw^vcX}LAakt2BUQ7Rck={d)~*`;xg~otf8dN$UByqwmp?ZxY<>HLehxhL^SU1 z`?W9m1dBjs6QOF6x7C?}52oX{N!l`Aw??VZJ^1EIL3RW*tva)+;iiPl{N*6ZT}hcL z7d!_4tFU0>_uv9M?8C6#eQi?-eVDyH`ct0(d!LX|doe^NT z;(GhhMgOEkX;6q1*d`Cqy$b>Wq~ybNzX1j7now5VCwZ||9w!Dbd>vJtI_#Tn6LF8{ zeD`yqoi&c1GT%W|RA~9O>34@6zS4zV>&sT04vZAoW_XsL{dZmOrc*Dd*}Qx#$A4Hw zdhJt+mLLi09I`p4pq8^lx~zMwJ)APD6ZPRMwUah`*_N7@2E$TMInC5!5xve8e~ZhJ zH6g6Ea&tPr*z6{Q@~hsaZpU&epJ{-Ay2WYEO0z7Bn^kESmct(p6&#EK4=rmruCc&+ zBQk61t8)5QzAlzOwqBWPA;QBxOy<0(n37Tl`iujsgg&UkxmyBGA7uR``s+vF`>_5K zGu|8)gIMu7lK6OP%-(z3O+VC>T~Ap(#z?5yEIx>smqYb48q7WYLGZu%UoYCcb*jC) zRdxzr!X{uo zuhy-p$aQ`xlaweo5f*|@!UB=ex-vRXWMWE?RKM4G2j_Acb+#OJrbh;Wu*h6UU-ICr zJBCEUip;NUD^S4IAQx!rg3cb;wBl68`wp$sjx8pRZwZ-TVUn`5A5UeT7Zy&8W0yF* zz4A(ki&N3jxicoErFmR$Su!)j_zFM*66faT4h{nL_J}OeLA-p-U=+3f5FrZINJvWl zjH}kw*Y7ALNuvK@nVc7iOV*ZD1+{N)D^GWEeVQ|FF=!XhN?*B1 zb$OSGF{O*xW4^TJ<@Ar7l-uOF8ePIwLkQ3FGCReCBEJVhH&(}SRvpl6`08qTqO?5zu7ye;r>3p4;~_Dj;WwEum_cvN5)NA z+Jey9`cQku-`)xtgY3jiDO>$l!#P+g!n9(2bneeZ3H|E2uMJ=B9G%wUDy-MraM`SQ zcp316n^CPAF4_$7YHHHs8gI9l34JKj?mN4>#2tFVGb@)tf8D;VV)QHVMPD7a2EjrU zl_~|GG8dy17$Fi}qQ`}GJ}fVljF4-ttEHZWt2<)f&z8vW^8CC1B8aMq!zU1uu6U1VIn^9&@o$<;3&5aG|?E>BEH zO!VDE%aw(EXEG6xhzHF!*p+k(EwG@`54Uq{{J$wjTHpRKAI7~bjJAWWsp+UJMQ`fj zcM84%4tY)-2mp#LJwH`c_{Xw5Y`Nw}SXWoq>Po@IIVedMhmZxx2s)dJgyeSwi-q|^yzT?{mPALDe8aI#aP%2;uuH~o?K9Li@Vh48$aVJ}vCCMOO#uM+=XEx-{vd##WOv%~g=o?q&%BEJwrlJj=i5^88DHTN;km)U65 zZ)IVGOP^=~>UeK4&J8gNTHpW4`3dhjgm~BYP8hz;;ps&F^nl)p5x*pv{r2(5gF@e<^DL!PZNTWdNCS4MTIf-X-Kzl+g( z3~&~B*p0bBP+NaU;|*bcR)a#G@<(x}#Xg|WI|j>Gh6c1LRZ6|k0OT~7>Nj90_nqDy zE{6d6mdsPts*Q%z-MdtH*zr`X=Kc)SG^Hu6z#6rwoTU-Tax_M{B=XY|5E3dW@^bg& z{0qlHX-j~u&$?q0=45F$5%*_)DqXMP78;GK1#h1%e{w|DW$2++klH`;L}i{wp?WJF zj>11yRmc3P-t{4a72)F@HNBC4COAtD)|3H@RZv#6{L{&xaouMZD_(sBTc$;9tjd|g zUVroa8z+ER@YW|q&Fr!J%3zngS6oyD#V zgaYLyn08aaSkL$#5FI*IPCHAEQ54V-%JcZYkJShCL?vn&TJ67qxe&7dEY~tj4m5)1wws;FDB+;9;!>o}9KiQ$BBp&dv-;YaxyT^ZXObr8TZ0#h_&C1z8fm zQL`Bwts>s0^7$cN?ONXDhSq$lXSZtCd$A)tT{je6cg3nygm8D2h``m$JQYCdd1bZ# z!C2E%i0GwET1_S0^+9ISU0}e;uuxw^9Xs$F!^iN>p#h~w%g4z+ov_}$y+%Iz+v5Wf zIY#WnCfMBZ7WvtVxI;jb30lfjTN03G7O9LelYpfKJJo+&t+hTHq3PoM9hy@29-^01 zUI`S>hwuz>jr~&*wg7cbllMCUyMR+$=WMdtN5aO0{(Vh;^NjE`;KGB4^=JD)pjv2K z=@*2lTve6Q6Pi%G)Y+~g^k*?5LD@ltl<%|lWU!jEv6n`!;2_X%xSDa@NlF>c?~RbL~f%={>H83qE__pTPOByv02sVs*dU=jdtUo$;Fob}a66 z-SJFk!dRD-b4|pgUJ(WRxY_PqXM#f2>S1$O?E5DBPNBPY+bg~$c@!?C#R>oEP#ItS z7&qJgl+uKy_S?f*Z=;KTV<2F$2lYHaGYC!9_3@EmzeZK@ZIRcT&)|*HYCgI8$_kP} z^D_09VqfzTc`>!{pP9eR! zmL)n%9NH2(Qv~a4sFK46PG>8A`m4>=(rMY=Y~zFPQ#M?WlkA$^aqh>J8n4=3)a@F* zTJz{w$u`Vxx0(7K;%)V>Sq9CR6+3c>&PkWtg+l~&Qc2G|v z@7-~-u~oKDVq6tO8)|%wH;Wc1uq6+J*S&wgA1YmE<_}rE7xf%vc>im{x%ugYUh!^7 zevMB|(=T%+Lrc&)qHtb`l0AoeX$E39(Kz3@;-P25PpmbVZrjc=kb82yPQ?D5yjDvJ zGuRh`X@<%lrnt}Ag#TBbD)SK94N`P*HE*B)qu+I@YGlL!@p_LAhSOYh zrLfQ>+p>M_>8ZVbFs&)m^IB0@Iz@lZ=rp?wn~l0FG4F+v=ber&N@72bI~YeOkM5mq z(XzevZnu71Wi{$svX;y}&o-6>nl5P6&nDEm(>C_7Ep%F5<|Qi!CSTPbDod^UD8Oc3 z!m|>%5BH*CeJN4jF2zcc!Z{F+fPRGRvcQxazzMrX)IYF<4u0n&P!=2`h+U#(XIsGobH= zdg1>^gKKHYZ}7Mm5`J1!n%yL~X|WVpR*exz!|ZPQcxJ`M9aWXYv=R?AuTaZtk* zJ<9x|vlmb%4lwW7@F8VlWMpAD=Q_)mo5~pRcJ^{63#)96mao&A-;CJ(Tz%f4hWTp^ zfkibwRiB<6ln2)3jKlNRUs59gLc4mu1>*h);_EYhICf~q!e1`#^C?(D+@od2`q@I- z(pFX1{A=q(>ZTCxBvD=Ele?=cDEx{q@{;FP&ALBmx3IF%rqR<@+v3h?Jx^;@ zWN>;S)!v?Sdt)xI$CUf{128O?LORxd@62mC$bZ z{45!Ov!AM_-s;o3Kia7L2>#kse~-1pK zY>ut7fm_(9CZQK0f(moo7^*K_s%UMa@mv17ONivl@nNqnSM}>2jL;G(_xp%$;)86b zMat=lAZym)IAtl9Slxy0_9chi>g>1Iq{~iF9RMbmqa7pN9lw2EUYXef%)h;Ej=ofE z8Bxy1t42fj^Sq3A&i*d=DkZe5`FwRR2bJcR{?{Mnq6`HPxRaA;+|B)V^|rT65foPW z$(Hj$PSmUNv)Ls%t>)4;5~vyI`4zt9^|aUQHr3Ort^1Z7klvIB0`~IT=zi11gT%kT zGzQIe4mAu^p)C)e?A}||SXOx;mYUd<=$>4>ux3=GE;kQO?)>bwXxoI~?KP^3w zaGzaNv1Y!W7bkFL!_ZT!J029`H(j>VgGE@IH6ahZaxEgAwb`hoE_-V}hp!Gz2Uo5x z5z^%=T3i_jk-Xz-KKFOe;z(^Tl|RP_3O@+tCz+R1?VL2>t66J+OdGGq1PJ9=TU*OQ zTZ3T3yGhzkw#0tjvw5s&A!v~pT?W&AIa(CLkbM1<`kB6R(;6wYUQVMxpUh$~mO8FA zdcFCy!jOn`9;w8=9-F zV~PpTUTfT6KF7>yQWr?=w>6qfZD@aQuD;xPSw1M`?xIzq*RWSZmI-yICxF8~nuVqS z!P8DuG6Da|Q=sZji6>tO9cBzP0eI>LY@O-u+5 zfPTbdhuPmeHV+Ze5tWz)!qB1o3ln-I41#jlAFWyCCA7(b^rdBuAf=Q>LBP2$mJ>4GdN;HsD z1((6e#!wyR;z6fb^hY=;o#8zd60YZ0XGb=A;jVe5S_pxjI7JU2gjnNgo$ z@+|Jg_l-lEbPjYmS0h%Ba?Yd%d7(tu)w|%~?p2EC>y=t{c1yq|Cn5@>%ki8)k`!Q3 zp1#<*vdSS~IJl`w{QP38HlG#Gy}79vT6!tde>+C?3wLva=fY92x3F7Cp9n9bJ$7ud zZDm|3Td4>l`vDy5l~8AUXi*PZha+4>5wQuibgiVsB6WJ!nr-xx*W&SV-I)>H&!xM6 zTCbiMKdT$9x03mcRAJe@>3EpWuHIxmw>{N|ChD8^N8tQ48tFIGjeQKBy;(03=hhq< zZ*I)(=>ldz@}7+V6&~T?vg?MT%#BQOxmSI@s{6w&;yy>+)DxXp-WxTWD)x-dUPb;b zM;V`X8DQI$V<+T^4&MvI`#w+R_yKTFme!mAn^O-K?nCy!%5bGwmQIy}Wkehe*U!R{ zn&IPPs5B=}+~3a4R5w-uh@gL?VO#?Ti_4gUs-M1&WRaLjy4J# zGk2BUY+Uy;B1V68qK%e!G6I}9euh9e+s!L=P0TfuI$HEv{2o3Ks0*8f2*f+&TxXz$ z(Y0Lpz0LMBay>Vo=nme*@!yd!oJ_6es4Zj9XF!QE%-Wa9%IyPSO~UdN?NoI||IK`=>jr+* zkwz>y#dlrk5018b?KqIUcgo9|gjur#?_3z6p%yZcl%6f*PZ!RM&6E#3qbEufLK*ea z>;ZH4ut;?llT!b`T0s7q2UyOw4bJ}U%P+x^L%T}l@>LdPW%h!LNj#P6|G#lbNgdTy zc}_wS)dI^D8ccbGgya}06Dk-!;wzS_ui{@+iQ2;Wz!;#gj*tKiw&dw@40u^rYS{xZ zGJ|?f4=q(2@&Bl@#F5{d`sPkhQjao?4c+DA@Y3b`j_$_l;w2>%6&L4ANlU2`BnXwM z7WK@HQ;2ywUvM%8cW*h@q{n(Zfg!*#%ID~!&YAcNI_L$c453NIPfeV3rwCgijeCUv zQ3|k-K5C?$P)Y7 zaH^>iD@n>jroxfASq%lLSaC2O4O)fSLvU7{(pCKOEZNtcv@#@BCPUz}xrda5xgkZ= z4fQG%b&|LXIL9 z6QC5#1=caB8i(3uvVz(qH0@pJ*UO_iQq+7z<~ZO?)Lm>7?Ps)*Md2+);!+etebmKZ zsxLmyk^!`r0i%IcS+d)K!jy(gq^1VjOEKddRLMIwV29`e$Nbq@*d+YSb??g4D*z7? zy)m$y@G?cfYh*!F|AO;rlCh-Fa{s(Z&*L4^8MpXhP&lQ%g+IGPYK@H6V8_!@vEX{f z&gHmIdDUVL6Dv+tT;iB`+a8-5C6xYcSKusX-nRJRS+_Y_LoJI?(w zdz0bpk_Ys5=He-TXQdJQoV_});@g#KpE+*^U5;5j=(090nUHs=X)a5=KFT()n`t3G z9?S0qpVUeCE& z*?Y&;Bai9~78?QXuRiQ@1~3RRVvL#ip6n#y@1h;+icHYU1;JX|`95CxwrJZO5q|pv z+5A>f(XQZWkPT)N>A7m!=xv#Udc9LMY<0GeDp@+-KxZY#7YfLq z$y{7%7rj-xdO?bLx^SmYDtM;cpHjwZPrid#6PiIT_!=G0R|1&AWGQ}yEXyNHypQ26{KZ%$qD1nf`s4eqLMrF1(n;_z&7RBq`)=!&<#c z&rFpLy`y|5%~A6AA{5Vadvw~)Vr>({o}=CU3W74Q7>SE>b=nZ}&Gk2)9EVKE+I)L} zQ)>M)yxJt|HK-c6r3Me(mpx=x8n_hjSwpv`B2}V%>4Aw({c4&QU)q`iOiixYA!y|= z16<_T?x*9UH@*A)f7!Wtr1CMb3GZHTBVq}?N8z%;&$PYBA%gb)ljwO*0VYT`zF>rP zz_g&k$jrQ+{d;+2DNtZ$5pe#E>N6K{(JYu9;~5 z{{k&(agwySXW~o@4gYg6_a;HxuWHCA4((Wdg~o$K6t577Eq|71uijujUxTUQ!KV#; z_-m3Uxy{KGugWX1j@?>IPSp48r^V>Q60Qh80!dSig&wGJ%AlvQmgM_T4#0-Q%wW)}gn-&!O zZ%2JS3>rG{YDjHw;3kF}&xn2mE<9O&xq(xICU!a0yV@rJ%bEDFhyRN=LKWt>6!1fVQ5+1Er#ffn|1An&5$4x0tjG;KwqQi%){;a1LES+Gu7+`O>@g1r z^h*360#1BKqZ6%V!%ueQV@bgJsAkn-uB8xS-`;lW0nPXGtz zTY~Qr*+X5JP;f}{KU&=^(u|>h6zMn65hKw28CKeiFW`G_A`JG1x$sjU*^-KVsL?3L z0=?qH{BRmIeT=WaTPQ1S01{0QNFI2L=GSc)wHMJAG)Gi_vIi{#$_X~;ThjEgFh8gi zA^L$+Ia2(7MOzgqep)qM zGBzAI1-TtoIl;NvjramT9sQ>yX2S|WCg^A&Arf}t-*Dhs^fzU44HYhn|0QuZ$V!mB z;N0K}ab||^hKtVP3({qpEB0AbJK~(;ghp00F$d74EkhA>XXBdS26%wrtX?|#! z*abn4QAKf7q%p}P50#oZ@2Pb(mXlEXbTfnx1QDU~%qD=$JN(z;qAq|S%x})`gC;7n zstySkC--7P0Sz+ycLw19pKt3AjBkM;W$0<$_7n-aiy|t)AD%wV%oKFY>T2{9zzXYN z0nBz^s4TU~nIl|OVnPzoz`JmGnEGu~j;RDTb|F|>Ti(xU0C#-kpdBHE{4?)Uh4mBf z1PIh1F@Rnr$Yw6oe>tsDL=+A~h~9-nj8N=v)GS;?aVBMX5W_?$0=0#5JJxg;#N@MT zZ!w!-1=hQE$||M?tLKzc|My8m2<5>4IKKXWs~yih*@UEk8bl%;V%wcR8vGjK)KwLk z4Sk~;9+C||9vXsgqlI^=bhBGhl@Z8`#98mhMDfl{LBQ^ zEOc7Mm3_7R!Q-H1MNlSjNhk>ay-;%rx7ZX|iZo#w-#K{y>QkKhF?cNl)A53K54mT` z@+t~;cGjJdQTmiK*=JQa^Ve7C-_=Ki5I#JSS(2)l`A<#rBR;;i+kaN^-^cgkmC`Qp zmk`4Wev2|E1q4#dT*xZfNtOLkJeP)1=>T@^fgI>_ds$>^>Mz(eD70zm+2FehEV)gF zjy=cX_7GJ8DKt`ziyp8fo$uza4BFqh49-{-i5gWTteV^rJ2x*ld!$Fut@42ERyE3a z>curc`}tT*g5)6yShIcLN89ljW1{4;Szf7=AMKufIk}&jg6+##dU>2UirLZOv$~q) zQJ_9U1}0O_5VpnC(M?NZs)9snL~ZYnS;$zTfBlLEUNP8Uge8fTlecUcutncLcL;$h za6KnarqD7(h2o)q+Pw2xmGVgli3dKdI7xgUz1iAUVP?<0Cf)j!%F!=jumOjtZW) z+JG6L;%^ip2coprFDVdtD6PbXgg-Y%zFh#>e=OQ*Mq0yl3_XL|9le#|TJc>bKSrPA zYTT+`qSB&zCOQIf0rVvsQkR^bs@B+mjYm&(d5CGP16?Hy+qauZl$kw3RHQD2ZMn@g z>^|q|vb}_!b9Dc(GqjW(&gIhU5v$98E;o|mb~BGKzJSReoo(0fNiS|?;6)uQ{jUR< zzzqo`Aj;&C3vlW4UBq{FKnuzHdDrmzP<~Y6p7VF+v3><;2l15q z<8!NPWNEmr1>=<&aj;MPfiPF+4sJ!U1spL7dDmX^i1vhgHo@ui04Xge_~1=&#tysM zNp~6SaBUvQ)dn0j0<2C&*T%Uy17<-8IDK#C^JR&I$Sl!$s7}S{Sc@3{2gY(Yc|Jvu z0Lj?>i_KKm>XlXoi*}(m*K-{;0g=JT4irf7J`(smf;(7&TxpKSAX)?ydL))b5k;)Y z;4t4tO5Z^(vqh=^1#w%XdTov9WT);^M&NFGwpPrqDCr!9GV;U4Z_IpTT?wqh65^vq zrRbzmd4C+hRD=6aWHun(mU_U!ogUkDv9dIqZf1VgXMcA{E@05;i;RmYcn@ zyypu&D36uJvCl)=6_20yYYkHTE6)6*R>4!c_X9`B9xH+w;E=@7@LU*aN;)Pv zv56WFS@e&4FR&0RVb!@InrCtH$)n8f^(|U;Oz zllB%KyvmJ*<)%c*SaKCl3a4p6QU*qm9|P_F?;-pxZ%-~Eb$yy7yY(UO_nvlIQzv>O z`I;deiafG{#19@kM*G{LJWq~OzNmzPVYsIbkK6qd-cN7r+F4J&xDm2z79(xyN9mWJ(D<49G0JapxYIy_8U; zh7BJK`lq#Nms2392D-Any{vR7)kaU5+H0E|d5H;ea$Y?^6(z=U-fglFEKXn&X5lIe zSU=D0eP?ZKCijvxr_DWDEo;M^hBMF@2O<6$O4);Vr06?{ERIy3Nly;)Bbsy=e)DF| z`5D}_WIbeQ{VZjemJM=exK{Fd#2 zn>bvFOxYrQkPgSxv03>DtRx{-wZ)DIShtWROC8~-Z+Ck`TIYTK2mHk|CHS^TMxhUr zX&t~V&9En^I8{+dO#iFpjD_YFEE@i5JqyfFe8`;9)Rx5rdyN2}iHrZ%i{^jc|rIAE$wHOYuVb=cFMBwnFX zZf;yECk@)2vK@y*+CC>Bx4~JsG2`5X5~0=(atT^R$xKAuVkw$q<@&_*$G8+QOEm2^ zGtL?*dq6p67gy{8S7)#D z>{1$wZ1re!?N?ei4vgCOjJF$AYT z8m;Dc<2Jj)5sZ`8(d{3hk@3d7Zj3 zwdAVbAx)uw<=)a7&4PFBUxlP#HJ!nAyKgh=p9KE~Z+f_0ojTtTiB9vfx%y%2lN6KX z!*rP!mSOEo2UC2qo*!~V4gcx3>%w7sev2A+k-S&OS+zV1+kb9qghD*!-1|;H$BVhT zz#qylo=Fzj<~?>89r$LuuY}zwGRg*LSX8ynQ%8yDWoC^)CCNP`ph)qb116y10=foX zroGBYl&_~VfEk=UsNZTTgACFrZhVv)U_b z=ZN$|XnyQcGWUn$qVKYBvX{re=>%yN-4W0@2hYu!b?P+&5K~E!N-7Nt40!JCf`GJG zJP3ORaYa>Qwh5J9$EpO0MKL@@hLlqa5UmOc1G!eE-_+a<7A;JB{h82j4pyw;4U&pa ziw(Pv`LQC`&9J!Y0U08m)Q6Q^6(^hfUC5@dS0eA@P}8ln0Jdd zXNK`!SI;6_#Yl-ZX`NZ#JkAxw8OSwvDu=N6F$^UKp|mPCSsPnqJNL6)pIoTm)vv!3 zUSxis3;pNp?a#-=-02R097zQBFo8h9DbqEjLd^}-__PnFrX;#(RQ`Q-{ zXC_j5JSJo9Q`*OkxS^aUb~32wf;ht&hr&U;z24P)@FH%=s}Sx3f(b`N@Py6#<&|~_ z!zAm)XGQvPuTNinPh`~&d50nwaYGX}VI&Ffjx^H#oK@~-Nti^LZgwCwZ#49s zGaP)>!@AjR@D<>gb!O?ASjBL{)2XJ3QJ^625F6DtBqS=@i1}gX^r|3)%JeHlf%*WfYAVp@8C<CbIfSA^*l2uy zyj&XqEx-4y@toL_L^N0HFTkVsO49S4a1YmVch+3R%aVRRY`5w9@I<$H&u($?J|AVI zZOi(L4PUZ2^1DikuaBT^*|H-mg25Ld|Jk|zsPrf5N_BZ%d3A9Dq$V4csA7wSv{5ce36!k^7O`|1R)r9P!or zgPE*}6qh}xv)BrsL<>eCtc^Sa>rkhNyw{Cw6w6FpW*GkskRz!}lBW~^jYy>MiqCmx zWge1wA^HG>4(eCpeYQxFGMG?KQM4?h*6coK_R$~Fg&$5pukr@prSq*CIef5_v!VWi0K*(#kk&~1sI>WV`Bt|XcFp|u5du9x#H|^9T-b^- z4l&CAf#W=Ue;f$e|1tQ@iaWRM)Br&WcX>-5eXP^>nY_)QKPc=i7SY__^!G+?l&4Gz z%L65(y~Xnoq^@m}O?4nS5WBr;?6Z~iQu_92M^+K}ytedPc(*VhQ11tvT-}&7Tb$O& zd*$Hzx+K`=LTc7l9Z{1I1;ULEhh5In`UGf2hznhQHCNnkeBU#LOK9iTsjIgu6c8a? zrj*Fp0hF))gpV8MAO&go2ID9jZQVUt${h4{^Kk*oLG7;1X6d_;-bPKxWRP}mmqQd!T|D*sSk{107z%uRih^x89GEncesic z)_@#D72;amEs^!|?oK04js&9*gXv@NR%hJtwhE66)%hjq2~~M9o}k%T*C>>9SF2*Hep1vg<>k>%z=%H~Jo{-ihLSI4Qtv zI&wt;Bvoo6PmjuHzC6e|`*rr+9k_o8&Q>PPEjSt2 z5-&O08-R!5+1~GULi%%iaMMYeegy6w(-3K3PYd(FU)l2w{8%yCPA#C61vdr?tjrM+ z{lr;uJ!P}2rYADQiZ1vKH0&5hNEzJUO)}aRlk$aHV0aA8_kgNHJpiJS8Hb@f@UCGp zNocl-40It-uMi~AZ#hkX1sn1>*%38h@h1z{bZ@n>{MD`z>j^m2!;ArwSX0EaD1O5? z6J5bI_Ka~diS^cJh!vmpz;J@Jfs6KXr>5)UN~_~p42Hr4Z#10f`MkDpc1|fN`@mhC z9+jLe>5Gg4$Wv&UO^_>gj&U`}6s|T2LU}Sp;LhLj(vI37k3lF4 z6Ov4aBnwsmWMg4^;jC%gE`EKr=3**ML5`-AiQ^EC+V0cgA|PmEpS7v{<8f&&M}lAd zXWYK8K zd@$#&xlm|Xl=qa&U}3ikoIfLV0JZ z+?F*omU&(=Q@qtyh34m?&(DasEKzRh$(`%xgxwFl=ZM04E%BarcL8ST<%0u7b~vX1=N6>7Af6ZenY^Kn@28wf5AY9N*%8M zwWBmulkTtpJmLESOxVKo)RK)lix}zd^e%OmmiXa~ZU^@bu86m_$tX?a_;=su>iD8Q z%B}A3cTNVwR7PaHfx+2{RLUs#@qR&Z9@IpJr99`S7z6*zwC|!RI4&G54$*2q;P>j7 zImo=qDhubcT%F|7H89Sc# zy@%YGc){3ai^4kSp4~5*Z0~T^1xB}JDU<1dOT8rtHUI2*R){t>WVRCitQb3ppxyx> z>_4}ctk0jwDxBOP%V;|Px?NTfsHRLb&|<~OBu zoY6_1Ec}FnHFYs#Z0R6Q(oTNgMN7UjnyRem6*G*xU>qvn`lE1vfbYuSM$3!#4-%Ox z%%``lhIAA2eZa#u#L{d@`0)1mlAvOH1-PbI+Q-}~wiA156g-@ql{Ukp4-01Bh+(r& zPTSHAg{Q{~JSsq8#4WRKH3cF3NWW7GL{XIqd@C%`Ig+g9RTxSX<{5vvnTp$h?`d&R zKr_jQo3sL(m14NjY;e@_1*J~F+`f0m@dqmHh4L&?iAL%H-+AEdcTHHmZ7_#QRmvR7?|GN+juQS)M!QceB-*gvj2wUi<21o5t6?(l6+@<8bmWD^XY5tipn&tO4PgohkCo~*za5}8{H74@n4 zP=Fnt@s8J(vEeZ`MwW71vw?Ln-S(}1u`k(E9>yf5Q3ZPjje-K zE0AL4T>e4`)Hf_AOonxlWP8V_LLwlH6;EfZ7Q-!8?xCs6y4%&4X{v+n#oJ=ow58w# z$Qb)$#(7{_SkKjODq#mYp%W=b`bW$aM6PFZ3UI(m5^FPaYiD)gSz6?0>t#krL0I87 z0zBAuv;&7;mo`W28-aBry1DdCnCulDz-7lL##yy8F%p9h$*op)8dJ5)SPk9_#G&6L

!&5QpMCe{nkv8gCswhA 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 literal 0 HcmV?d00001 diff --git a/docs/internal/Abraeumen.md b/docs/internal/Abraeumen.md index 39ba9a4..4f6dd0b 100644 --- a/docs/internal/Abraeumen.md +++ b/docs/internal/Abraeumen.md @@ -6,7 +6,7 @@ Nach dem Druck kann Grotti auf der **Kobra 3** Bett und Düse abkühlen und die ## Quittierung vs. Schieben -- **Wand „Platte geräumt“:** Nur Quittierung. Tabelle `bed_status`, API `POST /api/v1/drucker//abraeumen` mit `confirm`. Fährt keine Bahn. `needs_clear` setzt der Watcher beim Live-Übergang Druckende/Abbruch/Fehler (nicht schon beim Start; nicht blind beim ersten Blick nach Neustart). Solange gesetzt, startet „wenn frei“ den nächsten Job nicht. +- **Wand „Platte geräumt“:** Quittierung + optional Urteil. Tabelle `bed_status`, API `POST /api/v1/drucker//abraeumen` mit `confirm`. Fährt keine Bahn. Offener Live-Lauf wird als `completed` geschlossen (History von Moonraker oft ohne Endzeit). `needs_clear` setzt der Watcher beim Live-Übergang Druckende/Abbruch/Fehler (nicht schon beim Start; nicht blind beim ersten Blick nach Neustart). Solange gesetzt, startet „wenn frei“ den nächsten Job nicht. - **Ablauf (Dateien):** Drucken → Abkühlen → Schieben → Tasten, dann der nächste Druck mit LeviQ. Eine Bestätigung beim Einreihen; Schieben läuft ohne zweite Rückfrage. Nach erfolgreichem Schieben ist die Platte automatisch quittiert. - **Normale Drucke** und „Nochmal“ schieben nicht — nach dem Druck muss man auf der Wand quittieren. diff --git a/docs/internal/Dateien-Metadaten.md b/docs/internal/Dateien-Metadaten.md index 9e43a98..305dd53 100644 --- a/docs/internal/Dateien-Metadaten.md +++ b/docs/internal/Dateien-Metadaten.md @@ -14,7 +14,7 @@ In der Bibliothek gespeichert: | Feld | Bedeutung | |------|-----------| -| `filaments` | Typ/Farbe (`paint_info` oder `filament_type`/`filament_colour`) | +| `filaments` | Typ/Farbe (`paint_info` oder `filament_type`/`filament_colour`) plus `profile` aus `filament_settings_id` (Sorte, z. B. Sunlu PLA Meta) | | `weight_g` / `estimated_s` | Filamentgewicht und Druckzeit | | `printer_family` | aus `printer_model` | | `slice_meta` | jsonb: `layer_height`, `layers`, `max_z_mm`, `nozzle_mm`, `temp_nozzle`, `temp_bed`, `slicer` | diff --git a/docs/internal/Features-UI.md b/docs/internal/Features-UI.md index 3f8fc14..3af424d 100644 --- a/docs/internal/Features-UI.md +++ b/docs/internal/Features-UI.md @@ -1,6 +1,6 @@ # Grotti: Alltag und Oberfläche -Stand **18.10.2026**, v3.2.0. +Stand **18.10.2026**, v3.3.0. ## Design diff --git a/docs/internal/Ops-Queue-Alerts.md b/docs/internal/Ops-Queue-Alerts.md index 4d923d6..25c0aa8 100644 --- a/docs/internal/Ops-Queue-Alerts.md +++ b/docs/internal/Ops-Queue-Alerts.md @@ -12,7 +12,7 @@ Seite `/einstellungen`: Druckernamen/Host/Familie/gesperrt (PostgreSQL `printers ## Druckwarteschlange „wenn frei“ -Dateien → „Wenn frei“ oder „Nächster freier“ → `when_free` in `POST /api/v1/senden`. Einträge in `print_queue`; Dispatcher startet, sobald der Host (oder ein freier Drucker) `standby`/`complete`/`cancelled` ist, Platte frei und nicht gesperrt. **Alle freien Drucker im selben Tick** (Upload/Start je Host im Worker parallel) — nicht einer alle ~8 s. Sidebar und Seite `/warteschlange`. Sofort-Senden bleibt über die Send-Queue (mehrere Worker). Hängender Fehler wird nur geräumt, wenn für den Host ein Auftrag wartet. Ablauf: Phase `starting` vor Upload; Timeout 10 min ohne `print_active`. +Dateien → „Dieser Drucker“ (belegt) oder „Nächster passender“ → `when_free` in `POST /api/v1/senden`. Einträge in `print_queue`; Dispatcher startet, sobald der Host (oder ein freier Drucker) `standby`/`complete`/`cancelled` ist, Platte frei und nicht gesperrt. Material-Score: Katalog-Sorte aus `filament_settings_id` (streng), Farbe soft; bei ACE und leerer Map baut Grotti die Zuordnung aus Spulen. **Alle freien Drucker im selben Tick** (Upload/Start je Host im Worker parallel) — nicht einer alle ~8 s. Sidebar und Seite `/warteschlange`. Sofort-Senden bleibt über die Send-Queue (mehrere Worker). Hängender Fehler wird nur geräumt, wenn für den Host ein Auftrag wartet. Ablauf: Phase `starting` vor Upload; Timeout 10 min ohne `print_active`. ## Spulen diff --git a/grotti/acm.py b/grotti/acm.py index f826ea0..8d20d74 100644 --- a/grotti/acm.py +++ b/grotti/acm.py @@ -73,13 +73,182 @@ def normalize_mapping(raw): ams = int(item.get("ams_index")) except (TypeError, ValueError): continue - if paint < 0 or ams < 0 or ams > 7 or paint in seen: + # paint_index = Slicer-T (oft 0–15 bei Bambu), ams_index = ACE-Platz 0–7 + if paint < 0 or paint > 63 or ams < 0 or ams > 7 or paint in seen: continue seen.add(paint) out.append({"paint_index": paint, "ams_index": ams}) return out +def filament_for_paint(filaments, paint): + """Filament-Eintrag zur Slicer-T-Nummer; Fallback Array-Index für Alt-Daten.""" + filaments = filaments or [] + try: + paint = int(paint) + except (TypeError, ValueError): + return {} + for index, row in enumerate(filaments): + if not isinstance(row, dict): + continue + try: + key = int(row.get("paint_index")) + except (TypeError, ValueError): + key = index + if key == paint: + return row + if 0 <= paint < len(filaments) and isinstance(filaments[paint], dict): + return filaments[paint] + return {} + + +def densify_mapping(mapping): + """Slicer-T mit Lücken (T1/T10) → dichte 0…n−1 für gklib/ACE. + + gklib meldet bei zu hoher Filament-ID ``invalid filament id`` (10011700). + Rückgabe: (remap alt→neu, dichte Map). Identity → ({}, unverändert). + """ + mapping = normalize_mapping(mapping) + paints = sorted(item["paint_index"] for item in mapping) + if not paints or paints == list(range(len(paints))): + return {}, mapping + remap = {old: new for new, old in enumerate(paints)} + dense = [ + {"paint_index": remap[item["paint_index"]], "ams_index": item["ams_index"]} + for item in mapping + ] + return remap, dense + + +def remap_filaments(filaments, remap): + """paint_index der Filament-Metadaten mit densify-Remap mitziehen.""" + if not remap: + return list(filaments or []) + out = [] + for row in filaments or []: + if not isinstance(row, dict): + continue + copy = dict(row) + try: + old = int(row.get("paint_index")) + except (TypeError, ValueError): + out.append(copy) + continue + if old in remap: + copy["paint_index"] = remap[old] + out.append(copy) + return out + + +def align_tools_for_ace(content, mapping, filaments=None): + """G-code-T und ACE-Map auf dichte 0…n−1 bringen. + + Maßgeblich sind die ``Tn`` im G-code. Die UI-Map darf echte Slicer-Indizes + (T1/T10) oder fälschlich kompakte 0/1 haben — ACE-Plätze werden in + Sortierreihenfolge der Map-Einträge den sortierten T-Nummern zugeordnet. + Sonst: ACM 0/1 + G-code T10 → 10011700 invalid filament id. + """ + from grotti.parse import tools_used_in_gcode + + mapping = normalize_mapping(mapping) + used = sorted(tools_used_in_gcode(content)) + if not mapping: + return content, mapping, list(filaments or []), {} + if not used: + remap, dense = densify_mapping(mapping) + new_content = rewrite_gcode_tools(content, remap) if remap else content + return new_content, dense, remap_filaments(filaments, remap), remap + + if len(mapping) != len(used): + from grotti.errors import GrottiError + + raise GrottiError( + "ACE-Zuordnung (%d Farben) passt nicht zum G-code (T%s). " + "Datei neu lesen und Zuordnung erneut setzen." + % (len(mapping), ", T".join(str(t) for t in used)), + "ace", + ) + + mapped_paints = {item["paint_index"] for item in mapping} + paints_match_gcode = mapped_paints == set(used) + if paints_match_gcode: + ams_by_tool = {item["paint_index"]: item["ams_index"] for item in mapping} + ams_order = [ams_by_tool[tool] for tool in used] + else: + # UI kompakt 0…n−1, G-code sparse — Reihenfolge der Map = Reihenfolge der Farben + ordered = sorted(mapping, key=lambda item: item["paint_index"]) + ams_order = [item["ams_index"] for item in ordered] + + remap = {old: new for new, old in enumerate(used)} + dense = [ + {"paint_index": index, "ams_index": ams_order[index]} + for index in range(len(used)) + ] + + source = list(filaments or []) + if paints_match_gcode: + new_fils = remap_filaments(source, remap) + else: + # Metadaten folgen der Map-Reihenfolge (0,1,…), nicht den G-code-T + ordered_paints = sorted(item["paint_index"] for item in mapping) + new_fils = [] + for index, tool in enumerate(used): + row = filament_for_paint(source, ordered_paints[index]) or ( + source[index] if index < len(source) and isinstance(source[index], dict) else {} + ) + if not row: + row = filament_for_paint(source, tool) or {} + copy = dict(row) if isinstance(row, dict) else {} + copy["paint_index"] = index + new_fils.append(copy) + + # Identity-Remap weglassen (schon T0…n−1) + if used == list(range(len(used))): + return content, dense, new_fils, {} + new_content = rewrite_gcode_tools(content, remap) + return new_content, dense, new_fils, remap + + +_TOOL_NUM_RE = re.compile( + rb"^(?P(?:MMU_CHANGE_TOOL(?:_STANDALONE)?[^\n]*?\bTOOL=)|T)(?P\d{1,2})\b", + re.I, +) +_REF_TOOLS_RE = re.compile(rb"^(;\s*referenced_tools\s*=\s*)(.+?)\s*$", re.I) + + +def rewrite_gcode_tools(content, remap): + """Tn / TOOL=n / referenced_tools laut Remap umschreiben (T10 vor T1).""" + if not content or not remap: + return content + text = content if isinstance(content, (bytes, bytearray)) else str(content).encode("utf-8", "replace") + lines = text.splitlines(keepends=True) + out = [] + for line in lines: + stripped = line[:-2] if line.endswith(b"\r\n") else (line[:-1] if line.endswith(b"\n") else line) + ending = line[len(stripped) :] + ref = _REF_TOOLS_RE.match(stripped) + if ref: + parts = [] + for bit in ref.group(2).split(b","): + raw = bit.strip() + try: + number = int(raw) + except ValueError: + parts.append(raw) + continue + parts.append(str(remap.get(number, number)).encode("ascii")) + out.append(ref.group(1) + b",".join(parts) + ending) + continue + match = _TOOL_NUM_RE.match(stripped) + if match: + old = int(match.group("num")) + if old in remap: + new = str(remap[old]).encode("ascii") + stripped = stripped[: match.start("num")] + new + stripped[match.end("num") :] + out.append(stripped + ending) + return b"".join(out) + + def build_acm(filaments, mapping, slots): filaments = filaments or [] slots = slots or [] @@ -88,7 +257,7 @@ def build_acm(filaments, mapping, slots): for item in normalize_mapping(mapping): paint = item["paint_index"] ams = item["ams_index"] - filament = filaments[paint] if 0 <= paint < len(filaments) else {} + filament = filament_for_paint(filaments, paint) slot = slot_by_index.get(ams) or {} paint_color = color_channels(filament.get("color")) or [0, 0, 0] ams_color = color_channels(slot.get("color")) or paint_color diff --git a/grotti/jobs.py b/grotti/jobs.py index 0e99420..a0e071f 100644 --- a/grotti/jobs.py +++ b/grotti/jobs.py @@ -57,25 +57,47 @@ def unix_to_dt(value): def choose_spool(conn, host, material, hinted_id, ended_at=None): """Spule für History-Abzug. Ohne Hint nur Rollen, die vor Druckende existierten.""" + at_host = [ + row for row in store.spools_for_printer(conn, host) + if _spool_valid_for_end(row, ended_at) + ] + if hinted_id: + row = store.get_spool_row(conn, hinted_id) + if row and not _spool_valid_for_end(row, ended_at): + return None + # Hint nur nutzen, wenn die Rolle noch am Drucker liegt — sonst Rollenwechsel + if row and (row.get("printer_host") or "") == (host or ""): + return row + if row and not at_host: + # Rolle schon im Regal, kein Nachfolger am Host → Hint behalten (alte Jobs) + return row + if len(at_host) == 1: + return at_host[0] + if material: + same = [row for row in at_host if (row["material_type"] or "").lower() == material.lower()] + if len(same) == 1: + return same[0] if hinted_id: row = store.get_spool_row(conn, hinted_id) if row and _spool_valid_for_end(row, ended_at): return row - if row and not _spool_valid_for_end(row, ended_at): - return None - rows = [ - row for row in store.spools_for_printer(conn, host) - if _spool_valid_for_end(row, ended_at) - ] - if len(rows) == 1: - return rows[0] - if material: - same = [row for row in rows if (row["material_type"] or "").lower() == material.lower()] - if len(same) == 1: - return same[0] return None +def resolve_tracking_spool(conn, host, hinted_id, ace_map=None): + """Spule für Live-Tracking: am Host liegende Rolle schlägt veralteten Send-Hint.""" + current = resolve_spools_at_host(conn, host, ace_map=ace_map) + if len(current) == 1: + return current[0]["id"] + if hinted_id: + row = store.get_spool_row(conn, hinted_id) + if row and (row.get("printer_host") or "") == (host or ""): + return hinted_id + if current: + return current[0]["id"] + return hinted_id + + def _spool_valid_for_end(spool, ended_at): """Verhindert Abzug alter Moonraker-Jobs auf neu eingelegte Spulen.""" if ended_at is None or not spool: @@ -544,12 +566,30 @@ def send_file( ) # CJK/Unicode im Namen → Rinkhals 10115 not found (Upload ≠ Start-Name) filename = moonraker.safe_upload_name(filename) - ace_tools = [item.get("paint_index") for item in mapping] if mapping else None if (row.get("kind") or "") == "gcode" and str(filename).endswith(".gcode"): + if slots and mapping: + # Immer an G-code-Tn ausrichten (auch wenn UI fälschlich 0/1 schickt) + content, mapping, filaments, remap = acm.align_tools_for_ace( + content, mapping, filaments + ) + if remap: + progress( + "ace", + 32, + "Slicer-T verdichtet (%s)…" + % ", ".join("T%s→T%s" % pair for pair in sorted(remap.items())), + ) + ace_tools = [item.get("paint_index") for item in mapping] if mapping else None content = prepare_rinkhals_gcode(content, tools=ace_tools) if keep_motors: # Slice-Ende `M84` → sofort unhomed; Abräumen braucht die Stepper. content = eject.strip_end_motor_disable(content) + else: + if slots and mapping: + _, mapping, filaments, _ = acm.align_tools_for_ace( + b"", mapping, filaments + ) + ace_tools = [item.get("paint_index") for item in mapping] if mapping else None size_mb = len(content) / (1024 * 1024) def upload_progress(sent, total): diff --git a/grotti/library.py b/grotti/library.py index f019970..317e5da 100644 --- a/grotti/library.py +++ b/grotti/library.py @@ -59,10 +59,55 @@ def sync_watch(conn, cfg, force=False): except (OSError, zipfile.BadZipFile): continue store.forget_missing_watch(conn, seen) + link_watch_files_to_projects(conn, cfg) _LAST_WATCH_SYNC = now return {"gelesen": len(seen), "hinweis": ""} +def _apply_meta(conn, file_id, row, meta): + store.update_file( + conn, + file_id, + { + "printer_family": meta.get("printer_family") or row.get("printer_family"), + "filaments": meta.get("filaments") or [], + "weight_g": meta.get("weight_g") or 0, + "estimated_s": meta.get("estimated_s") or 0, + "thumb_entry": meta.get("thumb_entry") or "", + "kind": meta.get("kind") or row.get("kind"), + "plate": meta.get("plate") or "", + "slice_meta": meta.get("slice_meta") or {}, + }, + ) + + +def refresh_one_file(conn, file_id, watch_dir=None): + """Eine Datei neu parsen (paint_index / ACE). None wenn nicht gefunden.""" + row = store.get_file_row(conn, file_id) + if not row: + return None + stored = row.get("stored_path") or "" + path = Path(stored) if stored else None + if path is not None and path.is_file(): + try: + meta = meta_for(path) + _apply_meta(conn, file_id, row, meta) + row = store.get_file_row(conn, file_id) or row + except (OSError, zipfile.BadZipFile): + pass + return store.file_out(row, watch_dir=watch_dir) + + +def filaments_need_paint_index(filaments): + rows = filaments if isinstance(filaments, list) else [] + if len(rows) < 2: + return False + for item in rows: + if not isinstance(item, dict) or "paint_index" not in item: + return True + return False + + def refresh_file_meta(conn, force=False): """Metadaten bestehender Dateien neu lesen (z. B. paint_info → filaments).""" if not force: @@ -80,20 +125,7 @@ def refresh_file_meta(conn, force=False): meta = meta_for(path) except (OSError, zipfile.BadZipFile): continue - store.update_file( - conn, - row["id"], - { - "printer_family": meta.get("printer_family") or row.get("printer_family"), - "filaments": meta.get("filaments") or [], - "weight_g": meta.get("weight_g") or 0, - "estimated_s": meta.get("estimated_s") or 0, - "thumb_entry": meta.get("thumb_entry") or "", - "kind": meta.get("kind") or row.get("kind"), - "plate": meta.get("plate") or "", - "slice_meta": meta.get("slice_meta") or {}, - }, - ) + _apply_meta(conn, row["id"], row, meta) updated += 1 return updated @@ -213,27 +245,39 @@ def delete_folder(cfg, rel): return folder +def _link_file_to_project_folder(conn, file_id, stored_path, watch_dir): + """Wenn Pfad unter einem Projekt-Ordner liegt → project_id setzen.""" + folder = store.file_folder(stored_path, "watch", watch_dir) + if not folder: + return + for row in store.list_project_folders(conn): + base = (row.get("folder") or "").strip().replace("\\", "/").strip("/") + if not base: + continue + if folder == base or folder.startswith(base + "/"): + store.set_file_project(conn, file_id, row["id"]) + return + + def move_file(conn, cfg, file_id, folder): - """Watch-Datei in Unterordner (oder Wurzel) verschieben; ID bleibt.""" + """Watch- oder Upload-Datei in Slice-Unterordner (oder Wurzel); Upload wird Watch.""" root = require_watch_dir(cfg) folder = normalize_folder(folder) row = store.get_file_row(conn, file_id) if not row: raise GrottiError("Die Datei fehlt.", "datei", 404) - if row.get("source") != "watch": - raise GrottiError( - "Nur Dateien aus dem Slice-Ordner lassen sich verschieben.", - "watch", - ) + source = row.get("source") or "" + if source not in ("watch", "upload"): + raise GrottiError("Diese Datei lässt sich nicht verschieben.", "datei") src = Path(row.get("stored_path") or "") if not src.is_file(): raise GrottiError("Die Datei liegt nicht mehr auf der Platte.", "datei", 404) dest_dir = path_under_watch(root, folder) dest_dir.mkdir(parents=True, exist_ok=True) dest = dest_dir / src.name - if dest.resolve() == src.resolve(): + if dest.resolve() == src.resolve() and source == "watch": return store.file_out(row, watch_dir=root) - if dest.exists(): + if dest.exists() and dest.resolve() != src.resolve(): dest = unique_path(dest_dir, src.name) try: shutil.move(str(src), str(dest)) @@ -247,11 +291,204 @@ def move_file(conn, cfg, file_id, folder): name=dest.name, size_bytes=stat.st_size, mtime=stat.st_mtime, + source="watch", ) + _link_file_to_project_folder(conn, file_id, str(dest.resolve()), root) row = store.get_file_row(conn, file_id) return store.file_out(row, watch_dir=root) +def path_allowed_for_delete(stored_path, cfg): + """Nur Dateien unter Slice-Ordner oder data/eingang.""" + try: + resolved = Path(stored_path).resolve() + except (OSError, ValueError): + return False + roots = [] + watch = cfg.get("watch_dir") + if watch: + roots.append(Path(watch).resolve()) + data_dir = cfg.get("data_dir") + if data_dir: + roots.append((Path(data_dir) / "eingang").resolve()) + for root in roots: + try: + if resolved == root or root in resolved.parents: + return True + except OSError: + continue + return False + + +def delete_library_file(conn, cfg, file_id): + """Bibliotheksdatei löschen (Upload oder Watch) — DB + Datei auf der Platte.""" + row = store.get_file_row(conn, file_id) + if not row: + raise GrottiError("Die Datei fehlt.", "datei", 404) + stored = row.get("stored_path") or "" + if stored and not path_allowed_for_delete(stored, cfg): + raise GrottiError("Löschen außerhalb von Slice-Ordner oder Eingang ist nicht erlaubt.", "datei") + store.delete_file(conn, file_id) + if stored: + Path(stored).unlink(missing_ok=True) + return row + + +_PRINT_SUFFIXES = (".gcode", ".3mf", ".gcode.3mf") + + +def is_print_filename(name): + low = (name or "").lower() + return any(low.endswith(suf) for suf in _PRINT_SUFFIXES) + + +def project_slug(name): + """Ordner-sichere Kurzform aus Projektnamen.""" + text = re.sub(r"[^\w\- äöüÄÖÜß.]+", "-", (name or "").strip(), flags=re.UNICODE) + text = re.sub(r"\s+", "-", text) + text = re.sub(r"-{2,}", "-", text).strip("-.") + text = text[:60] or "projekt" + return normalize_folder(text) + + +def ensure_project_folder(cfg, folder_rel): + """Projektordner unter watch_dir anlegen; gibt normalisierten Relativpfad zurück.""" + root = require_watch_dir(cfg) + folder = normalize_folder(folder_rel) + if not folder: + raise GrottiError("Projektordner fehlt.", "ordner") + path_under_watch(root, folder).mkdir(parents=True, exist_ok=True) + return folder + + +def default_project_folder(name): + return "Projekte/" + project_slug(name) + + +def link_watch_files_to_projects(conn, cfg): + """Watch-Dateien in Projektordnern dem passenden Projekt zuordnen.""" + watch = cfg.get("watch_dir") + if not watch: + return 0 + projects = store.list_project_folders(conn) + if not projects: + return 0 + linked = 0 + for row in store.list_file_rows(conn): + if (row.get("source") or "") != "watch": + continue + folder = store.file_folder(row.get("stored_path"), "watch", watch) + if not folder: + continue + for proj in projects: + base = (proj.get("folder") or "").strip().replace("\\", "/").strip("/") + if not base: + continue + if folder == base or folder.startswith(base + "/"): + if row.get("project_id") != proj["id"]: + store.set_file_project(conn, row["id"], proj["id"]) + linked += 1 + break + return linked + + +def list_project_assets(cfg, folder_rel): + """Nicht-Druckdateien im Projektordner (flach + Unterordner).""" + root = require_watch_dir(cfg) + folder = normalize_folder(folder_rel) + if not folder: + return [] + base = path_under_watch(root, folder) + if not base.is_dir(): + return [] + items = [] + for path in sorted(base.rglob("*")): + if not path.is_file() or skip_path(path, cfg["data_dir"]): + continue + if is_print_filename(path.name): + continue + try: + rel = path.resolve().relative_to(base).as_posix() + except ValueError: + continue + stat = path.stat() + items.append({ + "name": path.name, + "rel": rel, + "size_bytes": stat.st_size, + "mtime": stat.st_mtime, + }) + return items + + +def resolve_project_asset(cfg, folder_rel, rel_name): + """Sicheren Pfad zu einer Asset-Datei im Projektordner.""" + root = require_watch_dir(cfg) + folder = normalize_folder(folder_rel) + if not folder: + raise GrottiError("Projektordner fehlt.", "ordner") + base = path_under_watch(root, folder) + rel = str(rel_name or "").replace("\\", "/").strip("/") + if not rel or ".." in rel.split("/"): + raise GrottiError("Ungültiger Dateiname.", "datei") + target = (base / rel).resolve() + if base not in target.parents and target != base: + raise GrottiError("Pfad außerhalb des Projektordners.", "ordner") + if not target.is_file(): + raise GrottiError("Die Datei fehlt.", "datei", 404) + return target + + +def save_project_asset(cfg, folder_rel, filename, content): + root = require_watch_dir(cfg) + folder = normalize_folder(folder_rel) + if not folder: + raise GrottiError("Projektordner fehlt.", "ordner") + dest_dir = path_under_watch(root, folder) + dest_dir.mkdir(parents=True, exist_ok=True) + path = unique_path(dest_dir, filename) + path.write_bytes(content) + return { + "name": path.name, + "rel": path.name, + "size_bytes": path.stat().st_size, + "mtime": path.stat().st_mtime, + } + + +def delete_project_asset(cfg, folder_rel, rel_name): + path = resolve_project_asset(cfg, folder_rel, rel_name) + path.unlink(missing_ok=True) + return True + + +def delete_project_disk_folder(cfg, folder_rel, force=False): + """Projektordner auf der Platte entfernen (leer oder force inkl. Inhalt).""" + if not folder_rel: + return + try: + root = require_watch_dir(cfg) + except GrottiError: + return + folder = normalize_folder(folder_rel) + if not folder: + return + target = path_under_watch(root, folder) + if not target.is_dir(): + return + if force: + shutil.rmtree(target, ignore_errors=True) + return + try: + next(target.iterdir()) + raise GrottiError( + "Projektordner ist nicht leer — mit Bestätigung inkl. Inhalt löschen.", + "ordner", + ) + except StopIteration: + target.rmdir() + + def save_upload(conn, filename, content, cfg, project_id=None): directory = cfg["data_dir"] / "eingang" directory.mkdir(parents=True, exist_ok=True) diff --git a/grotti/migrations/012_spool_archived.sql b/grotti/migrations/012_spool_archived.sql new file mode 100644 index 0000000..adfa201 --- /dev/null +++ b/grotti/migrations/012_spool_archived.sql @@ -0,0 +1,2 @@ +-- Leere/alte Spulen aus der Alltagsliste nehmen +ALTER TABLE spools ADD COLUMN IF NOT EXISTS archived_at timestamptz; diff --git a/grotti/migrations/013_project_folder.sql b/grotti/migrations/013_project_folder.sql new file mode 100644 index 0000000..811e7aa --- /dev/null +++ b/grotti/migrations/013_project_folder.sql @@ -0,0 +1,2 @@ +-- Projekt-Arbeitsmappe: Relativordner unter dem Slice-Pfad +ALTER TABLE projects ADD COLUMN IF NOT EXISTS folder text NOT NULL DEFAULT ''; diff --git a/grotti/parse.py b/grotti/parse.py index 57ae511..a58665b 100644 --- a/grotti/parse.py +++ b/grotti/parse.py @@ -91,40 +91,111 @@ def color_hex(channels): return "#%02x%02x%02x" % tuple(rgb) +def profile_name(raw): + """Orca-Profil → Katalogname: `Sunlu PLA Meta @Kobra 3` → `Sunlu PLA Meta`.""" + text = (raw or "").strip().strip('"').strip("'").strip() + if " @" in text: + text = text.rsplit(" @", 1)[0].strip() + return text + + +def filament_profiles(comments): + """Profilnamen aus `; filament_settings_id`, Index = Slicer-Slot / paint_index.""" + raw = (comments or {}).get("filament_settings_id") or "" + if not raw.strip(): + fallback = profile_name((comments or {}).get("default_filament_profile") or "") + return [fallback] if fallback else [] + return [profile_name(part) for part in raw.split(";") if profile_name(part)] + + +def attach_profiles(filaments, comments): + """Hängt `profile` (Sorte) an Filament-Einträge anhand des paint_index.""" + profiles = filament_profiles(comments) + if not filaments: + return filaments + if not profiles: + return filaments + for index, row in enumerate(filaments): + if not isinstance(row, dict): + continue + try: + paint = int(row.get("paint_index")) + except (TypeError, ValueError): + paint = index + name = "" + if 0 <= paint < len(profiles): + name = profiles[paint] + elif len(filaments) == 1 and len(profiles) == 1: + name = profiles[0] + if name: + row["profile"] = name + return filaments + + def filaments_from_paint_info(raw): + """Farben aus `; paint_info` — inkl. originalem `paint_index` (T-Nummer). + + Bambu/Anycubic nutzen oft Lücken (T1 + T10 bei 16 Slots). Die Liste bleibt + kompakt (nur belegte Farben), aber jeder Eintrag trägt den Slicer-Index, + damit `.acm` und ACE-Map denselben Wert wie `Tn` im G-code schreiben. + """ try: items = json.loads(raw or "") except (TypeError, json.JSONDecodeError): return [] if not isinstance(items, list): return [] - found = [] + found = {} + next_fallback = 0 for item in items: if not isinstance(item, dict): continue try: index = int(item.get("paint_index")) except (TypeError, ValueError): - index = len(found) - while len(found) <= index: - found.append({"type": "", "color": ""}) - found[index] = { + index = next_fallback + if index < 0: + continue + next_fallback = max(next_fallback, index + 1) + row = { "type": str(item.get("material_type") or "").strip(), "color": color_hex(item.get("paint_color")), + "paint_index": index, } - return [row for row in found if row["type"] or row["color"]] + if row["type"] or row["color"]: + found[index] = row + return [found[key] for key in sorted(found)] def filaments_of(comments): painted = filaments_from_paint_info(comments.get("paint_info")) if painted: - return painted + return attach_profiles(painted, comments) types = [part.strip() for part in (comments.get("filament_type") or "").split(";") if part.strip()] colors = [part.strip() for part in (comments.get("filament_colour") or comments.get("filament_color") or "").split(";")] - return [ - {"type": kind, "color": colors[index] if index < len(colors) else ""} + rows = [ + { + "type": kind, + "color": colors[index] if index < len(colors) else "", + "paint_index": index, + } for index, kind in enumerate(types) ] + return attach_profiles(rows, comments) + + +def tools_used_in_gcode(content): + """T-/MMU-Werkzeuge aus dem G-code (wie Rinkhals mmu_ace_metadata).""" + if not content: + return set() + if isinstance(content, str): + content = content.encode("utf-8", "replace") + found = set() + for line in _gcode_lines(content): + match = _TOOL_RE.match(line) + if match: + found.add(int(match.group(1))) + return found def _gcode_lines(content): diff --git a/grotti/print_queue.py b/grotti/print_queue.py index 612a215..77c0216 100644 --- a/grotti/print_queue.py +++ b/grotti/print_queue.py @@ -10,7 +10,7 @@ import threading import time from datetime import datetime, timezone -from grotti import eject, jobs, moonraker, notify, store +from grotti import acm, eject, jobs, moonraker, notify, store from grotti.db import connect from grotti.errors import GrottiError from grotti import time_window @@ -306,6 +306,11 @@ class PrintQueueDispatcher: if (waiting.get("message") or "") != WINDOW_WAIT_MSG: store.update_print_queue(conn, waiting["id"], {"message": WINDOW_WAIT_MSG}) continue + # Nächster passender / leere Map: ACE aus Spulen bauen oder warten + if not _has_ablauf(waiting): + waiting = self._ensure_ace_map(conn, waiting, host, live) + if not waiting: + continue claimed = store.claim_print_queue(conn, waiting["id"], host) if not claimed: continue @@ -317,6 +322,49 @@ class PrintQueueDispatcher: self._start_simple(conn, claimed, printer) conn.commit() + def _ensure_ace_map(self, conn, waiting, host, live): + """Bei ACE-Drucker und leerer Map: aus Spulen zuordnen oder Wartegrund setzen.""" + slots = (live or {}).get("slots") or [] + if not slots: + return waiting + ace = waiting.get("ace_map") or [] + if isinstance(ace, str): + import json + + try: + ace = json.loads(ace) + except json.JSONDecodeError: + ace = [] + if acm.normalize_mapping(ace if isinstance(ace, list) else []): + return waiting + file_id = waiting.get("file_id") + if not file_id: + return waiting + mapping, reason = store.auto_ace_map(conn, host, file_id) + if reason: + if (waiting.get("wait_reason") or "") != reason or (waiting.get("message") or "") != reason: + store.update_print_queue( + conn, + waiting["id"], + {"wait_reason": reason, "message": reason}, + ) + return None + if not mapping: + # ACE am Drucker, aber keine ACE-Spulen in Grotti → nicht raten + reason = "ACE-Zuordnung fehlt (keine passende Spule)" + if (waiting.get("wait_reason") or "") != reason: + store.update_print_queue( + conn, + waiting["id"], + {"wait_reason": reason, "message": reason}, + ) + return None + store.update_print_queue(conn, waiting["id"], {"ace_map": mapping, "wait_reason": "", "message": ""}) + waiting = dict(waiting) + waiting["ace_map"] = mapping + waiting["wait_reason"] = "" + return waiting + def _start_simple(self, conn, item, printer): """Upload/Start im Worker — mehrere freie Drucker parallel.""" host = printer["host"] diff --git a/grotti/schema.sql b/grotti/schema.sql index c555054..6a33cf2 100644 --- a/grotti/schema.sql +++ b/grotti/schema.sql @@ -7,9 +7,12 @@ CREATE TABLE IF NOT EXISTS projects ( notes text NOT NULL DEFAULT '', status text NOT NULL DEFAULT 'idee', quote jsonb NOT NULL DEFAULT '{}'::jsonb, + folder text NOT NULL DEFAULT '', created_at timestamptz NOT NULL DEFAULT now() ); +ALTER TABLE projects ADD COLUMN IF NOT EXISTS folder text NOT NULL DEFAULT ''; + CREATE TABLE IF NOT EXISTS spools ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text NOT NULL, @@ -123,6 +126,7 @@ ALTER TABLE spools ADD COLUMN IF NOT EXISTS material_id bigint REFERENCES materi ALTER TABLE spools ADD COLUMN IF NOT EXISTS chargen_nr text; ALTER TABLE spools ADD COLUMN IF NOT EXISTS hersteller_charge text NOT NULL DEFAULT ''; ALTER TABLE spools ADD COLUMN IF NOT EXISTS code text; +ALTER TABLE spools ADD COLUMN IF NOT EXISTS archived_at timestamptz; CREATE UNIQUE INDEX IF NOT EXISTS spools_chargen_nr ON spools (chargen_nr) WHERE chargen_nr IS NOT NULL AND chargen_nr <> ''; CREATE UNIQUE INDEX IF NOT EXISTS spools_code ON spools (code) WHERE code IS NOT NULL AND code <> ''; diff --git a/grotti/store.py b/grotti/store.py index ec411fa..789d20a 100644 --- a/grotti/store.py +++ b/grotti/store.py @@ -20,6 +20,7 @@ def project_out(row, files=None, file_count=None): "notes": row["notes"], "status": row["status"], "quote": row["quote"] or {}, + "folder": (row.get("folder") or "").strip(), "created_at": iso(row["created_at"]), } if files is not None: @@ -76,6 +77,7 @@ def spool_out(row): colors = row.get("colors") or [] if isinstance(colors, str): colors = json.loads(colors) + archived = row.get("archived_at") return { "id": row["id"], "name": row["name"], @@ -93,6 +95,8 @@ def spool_out(row): "chargen_nr": row.get("chargen_nr") or "", "hersteller_charge": row.get("hersteller_charge") or "", "code": row.get("code") or "", + "archived_at": archived.isoformat() if archived and hasattr(archived, "isoformat") else (archived or None), + "archived": bool(archived), } @@ -209,14 +213,14 @@ def get_project(conn, project_id): return project_out(row, files=[file_out(item) for item in files]) -def create_project(conn, name, notes, status): +def create_project(conn, name, notes, status, folder=""): row = conn.execute( """ - INSERT INTO projects (name, notes, status) - VALUES (%s, %s, %s) + INSERT INTO projects (name, notes, status, folder) + VALUES (%s, %s, %s, %s) RETURNING * """, - (name, notes, status), + (name, notes, status, (folder or "").strip()), ).fetchone() return project_out(row, files=[], file_count=0) @@ -224,10 +228,10 @@ def create_project(conn, name, notes, status): def update_project(conn, project_id, fields): sets = [] values = [] - for key in ("name", "notes", "status"): + for key in ("name", "notes", "status", "folder"): if key in fields: sets.append("%s = %%s" % key) - values.append(fields[key]) + values.append(fields[key] if key != "folder" else (fields[key] or "").strip()) if "quote" in fields: sets.append("quote = %s") values.append(Json(fields["quote"])) @@ -243,6 +247,24 @@ def update_project(conn, project_id, fields): return get_project(conn, project_id) +def list_project_folders(conn): + """id + folder für Sync (nur Projekte mit Ordner).""" + return conn.execute( + """ + SELECT id, folder FROM projects + WHERE coalesce(folder, '') <> '' + ORDER BY length(folder) DESC, folder + """ + ).fetchall() + + +def set_file_project(conn, file_id, project_id): + conn.execute( + "UPDATE files SET project_id = %s WHERE id = %s", + (project_id, file_id), + ) + + def delete_project(conn, project_id): row = conn.execute("DELETE FROM projects WHERE id = %s RETURNING id", (project_id,)).fetchone() return bool(row) @@ -341,7 +363,7 @@ def update_file(conn, file_id, fields, watch_dir=None): return file_out(row, watch_dir=watch_dir) if row else None -def update_file_location(conn, file_id, stored_path, name=None, size_bytes=None, mtime=None): +def update_file_location(conn, file_id, stored_path, name=None, size_bytes=None, mtime=None, source=None): """Pfad nach Verschieben auf der Platte — ID und Zuordnungen bleiben.""" sets = ["stored_path = %s"] values = [stored_path] @@ -354,6 +376,9 @@ def update_file_location(conn, file_id, stored_path, name=None, size_bytes=None, if mtime is not None: sets.append("mtime = %s") values.append(mtime) + if source is not None: + sets.append("source = %s") + values.append(source) values.append(file_id) return conn.execute( "UPDATE files SET %s WHERE id = %%s RETURNING *" % ", ".join(sets), @@ -440,6 +465,14 @@ def update_spool(conn, spool_id, fields): if key in fields: sets.append("%s = %%s" % key) values.append(Json(fields[key]) if key == "colors" else fields[key]) + if "archived_at" in fields: + if fields["archived_at"] is None or fields["archived_at"] is False: + sets.append("archived_at = NULL") + elif fields["archived_at"] is True or fields["archived_at"] == "now": + sets.append("archived_at = now()") + else: + sets.append("archived_at = %s") + values.append(fields["archived_at"]) if not sets: row = get_spool_row(conn, spool_id) return spool_out(row) if row else None @@ -451,6 +484,23 @@ def update_spool(conn, spool_id, fields): return spool_out(row) if row else None +def mark_spool_empty(conn, spool_id): + """Rest auf 0, vom Drucker ins Regal, archivieren.""" + row = conn.execute( + """ + UPDATE spools + SET remaining_g = 0, + printer_host = '', + ace_slot = NULL, + archived_at = COALESCE(archived_at, now()) + WHERE id = %s + RETURNING * + """, + (spool_id,), + ).fetchone() + return spool_out(row) if row else None + + def material_out(row): s1 = row["volumetric_s1"] return { @@ -1278,6 +1328,27 @@ def find_live_run(conn, host, filename=""): ).fetchone() +def find_active_run(conn, host, filename=""): + """Offener Lauf am Host: Live bevorzugt, sonst offener History-Job (Pause/Resume).""" + return find_live_run(conn, host, filename) or find_open_history_run(conn, host, filename) + + +def update_send_spool(conn, host, filename, spool_id): + """Spule am letzten Send dieser Datei korrigieren (Rollenwechsel während Druck).""" + host = (host or "").strip() + if not host or not spool_id: + return None + sent = latest_send(conn, host, filename or "") + if not sent: + return None + if sent.get("spool_id") == spool_id: + return sent + return conn.execute( + "UPDATE sends SET spool_id = %s WHERE id = %s RETURNING *", + (spool_id, sent["id"]), + ).fetchone() + + def ensure_live_run(conn, host, printer_name, filename, spool_id=None): """Aktiven Lauf: History-Job bevorzugen, sonst Live — nie parallel beide offen.""" hist = find_open_history_run(conn, host, filename) @@ -1286,7 +1357,7 @@ def ensure_live_run(conn, host, printer_name, filename, spool_id=None): if live: adopt_live_run(conn, host, filename or hist.get("filename") or "", hist["id"]) hist = get_run_row(conn, hist["id"]) or hist - if spool_id and not hist.get("spool_id"): + if spool_id and hist.get("spool_id") != spool_id: conn.execute("UPDATE runs SET spool_id = %s WHERE id = %s", (spool_id, hist["id"])) hist = get_run_row(conn, hist["id"]) or hist return hist @@ -1298,6 +1369,9 @@ def ensure_live_run(conn, host, printer_name, filename, spool_id=None): (filename, row["id"]), ) row = get_run_row(conn, row["id"]) + if spool_id and row and row.get("spool_id") != spool_id: + conn.execute("UPDATE runs SET spool_id = %s WHERE id = %s", (spool_id, row["id"])) + row = get_run_row(conn, row["id"]) return row job_id = "live:%s:%s" % (host, datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")) return upsert_run(conn, { @@ -1349,6 +1423,30 @@ def abandon_live_runs(conn, host, keep_id=None): return len(rows) +def complete_open_run(conn, run_id, state="completed"): + """Offenen Lauf als beendet markieren (Abräumen / Druckende ohne History).""" + state = (state or "completed").strip() or "completed" + if state not in ("completed", "cancelled", "error", "interrupted"): + state = "completed" + return conn.execute( + """ + UPDATE runs + SET state = %s, + ended_at = COALESCE(ended_at, now()), + duration_s = CASE + WHEN COALESCE(duration_s, 0) > 0 THEN duration_s + WHEN started_at IS NOT NULL + THEN EXTRACT(EPOCH FROM (now() - started_at)) + ELSE COALESCE(duration_s, 0) + END + WHERE id = %s + AND state IN ('in_progress', 'printing', 'paused') + RETURNING * + """, + (state, run_id), + ).fetchone() + + def adopt_live_run(conn, host, filename, real_run_id): """Proben/Segmente vom Live-Lauf auf den History-Lauf umhängen.""" live = find_live_run(conn, host, filename) @@ -1772,7 +1870,7 @@ def exclusive_until(until): return until -def statistics(conn, since=None, until=None, host="", material="", brand="", project_id=None): +def statistics(conn, since=None, until=None, host="", material="", brand="", color="", project_id=None): where = ["r.state IN ('completed', 'cancelled', 'error', 'interrupted')"] values = [] until_bound = exclusive_until(until) @@ -1790,7 +1888,7 @@ def statistics(conn, since=None, until=None, host="", material="", brand="", pro values.append(project_id) spool_join = "" - if material or brand: + if material or brand or color: spool_join = ( "JOIN run_spools rs0 ON rs0.run_id = r.id " "JOIN spools s0 ON s0.id = rs0.spool_id" @@ -1801,6 +1899,9 @@ def statistics(conn, since=None, until=None, host="", material="", brand="", pro if brand: where.append("s0.brand ILIKE %s") values.append(brand) + if color: + where.append("s0.color ILIKE %s") + values.append(color) clause = " AND ".join(where) overview = conn.execute( @@ -1874,11 +1975,14 @@ def statistics(conn, since=None, until=None, host="", material="", brand="", pro if brand: mat_where.append("s.brand ILIKE %s") mat_values.append(brand) + if color: + mat_where.append("s.color ILIKE %s") + mat_values.append(color) mat_clause = " AND ".join(mat_where) by_material = conn.execute( """ - SELECT s.material_type, s.brand, + SELECT s.material_type, s.brand, s.color, COALESCE(SUM(rs.weight_g), 0) AS weight_g, COALESCE(SUM(rs.cost), 0) AS cost, COUNT(DISTINCT rs.run_id) AS jobs @@ -1886,7 +1990,7 @@ def statistics(conn, since=None, until=None, host="", material="", brand="", pro JOIN runs r ON r.id = rs.run_id JOIN spools s ON s.id = rs.spool_id WHERE %s - GROUP BY s.material_type, s.brand + GROUP BY s.material_type, s.brand, s.color ORDER BY weight_g DESC """ % mat_clause, mat_values, @@ -1920,6 +2024,17 @@ def statistics(conn, since=None, until=None, host="", material="", brand="", pro ov = overview or {} # Bei Spulen-Join können SUM(DISTINCT …) ungenau sein — Überblick ohne Join neu if spool_join: + spool_filters = [] + spool_vals = list(run_values) + if material: + spool_filters.append("AND s.material_type ILIKE %s") + spool_vals.append(material) + if brand: + spool_filters.append("AND s.brand ILIKE %s") + spool_vals.append(brand) + if color: + spool_filters.append("AND s.color ILIKE %s") + spool_vals.append(color) overview2 = conn.execute( """ SELECT @@ -1940,15 +2055,10 @@ def statistics(conn, since=None, until=None, host="", material="", brand="", pro JOIN spools s ON s.id = rs.spool_id WHERE rs.run_id = r.id %s - %s ) ) - """ % ( - run_clause, - ("AND s.material_type ILIKE %%s" if material else ""), - ("AND s.brand ILIKE %%s" if brand else ""), - ), - run_values + ([material] if material else []) + ([brand] if brand else []), + """ % (run_clause, " ".join(spool_filters)), + spool_vals, ).fetchone() ov = overview2 or ov @@ -1986,6 +2096,7 @@ def statistics(conn, since=None, until=None, host="", material="", brand="", pro { "material_type": row["material_type"] or "", "brand": row["brand"] or "", + "color": row.get("color") or "", "weight_g": float(row["weight_g"] or 0), "cost": float(row["cost"] or 0), "jobs": int(row["jobs"] or 0), @@ -2579,14 +2690,50 @@ def update_print_queue(conn, item_id, fields): return print_queue_out(fresh, fresh.get("filename")) if fresh else None +def _profile_matches(need_profile, spool_name): + """Sorte: exakter Katalogname (Meta ≠ Matte ≠ PLA). Zusatz nur hinter dem Namen.""" + a = (need_profile or "").strip().lower() + b = (spool_name or "").strip().lower() + if not a: + return True + if not b: + return False + if a == b: + return True + # Spulen-Anzeigename: "Sunlu PLA Meta schwarz" — nicht "Sunlu PLA" ⊂ "Sunlu PLA Matte" + if b.startswith(a) and len(b) > len(a) and not b[len(a)].isalnum(): + rest = b[len(a):].lstrip(" ·-–_|/") + # Nächstes Wort darf keine bekannte PLA-/PETG-Variante sein + first = (rest.split() or [""])[0] + if first in {"meta", "matte", "matt", "silk", "plus", "+", "hf", "cf"}: + return False + return True + return False + + +def _need_label(need): + profile = (need.get("profile") or "").strip() + if profile: + return profile + kind = (need.get("material_type") or "").strip() + color = (need.get("color") or "").strip() + return (kind + ((" " + color) if color else "")).strip() or "Material" + + def _spool_material_set(conn, host): rows = conn.execute( """ - SELECT lower(coalesce(material_type, '')) AS mt, - lower(coalesce(color, '')) AS color, - coalesce(remaining_g, 0) AS remaining_g - FROM spools - WHERE printer_host = %s + SELECT lower(coalesce(s.material_type, '')) AS mt, + lower(coalesce(s.color, '')) AS color, + coalesce(s.remaining_g, 0) AS remaining_g, + lower(coalesce(m.name, s.name, '')) AS material_name, + s.ace_slot AS ace_slot, + s.id AS spool_id + FROM spools s + LEFT JOIN materials m ON m.id = s.material_id + WHERE s.printer_host = %s + AND s.archived_at IS NULL + AND coalesce(s.remaining_g, 0) > 0.5 """, (host or "",), ).fetchall() @@ -2595,6 +2742,9 @@ def _spool_material_set(conn, host): "material_type": (r["mt"] or "").strip(), "color": (r["color"] or "").strip(), "remaining_g": float(r["remaining_g"] or 0), + "material_name": (r["material_name"] or "").strip(), + "ace_slot": r["ace_slot"], + "spool_id": r["spool_id"], } for r in rows ] @@ -2617,48 +2767,144 @@ def _file_needs(conn, file_id): except Exception: filaments = [] types = [] - for item in filaments if isinstance(filaments, list) else []: + for index, item in enumerate(filaments if isinstance(filaments, list) else []): if isinstance(item, dict): t = (item.get("type") or item.get("material_type") or "").strip().lower() c = (item.get("color") or "").strip().lower() - if t: - types.append({"material_type": t, "color": c}) + profile = (item.get("profile") or "").strip() + try: + paint = int(item.get("paint_index")) + except (TypeError, ValueError): + paint = index + if t or profile: + types.append({ + "material_type": t, + "color": c, + "profile": profile, + "paint_index": paint, + }) elif isinstance(item, str) and item.strip(): - types.append({"material_type": item.strip().lower(), "color": ""}) + types.append({ + "material_type": item.strip().lower(), + "color": "", + "profile": "", + "paint_index": index, + }) return types, float(row.get("weight_g") or 0) +def _match_spools_for_need(need, have, used_ids=None): + used = used_ids or set() + candidates = [h for h in have if h.get("spool_id") not in used] + profile = (need.get("profile") or "").strip() + if profile: + by_profile = [h for h in candidates if _profile_matches(profile, h.get("material_name"))] + if not by_profile: + return [] + colored = [ + h for h in by_profile + if not need.get("color") or h["color"] == need["color"] or not h["color"] + ] + return colored or by_profile + kind = need.get("material_type") or "" + return [ + h for h in candidates + if (not kind or h["material_type"] == kind) + and (not need.get("color") or h["color"] == need["color"] or not h["color"]) + ] + + def material_fit_score(conn, host, file_id): - """Höher = besser. -1 nur bei zu wenig Rest. Falsche Sorte startet trotzdem.""" + """Höher = besser. -1 = warten (falsche Sorte mit Profil, oder zu wenig Rest).""" need, weight = _file_needs(conn, file_id) have = _spool_material_set(conn, host) if not need: return 0, "" if not have: + # Mit Profil: ohne Spule warten. Ohne Profil: soft (wie bisher). + if any((n.get("profile") or "").strip() for n in need): + return -1, "braucht %s" % _need_label(need[0]) return 0, "" + + has_profiles = any((n.get("profile") or "").strip() for n in need) + if has_profiles: + score = 20 + used = set() + for n in need: + matched = _match_spools_for_need(n, have, used) + if not matched: + return -1, "braucht %s" % _need_label(n) + best = max(matched, key=lambda h: h["remaining_g"]) + used.add(best.get("spool_id")) + if n.get("color") and any(h["color"] == n["color"] for h in matched): + score += 5 + if weight and best["remaining_g"] + 1e-6 >= weight: + score += 5 + elif weight and all(h["remaining_g"] + 1e-6 < weight for h in matched): + return -1, "braucht %s (%s g fehlen)" % ( + _need_label(n), + int(max(0, weight - max(h["remaining_g"] for h in matched))), + ) + return score, "" + have_types = {h["material_type"] for h in have if h["material_type"]} need_types = {n["material_type"] for n in need if n["material_type"]} - # Falsche Sorte hält den Job nicht fest. Dateien warnt, der Start geht trotzdem. + # Ohne Profil: falscher Typ hält den Job nicht fest (Sofort-Start warnt in der UI). score = 0 if (need_types and have_types and need_types.isdisjoint(have_types)) else 10 - # Farbe + Rest for n in need: - matched = [ - h for h in have - if h["material_type"] == n["material_type"] - and (not n["color"] or h["color"] == n["color"] or not h["color"]) - ] + matched = _match_spools_for_need(n, have) if matched: score += 5 if weight and any(h["remaining_g"] + 1e-6 >= weight for h in matched): score += 5 elif weight and all(h["remaining_g"] + 1e-6 < weight for h in matched): return -1, "braucht %s (%s g fehlen)" % ( - n["material_type"] + ((" " + n["color"]) if n["color"] else ""), + _need_label(n), int(max(0, weight - max(h["remaining_g"] for h in matched))), ) return score, "" +def auto_ace_map(conn, host, file_id): + """ACE-Map aus eingelegten Spulen: Sorte (Profil) zuerst, Farbe soft. + + Rückgabe: (mapping, reason). reason gesetzt → nicht starten, warten. + Ohne ACE-Spulen am Host: leere Map und kein Grund (externe Rolle). + """ + need, _weight = _file_needs(conn, file_id) + have = [h for h in _spool_material_set(conn, host) if h.get("ace_slot")] + if not need: + return [], "" + if not have: + return [], "" + mapping = [] + used = set() + for n in need: + matched = _match_spools_for_need(n, have, used) + # Nur ACE-Plätze + matched = [h for h in matched if h.get("ace_slot")] + if not matched: + return [], "braucht %s auf ACE" % _need_label(n) + # Farbe bevorzugen, dann Rest + matched.sort( + key=lambda h: ( + 0 if (n.get("color") and h["color"] == n["color"]) else 1, + -h["remaining_g"], + ) + ) + chosen = matched[0] + used.add(chosen.get("spool_id")) + try: + paint = int(n.get("paint_index")) + except (TypeError, ValueError): + paint = len(mapping) + ams = int(chosen["ace_slot"]) - 1 + if ams < 0 or ams > 7: + return [], "ACE-Platz von %s ungültig" % _need_label(n) + mapping.append({"paint_index": paint, "ams_index": ams}) + return mapping, "" + + def next_waiting_print(conn, host=None, family=None): """Nächster wartender Druck: Termin (due_at) vor Priorität, Material-Match bevorzugt.""" if host: diff --git a/grotti/usage.py b/grotti/usage.py index 4eaea67..7a22750 100644 --- a/grotti/usage.py +++ b/grotti/usage.py @@ -53,15 +53,23 @@ def planned_shares(metadata, ace_map, target_count): weights = weight_list(metadata) mapping = ace_map if isinstance(ace_map, list) else [] if weights and mapping and len(weights) >= len(mapping) and target_count == len(mapping): - shares = [] + paints = [] for item in mapping: - paint = item.get("paint_index") try: - index = int(paint) + paints.append(int(item.get("paint_index"))) except (TypeError, ValueError): - index = len(shares) - if 0 <= index < len(weights): - shares.append(weights[index]) + paints.append(-1) + # Nur bei dichter 0…n−1-Map paint_index = Gewichtsindex; sonst Map-Reihenfolge + # (Bambu/Anycubic oft T1+T10 bei zwei kompakten Gewichten). + dense = sorted(paints) == list(range(len(paints))) and all( + 0 <= paint < len(weights) for paint in paints + ) + shares = [] + for order, paint in enumerate(paints): + if dense: + shares.append(weights[paint]) + elif order < len(weights): + shares.append(weights[order]) else: shares.append(0.0) if sum(shares) > 0: diff --git a/grotti/version.py b/grotti/version.py index 3697541..d76592b 100644 --- a/grotti/version.py +++ b/grotti/version.py @@ -1,3 +1,3 @@ """Version von Grotti (SemVer). Bei jeder Änderung CHANGELOG.md mitführen.""" -__version__ = "3.2.0" +__version__ = "3.3.0" diff --git a/grotti/watcher.py b/grotti/watcher.py index 3d88b25..ea01a00 100644 --- a/grotti/watcher.py +++ b/grotti/watcher.py @@ -6,7 +6,7 @@ import threading import time from datetime import datetime, timezone -from grotti import moonraker, notify, store, usage +from grotti import jobs, moonraker, notify, store, usage from grotti.events_bus import publish as publish_live from grotti.db import connect @@ -170,6 +170,17 @@ class StateWatcher: jobs.sync_history(conn, self.cfg) except Exception as exc: print("Watcher Abgleich %s: %s" % (host, exc), flush=True) + # History oft ohne Endzeit (interrupted) — Live trotzdem schließen. + dangling = store.find_live_run(conn, host) + if dangling: + closed = store.complete_open_run(conn, dangling["id"], state="completed") + if closed: + try: + from grotti import jobs + + jobs.apply_deduction(conn, closed, self.cfg["energy"]) + except Exception as exc: + print("Watcher Abzug %s: %s" % (host, exc), flush=True) elif state in ("cancelled", "standby"): # Live-Lauf schließen: normaler Abbruch, oder nach Offline/Neustart # (prev wird „offline“, der Übergang paused→standby geht sonst verloren). @@ -286,6 +297,8 @@ class StateWatcher: return None sent = store.latest_send(conn, host, filename) if filename else None spool_id = (sent or {}).get("spool_id") + # Send kann nach Rollenwechsel veraltet sein — aktuelle Rolle am Host bevorzugt + spool_id = jobs.resolve_tracking_spool(conn, host, spool_id, ace_map=(sent or {}).get("ace_map")) run = store.ensure_live_run(conn, host, name, filename, spool_id=spool_id) objects = self._merge_objects(run.get("objects"), live.get("exclude")) planned = usage.as_float(live.get("weight")) diff --git a/scripts/orca_senden.py b/scripts/orca_senden.py index 17ff28f..69c6c75 100644 --- a/scripts/orca_senden.py +++ b/scripts/orca_senden.py @@ -13,8 +13,11 @@ from __future__ import annotations import json import mimetypes import os +import re import shutil +import subprocess import sys +import time import uuid import webbrowser from pathlib import Path @@ -25,7 +28,19 @@ from urllib.request import Request, urlopen REPO_ROOT = Path(__file__).resolve().parent.parent CONFIG_PATH = REPO_ROOT / "data" / "orca-senden.json" DEBUG_LOG = REPO_ROOT / "data" / "orca-senden-last.log" +BROWSER_LOCK = REPO_ROOT / "data" / "orca-senden-browser.lock" +FOCUS_PATH = REPO_ROOT / "data" / "orca-senden-focus.json" +APP_PROFILE = REPO_ROOT / "data" / "grotti-app-profile" ALLOWED_SUFFIXES = (".gcode", ".gcode.3mf", ".3mf", ".gcode.pp", ".pp") +BROWSER_DEBOUNCE_S = 25.0 +# Anycubic: MMDD-HHMM-Name.gcode — Zeitstempel raus, sonst jedes Export ein neues Fenster +_TIME_PREFIX_RE = re.compile(r"^\d{4}-\d{4}-") +_CHROMIUM_CANDIDATES = ( + Path(r"C:\Program Files\Google\Chrome\Application\chrome.exe"), + Path(r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"), + Path(os.path.expandvars(r"%LOCALAPPDATA%\Microsoft\Edge\Application\msedge.exe")), + Path(os.path.expandvars(r"%LOCALAPPDATA%\Google\Chrome\Application\chrome.exe")), +) def show_error(message: str) -> None: @@ -76,7 +91,10 @@ def load_config(path: Path = CONFIG_PATH) -> dict[str, str]: raise ValueError("Config: url fehlt (z. B. http://127.0.0.1:8770).") if not token: raise ValueError("Config: token fehlt (grt_… aus Einstellungen).") - return {"url": url, "token": token} + browser = str(data.get("browser") or "app").strip().lower() or "app" + if browser not in ("app", "tab"): + browser = "app" + return {"url": url, "token": token, "browser": browser} def is_gcode_path(path: Path) -> bool: @@ -140,6 +158,83 @@ def send_page_url(base_url: str, file_id: int) -> str: return "%s/dateien?file=%s" % (base_url.rstrip("/"), int(file_id)) +def app_page_url(base_url: str) -> str: + """Stabile App-URL — Datei kommt über orca-fokus, kein neues Fenster pro Slice.""" + return "%s/dateien?orca=1" % base_url.rstrip("/") + + +def find_chromium() -> Path | None: + for path in _CHROMIUM_CANDIDATES: + try: + if path.is_file(): + return path + except OSError: + continue + return None + + +def app_already_running() -> bool: + """True wenn Chrome/Edge schon mit grotti-app-profile läuft (Profil-Sperre).""" + needle = "grotti-app-profile" + if sys.platform == "win32": + try: + creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0) + out = subprocess.check_output( + [ + "powershell", + "-NoProfile", + "-Command", + ( + "Get-CimInstance Win32_Process | " + "Where-Object { " + "($_.Name -match '^(chrome|msedge)\\.exe$') -and " + "($_.CommandLine -like '*%s*') " + "} | Select-Object -First 1 -ExpandProperty ProcessId" + ) + % needle, + ], + timeout=8, + stderr=subprocess.DEVNULL, + creationflags=creationflags, + ) + return bool((out or b"").strip()) + except (OSError, subprocess.SubprocessError): + pass + for name in ("SingletonLock", "SingletonSocket", "lockfile"): + path = APP_PROFILE / name + try: + if path.exists() or path.is_symlink(): + return True + except OSError: + continue + return False + + +def open_as_app(url: str) -> bool: + """Chrome/Edge --app=… → eigenes Fenster auf dem Desktop, kein Browser-Tab.""" + binary = find_chromium() + if binary is None: + return False + try: + APP_PROFILE.mkdir(parents=True, exist_ok=True) + subprocess.Popen( + [ + str(binary), + "--app=%s" % url, + "--user-data-dir=%s" % str(APP_PROFILE.resolve()), + "--no-first-run", + "--no-default-browser-check", + ], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + close_fds=True, + ) + return True + except OSError as exc: + write_debug(sys.argv, "app_open_fail=%s" % exc) + return False + + def _api_json( base_url: str, token: str, @@ -279,7 +374,79 @@ def upload_file( return payload -def open_browser(url: str) -> None: +def browser_key(name: str) -> str: + """Gleicher Slice trotz Zeitstempel / -2 / Leerzeichen → ein Browserfenster.""" + stem = Path(name or "").stem.lower() + stem = _TIME_PREFIX_RE.sub("", stem) + stem = re.sub(r"-\d+$", "", stem) + return re.sub(r"[^a-z0-9]+", "", stem) + + +def write_focus(file_id: int, url: str, key: str = "") -> None: + """Offene Dateien-Seite kann auf die neueste Orca-Datei umschalten.""" + try: + FOCUS_PATH.parent.mkdir(parents=True, exist_ok=True) + FOCUS_PATH.write_text( + json.dumps( + { + "file_id": int(file_id), + "url": url, + "key": key, + "ts": time.time(), + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + except (OSError, TypeError, ValueError): + pass + + +def open_browser( + url: str, + key: str = "", + file_id: int | None = None, + mode: str = "app", +) -> None: + """Anycubic/Orca starten das Skript oft 2× — nur ein Fenster pro Modell. + + mode=app (Standard): Chrome/Edge als App-Fenster auf dem Desktop. + mode=tab: klassischer Browser-Tab. + """ + token = (key or url).strip() or url + now = time.time() + if file_id is not None: + write_focus(int(file_id), url, key=token) + try: + BROWSER_LOCK.parent.mkdir(parents=True, exist_ok=True) + if BROWSER_LOCK.is_file(): + prev = json.loads(BROWSER_LOCK.read_text(encoding="utf-8")) + if ( + isinstance(prev, dict) + and prev.get("key") == token + and now - float(prev.get("ts") or 0) < BROWSER_DEBOUNCE_S + ): + write_debug( + sys.argv, + "browser_skip key=%s age=%.1fs focus=%s" + % (token, now - float(prev.get("ts") or 0), file_id), + ) + return + BROWSER_LOCK.write_text( + json.dumps({"key": token, "ts": now, "url": url, "mode": mode}, ensure_ascii=False), + encoding="utf-8", + ) + except (OSError, TypeError, ValueError, json.JSONDecodeError): + pass + if (mode or "app").strip().lower() != "tab": + # Zweites Chrome mit gleichem Profil = leeres Profil / erneutes Login. + # Offenes Fenster pollt orca-fokus und wechselt die Datei selbst. + if app_already_running(): + write_debug(sys.argv, "browser_reuse focus=%s" % file_id) + return + if open_as_app(url): + write_debug(sys.argv, "browser_app url=%s" % url) + return webbrowser.open(url) @@ -319,8 +486,21 @@ def main(argv: list[str] | None = None) -> int: % (path, upload_name_for(path), output_target() or ""), ) file_id = deliver(config["url"], config["token"], path) - write_debug(argv, "file_id=%s" % file_id) - open_browser(send_page_url(config["url"], file_id)) + name = upload_name_for(path) + write_debug(argv, "file_id=%s\nupload_as=%s" % (file_id, name)) + mode = str(config.get("browser") or "app").strip().lower() + # App: stabile URL; Tab: Deep-Link mit file= + open_url = ( + send_page_url(config["url"], file_id) + if mode == "tab" + else app_page_url(config["url"]) + ) + open_browser( + open_url, + key=browser_key(name), + file_id=file_id, + mode=mode, + ) except Exception as exc: write_debug(argv, "error=%s" % exc) show_error(str(exc)) diff --git a/tests/test_jobs_spool.py b/tests/test_jobs_spool.py index 32363bc..19ad6ea 100644 --- a/tests/test_jobs_spool.py +++ b/tests/test_jobs_spool.py @@ -28,6 +28,47 @@ class ChooseSpoolTests(unittest.TestCase): with patch("grotti.jobs.store.spools_for_printer", return_value=[old]): self.assertEqual(jobs.choose_spool(conn, "h", "PLA", None, ended_at=end)["id"], 1) + def test_prefers_host_spool_over_stale_hint(self): + """Rollenwechsel: Hint im Regal, neue Rolle am Drucker → Abzug auf die neue.""" + end = datetime(2026, 10, 18, 14, 58, tzinfo=timezone.utc) + hinted = { + "id": 4, + "material_type": "PLA", + "printer_host": "", + "created_at": end - timedelta(days=5), + } + current = { + "id": 14, + "material_type": "PLA", + "printer_host": "h", + "created_at": end - timedelta(minutes=10), + } + conn = MagicMock() + with patch("grotti.jobs.store.spools_for_printer", return_value=[current]): + with patch("grotti.jobs.store.get_spool_row", return_value=hinted): + self.assertEqual(jobs.choose_spool(conn, "h", "PLA", 4, ended_at=end)["id"], 14) + + def test_keeps_hint_when_no_host_spool(self): + end = datetime(2026, 10, 18, 14, 58, tzinfo=timezone.utc) + hinted = { + "id": 4, + "material_type": "PLA", + "printer_host": "", + "created_at": end - timedelta(days=5), + } + conn = MagicMock() + with patch("grotti.jobs.store.spools_for_printer", return_value=[]): + with patch("grotti.jobs.store.get_spool_row", return_value=hinted): + self.assertEqual(jobs.choose_spool(conn, "h", "PLA", 4, ended_at=end)["id"], 4) + + def test_resolve_tracking_spool_prefers_host(self): + conn = MagicMock() + with patch( + "grotti.jobs.resolve_spools_at_host", + return_value=[{"id": 14, "printer_host": "h"}], + ): + self.assertEqual(jobs.resolve_tracking_spool(conn, "h", 4), 14) + def test_spool_valid_for_end_naive(self): end = datetime(2026, 10, 1, 12, 0, tzinfo=timezone.utc) spool = {"created_at": datetime(2026, 9, 1, 12, 0)} diff --git a/tests/test_material_routing.py b/tests/test_material_routing.py new file mode 100644 index 0000000..81699c7 --- /dev/null +++ b/tests/test_material_routing.py @@ -0,0 +1,80 @@ +"""Sorten-Routing: Profil (Meta/Matte) und Auto-ACE-Map.""" + +import unittest + +from grotti import store + + +class ProfileMatchTests(unittest.TestCase): + def test_meta_not_matte(self): + self.assertTrue(store._profile_matches("Sunlu PLA Meta", "Sunlu PLA Meta")) + self.assertTrue(store._profile_matches("Sunlu PLA Meta", "Sunlu PLA Meta schwarz")) + self.assertFalse(store._profile_matches("Sunlu PLA Meta", "Sunlu PLA Matte")) + self.assertFalse(store._profile_matches("Sunlu PLA Matte", "Sunlu PLA")) + self.assertFalse(store._profile_matches("Sunlu PLA", "Sunlu PLA Matte")) + self.assertFalse(store._profile_matches("Sunlu PLA", "Sunlu PLA Meta")) + + def test_empty_profile_always_ok(self): + self.assertTrue(store._profile_matches("", "Sunlu PLA Meta")) + + +class MatchSpoolsTests(unittest.TestCase): + def setUp(self): + self.have = [ + { + "spool_id": 1, + "material_type": "pla", + "color": "#ff0000", + "remaining_g": 800, + "material_name": "sunlu pla meta", + "ace_slot": 1, + }, + { + "spool_id": 2, + "material_type": "pla", + "color": "#000000", + "remaining_g": 500, + "material_name": "sunlu pla matte", + "ace_slot": 2, + }, + ] + + def test_picks_meta_not_matte(self): + need = {"profile": "Sunlu PLA Meta", "material_type": "pla", "color": "", "paint_index": 0} + matched = store._match_spools_for_need(need, self.have) + self.assertEqual(len(matched), 1) + self.assertEqual(matched[0]["spool_id"], 1) + + def test_color_soft_among_profile(self): + need = { + "profile": "Sunlu PLA Meta", + "material_type": "pla", + "color": "#00ff00", + "paint_index": 0, + } + # Nur Meta vorhanden, Farbe anders → trotzdem Meta (Farbe soft) + matched = store._match_spools_for_need(need, self.have) + self.assertEqual(matched[0]["spool_id"], 1) + + def test_auto_ace_map_assigns_slots(self): + # Ohne DB: Logik über _match + manuell wie auto_ace_map + need = [ + {"profile": "Sunlu PLA Meta", "material_type": "pla", "color": "#ff0000", "paint_index": 1}, + {"profile": "Sunlu PLA Matte", "material_type": "pla", "color": "#000000", "paint_index": 10}, + ] + used = set() + mapping = [] + for n in need: + matched = store._match_spools_for_need(n, self.have, used) + self.assertTrue(matched) + chosen = matched[0] + used.add(chosen["spool_id"]) + mapping.append({"paint_index": n["paint_index"], "ams_index": int(chosen["ace_slot"]) - 1}) + self.assertEqual(mapping, [ + {"paint_index": 1, "ams_index": 0}, + {"paint_index": 10, "ams_index": 1}, + ]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_orca_senden.py b/tests/test_orca_senden.py index 365b238..a178cd4 100644 --- a/tests/test_orca_senden.py +++ b/tests/test_orca_senden.py @@ -23,6 +23,90 @@ class OrcaSendenHelpersTest(unittest.TestCase): "http://127.0.0.1:8770/dateien?file=42", ) + def test_app_page_url(self): + self.assertEqual( + orca.app_page_url("http://127.0.0.1:8770/"), + "http://127.0.0.1:8770/dateien?orca=1", + ) + + def test_browser_key_collapses_duplicates(self): + self.assertEqual( + orca.browser_key("1018-1648-Round Clip PLA + PETG(01)_PLA_0.2_30m34s.gcode"), + orca.browser_key("1018-1648-Round_Clip_PLA_+_PETG(01)_PLA_0.2_30m34s-2.gcode"), + ) + + def test_browser_key_strips_time_prefix(self): + self.assertEqual( + orca.browser_key("1018-1706-Round Clip PLA + PETG(01)_PLA_0.2_30m34s.gcode"), + orca.browser_key("1018-1709-Round_Clip_PLA_+_PETG(01)_PLA_0.2_30m34s.gcode"), + ) + self.assertEqual( + orca.browser_key("1018-1709-Round Clip PLA + PETG(01)_PLA_0.2_30m34s.gcode"), + "roundclipplapetg01pla0230m34s", + ) + + def test_open_browser_debounces(self): + with tempfile.TemporaryDirectory() as tmp: + lock = Path(tmp) / "browser.lock" + focus = Path(tmp) / "focus.json" + with patch.object(orca, "BROWSER_LOCK", lock): + with patch.object(orca, "FOCUS_PATH", focus): + with patch.object(orca, "open_as_app", return_value=False): + with patch.object(orca, "webbrowser") as browser: + orca.open_browser( + "http://127.0.0.1:8770/dateien?file=1", + key="roundclip", + file_id=1, + mode="tab", + ) + orca.open_browser( + "http://127.0.0.1:8770/dateien?file=2", + key="roundclip", + file_id=2, + mode="tab", + ) + self.assertEqual(browser.open.call_count, 1) + data = json.loads(focus.read_text(encoding="utf-8")) + self.assertEqual(data["file_id"], 2) + + def test_open_browser_app_mode(self): + with tempfile.TemporaryDirectory() as tmp: + lock = Path(tmp) / "browser.lock" + focus = Path(tmp) / "focus.json" + with patch.object(orca, "BROWSER_LOCK", lock): + with patch.object(orca, "FOCUS_PATH", focus): + with patch.object(orca, "app_already_running", return_value=False): + with patch.object(orca, "open_as_app", return_value=True) as app: + with patch.object(orca, "webbrowser") as browser: + orca.open_browser( + "http://127.0.0.1:8770/dateien?orca=1", + key="roundclip", + file_id=9, + mode="app", + ) + app.assert_called_once() + browser.open.assert_not_called() + + def test_open_browser_reuses_running_app(self): + with tempfile.TemporaryDirectory() as tmp: + lock = Path(tmp) / "browser.lock" + focus = Path(tmp) / "focus.json" + with patch.object(orca, "BROWSER_LOCK", lock): + with patch.object(orca, "FOCUS_PATH", focus): + with patch.object(orca, "app_already_running", return_value=True): + with patch.object(orca, "open_as_app", return_value=True) as app: + with patch.object(orca, "webbrowser") as browser: + orca.open_browser( + "http://127.0.0.1:8770/dateien?orca=1", + key="andere-datei", + file_id=11, + mode="app", + ) + app.assert_not_called() + browser.open.assert_not_called() + data = json.loads(focus.read_text(encoding="utf-8")) + self.assertEqual(data["file_id"], 11) + def test_find_gcode_arg_last_matching(self): with tempfile.TemporaryDirectory() as tmp: folder = Path(tmp) diff --git a/tests/test_paint_index.py b/tests/test_paint_index.py new file mode 100644 index 0000000..2eb410b --- /dev/null +++ b/tests/test_paint_index.py @@ -0,0 +1,171 @@ +"""Sparse paint_index / Bambu-T-Nummern für ACE-Map.""" + +import json +import unittest + +from grotti import acm, parse, usage + + +PAINT_SPARSE = json.dumps( + [ + {"material_type": "PLA", "paint_color": [255, 128, 0], "paint_index": 1}, + {"material_type": "PLA", "paint_color": [0, 0, 0], "paint_index": 10}, + ] +) + + +class PaintIndexTests(unittest.TestCase): + def test_filaments_keep_slicer_paint_index(self): + rows = parse.filaments_from_paint_info(PAINT_SPARSE) + self.assertEqual(len(rows), 2) + self.assertEqual(rows[0]["paint_index"], 1) + self.assertEqual(rows[0]["type"], "PLA") + self.assertEqual(rows[0]["color"], "#ff8000") + self.assertEqual(rows[1]["paint_index"], 10) + self.assertEqual(rows[1]["color"], "#000000") + + def test_build_acm_uses_paint_index_not_array_slot(self): + filaments = parse.filaments_from_paint_info(PAINT_SPARSE) + mapping = [ + {"paint_index": 1, "ams_index": 3}, + {"paint_index": 10, "ams_index": 2}, + ] + slots = [ + {"index": 2, "type": "PLA", "color": "#f4f4f4"}, + {"index": 3, "type": "PLA", "color": "#c23b3b"}, + ] + payload = acm.build_acm(filaments, mapping, slots) + paints = [item["paint_index"] for item in payload["ams_box_mapping"]] + self.assertEqual(paints, [1, 10]) + by_paint = {item["paint_index"]: item for item in payload["ams_box_mapping"]} + self.assertEqual(by_paint[1]["ams_index"], 3) + self.assertEqual(by_paint[10]["ams_index"], 2) + self.assertEqual(by_paint[1]["paint_color"], [255, 128, 0]) + self.assertEqual(by_paint[10]["paint_color"], [0, 0, 0]) + + def test_densify_mapping_and_rewrite_avoids_invalid_filament_id(self): + mapping = [ + {"paint_index": 1, "ams_index": 3}, + {"paint_index": 10, "ams_index": 2}, + ] + remap, dense = acm.densify_mapping(mapping) + self.assertEqual(remap, {1: 0, 10: 1}) + self.assertEqual( + dense, + [ + {"paint_index": 0, "ams_index": 3}, + {"paint_index": 1, "ams_index": 2}, + ], + ) + gcode = b"; hdr\nT10\nG1 X0\nT1 ; change\n; referenced_tools = 1,10\n" + out = acm.rewrite_gcode_tools(gcode, remap) + self.assertIn(b"T1\n", out) + self.assertIn(b"T0 ; change", out) + self.assertNotIn(b"T10", out) + self.assertIn(b"; referenced_tools = 0,1", out) + filaments = acm.remap_filaments( + parse.filaments_from_paint_info(PAINT_SPARSE), remap + ) + slots = [ + {"index": 2, "type": "PLA", "color": "#f4f4f4"}, + {"index": 3, "type": "PLA", "color": "#c23b3b"}, + ] + payload = acm.build_acm(filaments, dense, slots) + paints = [item["paint_index"] for item in payload["ams_box_mapping"]] + self.assertEqual(paints, [0, 1]) + self.assertEqual(payload["ams_box_mapping"][0]["paint_color"], [255, 128, 0]) + + def test_densify_identity_when_already_dense(self): + mapping = [{"paint_index": 0, "ams_index": 1}, {"paint_index": 1, "ams_index": 2}] + remap, dense = acm.densify_mapping(mapping) + self.assertEqual(remap, {}) + self.assertEqual(dense, mapping) + + def test_align_rewrites_when_ui_map_is_already_dense(self): + """Regression 10011700: UI schickt 0/1, G-code hat noch T1/T10.""" + gcode = b"; hdr\nT10\nG1 X0\nT1 ; change\n; referenced_tools = 1,10\n" + mapping = [ + {"paint_index": 0, "ams_index": 3}, + {"paint_index": 1, "ams_index": 2}, + ] + filaments = [ + {"type": "PLA", "color": "#ff8000", "paint_index": 0}, + {"type": "PLA", "color": "#000000", "paint_index": 1}, + ] + out, dense, fils, remap = acm.align_tools_for_ace(gcode, mapping, filaments) + self.assertEqual(remap, {1: 0, 10: 1}) + self.assertEqual( + dense, + [ + {"paint_index": 0, "ams_index": 3}, + {"paint_index": 1, "ams_index": 2}, + ], + ) + self.assertIn(b"T1\n", out) + self.assertIn(b"T0 ; change", out) + self.assertNotIn(b"T10", out) + self.assertIn(b"; referenced_tools = 0,1", out) + self.assertEqual([row["paint_index"] for row in fils], [0, 1]) + self.assertEqual(fils[0]["color"], "#ff8000") + self.assertEqual(fils[1]["color"], "#000000") + + def test_align_with_sparse_ui_map(self): + gcode = b"T10\nT1\n" + mapping = [ + {"paint_index": 1, "ams_index": 3}, + {"paint_index": 10, "ams_index": 2}, + ] + out, dense, fils, remap = acm.align_tools_for_ace( + gcode, mapping, parse.filaments_from_paint_info(PAINT_SPARSE) + ) + self.assertEqual(remap, {1: 0, 10: 1}) + self.assertEqual(dense[0]["ams_index"], 3) + self.assertEqual(dense[1]["ams_index"], 2) + self.assertNotIn(b"T10", out) + self.assertEqual([row["paint_index"] for row in fils], [0, 1]) + + def test_tools_used_finds_t10(self): + gcode = b"; header\nT10\nG1 X0\nT1 ; change\n" + self.assertEqual(parse.tools_used_in_gcode(gcode), {1, 10}) + + def test_planned_shares_sparse_paint_falls_back_to_order(self): + meta = {"filament_weight_total": [12.0, 8.0]} + mapping = [ + {"paint_index": 1, "ams_index": 0}, + {"paint_index": 10, "ams_index": 1}, + ] + self.assertEqual(usage.planned_shares(meta, mapping, 2), [12.0, 8.0]) + + def test_profile_name_strips_machine(self): + self.assertEqual(parse.profile_name('Sunlu PLA Meta @Kobra 3'), "Sunlu PLA Meta") + self.assertEqual(parse.profile_name('"Sunlu PLA Matte @Kobra S1"'), "Sunlu PLA Matte") + + def test_filaments_of_attaches_settings_id_profiles(self): + # Slot 1 = PLA, Slot 10 = Matte — wie sparse paint_info + profiles = ";".join( + ['"Sunlu PLA @Kobra 3"' if i not in (1, 10) else ( + '"Sunlu PLA @Kobra 3"' if i == 1 else '"Sunlu PLA Matte @Kobra 3"' + ) for i in range(15)] + ) + comments = { + "paint_info": PAINT_SPARSE, + "filament_settings_id": profiles, + } + rows = parse.filaments_of(comments) + self.assertEqual(rows[0]["profile"], "Sunlu PLA") + self.assertEqual(rows[1]["profile"], "Sunlu PLA Matte") + + def test_filaments_of_single_without_paint(self): + comments = { + "filament_type": "PLA", + "filament_colour": "#00FF00", + "filament_settings_id": '"Sunlu PLA Meta @Kobra 3"', + } + rows = parse.filaments_of(comments) + self.assertEqual(len(rows), 1) + self.assertEqual(rows[0]["profile"], "Sunlu PLA Meta") + self.assertEqual(rows[0]["type"], "PLA") + + +if __name__ == "__main__": + unittest.main() diff --git a/web/app.py b/web/app.py index bbb8095..962fca0 100644 --- a/web/app.py +++ b/web/app.py @@ -5,7 +5,7 @@ import re import secrets import time import uuid -from datetime import datetime +from datetime import datetime, timedelta from pathlib import Path from urllib.parse import quote @@ -77,6 +77,9 @@ def create_app(cfg): app.config["MAX_CONTENT_LENGTH"] = 256 * 1024 * 1024 app.config["SESSION_COOKIE_HTTPONLY"] = True app.config["SESSION_COOKIE_SAMESITE"] = "Lax" + # Orca-App-Fenster schließt oft — Session-Cookie muss überleben (30 Tage, gleitend). + app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(days=30) + app.config["SESSION_REFRESH_EACH_REQUEST"] = True if cfg.get("fqdn", "").startswith("https://"): app.config["SESSION_COOKIE_SECURE"] = True app.json.ensure_ascii = False @@ -421,6 +424,7 @@ def create_app(cfg): grotti_db.invalidate_user_count() store.touch_user_login(conn(), row["id"]) session.clear() + session.permanent = True session["user_id"] = row["id"] session["csrf"] = grotti_auth.new_csrf() store.add_event(conn(), "", "", "auth", "Admin %s angelegt" % username) @@ -442,6 +446,7 @@ def create_app(cfg): raise GrottiError("Benutzer oder Passwort falsch.", "auth", 401) store.touch_user_login(conn(), row["id"]) session.clear() + session.permanent = True session["user_id"] = row["id"] session["csrf"] = grotti_auth.new_csrf() store.add_event(conn(), "", "", "auth", "Login %s" % row["username"]) @@ -633,7 +638,8 @@ def create_app(cfg): @app.get("/kameras") def kameras(): - return page("kameras.html", "Kameras") + """Alte Links → Wand (Kameras sitzen als Snapshot-Thumbs auf den Karten).""" + return redirect("/", code=301) @app.get("/dateien") def dateien(): @@ -641,7 +647,7 @@ def create_app(cfg): @app.get("/projekte") def projekte(): - return redirect("/auftraege", code=301) + return page("projekte.html", "Projekte") @app.get("/auftraege") def auftraege_page(): @@ -1049,7 +1055,27 @@ def create_app(cfg): status = data.get("status") or "idee" if status not in PROJECT_STATUS: raise GrottiError("Unbekannter Status.", "status") - return jsonify(store.create_project(conn(), name, (data.get("notes") or "").strip(), status)), 201 + folder = (data.get("folder") or "").strip() + if not folder: + folder = library.default_project_folder(name) + try: + if cfg.get("watch_dir"): + folder = library.ensure_project_folder(cfg, folder) + else: + folder = library.normalize_folder(folder) + except GrottiError as exc: + if exc.code in ("ordner", "watch"): + raise GrottiError( + "%s — Ordner relativ unter dem Slice-Pfad, z. B. Projekte/%s (kein D:\\…)." + % (exc.message.rstrip("."), library.project_slug(name)), + exc.code, + getattr(exc, "status", 400) or 400, + ) from exc + raise + row = store.create_project( + conn(), name, (data.get("notes") or "").strip(), status, folder=folder + ) + return jsonify(row), 201 @app.get("/api/v1/projekte/") def project_get(project_id): @@ -1073,6 +1099,13 @@ def create_app(cfg): if data["status"] not in PROJECT_STATUS: raise GrottiError("Unbekannter Status.", "status") fields["status"] = data["status"] + if "folder" in data: + folder = (data.get("folder") or "").strip() + if folder and cfg.get("watch_dir"): + folder = library.ensure_project_folder(cfg, folder) + elif folder: + folder = library.normalize_folder(folder) + fields["folder"] = folder row = store.update_project(conn(), project_id, fields) if not row: raise GrottiError("Das Projekt fehlt.", "projekt", 404) @@ -1080,15 +1113,95 @@ def create_app(cfg): @app.delete("/api/v1/projekte/") def project_delete(project_id): + row = store.get_project(conn(), project_id) + if not row: + raise GrottiError("Das Projekt fehlt.", "projekt", 404) + # ?force=1 löscht den Projektordner inkl. Inhalt + force = (request.args.get("force") or "").strip() in ("1", "true", "yes") + folder = (row.get("folder") or "").strip() + if folder and cfg.get("watch_dir"): + library.delete_project_disk_folder(cfg, folder, force=force) if not store.delete_project(conn(), project_id): raise GrottiError("Das Projekt fehlt.", "projekt", 404) return "", 204 + @app.get("/api/v1/projekte//assets") + def project_assets_list(project_id): + row = store.get_project(conn(), project_id) + if not row: + raise GrottiError("Das Projekt fehlt.", "projekt", 404) + folder = (row.get("folder") or "").strip() + if not folder: + return jsonify([]) + if not cfg.get("watch_dir"): + return jsonify([]) + return jsonify(library.list_project_assets(cfg, folder)) + + @app.post("/api/v1/projekte//assets") + def project_assets_upload(project_id): + row = store.get_project(conn(), project_id) + if not row: + raise GrottiError("Das Projekt fehlt.", "projekt", 404) + folder = (row.get("folder") or "").strip() + if not folder: + raise GrottiError("Das Projekt hat keinen Ordner.", "ordner") + if not cfg.get("watch_dir"): + raise GrottiError("Kein Slice-Ordner gesetzt — unter Einstellungen festlegen.", "watch") + upload = request.files.get("file") + if not upload or not upload.filename: + raise GrottiError("Keine Datei angehängt.", "datei") + saved = library.save_project_asset(cfg, folder, upload.filename, upload.read()) + return jsonify(saved), 201 + + @app.get("/api/v1/projekte//assets/") + def project_assets_download(project_id, rel): + row = store.get_project(conn(), project_id) + if not row: + raise GrottiError("Das Projekt fehlt.", "projekt", 404) + folder = (row.get("folder") or "").strip() + if not folder: + raise GrottiError("Das Projekt hat keinen Ordner.", "ordner") + path = library.resolve_project_asset(cfg, folder, rel) + return send_file(path, as_attachment=True, download_name=path.name) + + @app.delete("/api/v1/projekte//assets/") + def project_assets_delete(project_id, rel): + row = store.get_project(conn(), project_id) + if not row: + raise GrottiError("Das Projekt fehlt.", "projekt", 404) + folder = (row.get("folder") or "").strip() + if not folder: + raise GrottiError("Das Projekt hat keinen Ordner.", "ordner") + library.delete_project_asset(cfg, folder, rel) + return "", 204 + @app.get("/api/v1/dateien") def file_list(): library.sync_watch(conn(), cfg, force=False) return jsonify(store.list_files(conn(), watch_dir=cfg.get("watch_dir"))) + @app.post("/api/v1/dateien//meta") + def file_meta_refresh(file_id): + """Eine Datei neu parsen — z. B. paint_index für ACE nach Orca-Deep-Link.""" + out = library.refresh_one_file(conn(), file_id, watch_dir=cfg.get("watch_dir")) + if not out: + raise GrottiError("Datei nicht gefunden.", "datei", 404) + return jsonify(out) + + @app.get("/api/v1/orca-fokus") + def orca_focus(): + """Zuletzt von orca_senden gesetzte Datei — offene Tabs schalten um statt neu zu öffnen.""" + path = Path(cfg["data_dir"]) / "orca-senden-focus.json" + if not path.is_file(): + return jsonify({}) + try: + data = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return jsonify({}) + if not isinstance(data, dict) or data.get("file_id") is None: + return jsonify({}) + return jsonify(data) + @app.post("/api/v1/dateien/neu-lesen") def file_rescan(): result = library.sync_watch(conn(), cfg, force=True) @@ -1203,13 +1316,7 @@ def create_app(cfg): @app.delete("/api/v1/dateien/") def file_delete(file_id): - row = store.get_file_row(conn(), file_id) - if not row: - raise GrottiError("Die Datei fehlt.", "datei", 404) - if row["source"] != "upload": - raise GrottiError("Die Datei kommt aus dem Watch-Ordner und bleibt, solange sie dort liegt.", "watch") - store.delete_file(conn(), file_id) - Path(row["stored_path"]).unlink(missing_ok=True) + library.delete_library_file(conn(), cfg, file_id) return "", 204 @app.post("/api/v1/senden") @@ -1575,6 +1682,17 @@ def create_app(cfg): except ValueError as exc: raise GrottiError(str(exc), "urteil") from exc run_json = store.set_verdict(c, run_row["id"], verdict, reason, ok_n, bad_n) + # Live-Lauf schließen: Moonraker-History bleibt oft ohne Endzeit hängen, + # sonst bleibt der Druck im Protokoll ewig „in_progress“ trotz Urteil. + if run_row and (run_row.get("state") or "") in ("in_progress", "printing", "paused"): + closed = store.complete_open_run(c, run_row["id"], state="completed") + if closed: + try: + jobs.apply_deduction(c, closed, cfg["energy"]) + except Exception as exc: + print("Abräumen Abzug %s: %s" % (host, exc), flush=True) + row = store.get_run_row(c, closed["id"]) or closed + run_json = store.run_out(row) reprint = None if bool(data.get("reprint")) and run_row and run_row.get("file_id"): reprint = store.enqueue_print( @@ -2098,11 +2216,27 @@ def create_app(cfg): @app.patch("/api/v1/spulen/") def spool_patch(spool_id): - row = store.update_spool(conn(), spool_id, spool_fields(body(), partial=True, spool_id=spool_id)) + data = body() + fields = spool_fields(data, partial=True, spool_id=spool_id) + if "archived" in data or "archived_at" in data: + if data.get("archived") is False or data.get("archived_at") is None: + fields["archived_at"] = None + elif data.get("archived") is True or data.get("archived_at") in (True, "now"): + fields["archived_at"] = "now" + row = store.update_spool(conn(), spool_id, fields) if not row: raise GrottiError("Die Spule fehlt.", "spule", 404) return jsonify(row) + @app.post("/api/v1/spulen//leer") + def spool_empty(spool_id): + """Rest 0, Regal, archivieren — wenn der Rest falsch geschätzt war.""" + row = store.mark_spool_empty(conn(), spool_id) + if not row: + raise GrottiError("Die Spule fehlt.", "spule", 404) + store.add_event(conn(), "", "", "spule", "Spule „%s“ als leer markiert" % row["name"]) + return jsonify(row) + @app.post("/api/v1/spulen//einlegen") def spool_load(spool_id): """Spule an Drucker/Platz legen; vorherige Rolle dort geht ins Regal.""" @@ -2112,7 +2246,8 @@ 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) + # Auch offene History-Jobs (Pause/Resume nach Moonraker-Split), nicht nur live: + live_run = store.find_active_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") @@ -2161,6 +2296,8 @@ def create_app(cfg): updated = store.update_spool(conn(), spool_id, {"printer_host": host, "ace_slot": slot}) if printing: store.open_segment(conn(), live_run["id"], spool_id, slot, mm_now) + store.assign_run_spool(conn(), live_run["id"], spool_id) + store.update_send_spool(conn(), host, live_run.get("filename") or "", spool_id) store.add_event( conn(), host, @@ -2462,6 +2599,7 @@ def create_app(cfg): host = (request.args.get("host") or "").strip() material = (request.args.get("material") or "").strip() brand = (request.args.get("brand") or "").strip() + color = (request.args.get("color") or "").strip() project_id = optional_id(request.args.get("project_id")) return jsonify(store.statistics( conn(), @@ -2470,6 +2608,7 @@ def create_app(cfg): host=host, material=material or "", brand=brand or "", + color=color or "", project_id=project_id, )) diff --git a/web/static/grotti.css b/web/static/grotti.css index eb21fd7..b79b733 100644 --- a/web/static/grotti.css +++ b/web/static/grotti.css @@ -693,6 +693,14 @@ h2.queue-title { font-size: var(--t-xs); } display: grid; gap: 0.45rem; } +.steer-group { display: grid; gap: 0.35rem; } +.steer-group-label { + font-size: 0.68rem; + font-weight: 650; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--faint); +} .steer-row { display: flex; flex-wrap: wrap; @@ -714,7 +722,8 @@ h2.queue-title { font-size: var(--t-xs); } .steer-ace-slots { display: grid; gap: 0.35rem; - padding-top: 0.35rem; + padding-top: 0.45rem; + margin-top: 0.15rem; border-top: 1px solid var(--line); } .steer-ace-head { @@ -726,9 +735,9 @@ h2.queue-title { font-size: var(--t-xs); } } .steer-ace-head b { font-weight: 650; } .steer-slot-row { - display: flex; - flex-wrap: wrap; - gap: 0.35rem 0.55rem; + display: grid; + grid-template-columns: auto 1fr auto; + gap: 0.45rem 0.55rem; align-items: center; font-size: 0.78rem; } @@ -739,7 +748,13 @@ h2.queue-title { font-size: var(--t-xs); } flex: none; border: 1px solid rgba(255, 255, 255, 0.15); } -.steer-slot-row .grow { min-width: 0; overflow-wrap: anywhere; } +.steer-slot-row .grow { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.steer-slot-row button { flex: none; white-space: nowrap; } .queue-name { color: var(--ink); font-size: var(--t-sm); font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; } .queue-msg { color: var(--muted); font-size: var(--t-xs); line-height: 1.3; overflow-wrap: anywhere; } .queue-sub { color: var(--faint); font-size: 0.66rem; font-variant-numeric: tabular-nums; flex: none; } @@ -871,6 +886,63 @@ h2 { font-size: 0.95rem; margin: 0 0 0.65rem; letter-spacing: -0.02em; font-weig padding: 0.4rem 0.55rem; white-space: nowrap; } +.stat-tools { + flex-direction: column; + align-items: stretch; + gap: 0.55rem; +} +.stat-tools .period-presets { + flex: 0 1 auto; + width: auto; +} +.stat-filters { + display: flex; + flex-wrap: wrap; + gap: 0.45rem 0.55rem; + align-items: center; +} +.stat-filters input[type="text"], +.stat-filters select { + width: auto; + min-width: 7rem; + max-width: 11rem; + flex: 0 1 auto; +} +.mat-extra-filters { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + align-items: center; +} +.mat-extra-filters select { + width: auto; + min-width: 7.5rem; + max-width: 11rem; + padding: 0.35rem 0.55rem; + min-height: var(--control-h); + border-radius: var(--radius-sm); +} +.mat-group td { + padding-top: 0.75rem !important; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--ink); + border-bottom: 1px solid var(--line); + background: rgba(255, 255, 255, 0.02); +} +tr.is-archived { opacity: 0.65; } +.kpi-row.kpi-primary { + grid-template-columns: repeat(auto-fill, minmax(10.5rem, 1fr)); +} +.kpi-row.kpi-secondary { + grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr)); + margin-top: -0.35rem; + opacity: 0.95; +} +.kpi.kpi-sm .kpi-value { font-size: 1.05rem; } +.kpi.kpi-empty { opacity: 0.55; } .lead, .tiny, .summary { color: var(--muted); } .summary { margin: 0.3rem 0 0; } .tiny { font-size: 0.8rem; margin: 0.2rem 0 0; line-height: 1.4; max-width: 52ch; } @@ -946,7 +1018,28 @@ article.machine.error:hover { border-color: rgba(248, 113, 113, 0.6); } article.machine.offline { opacity: 0.62; } article.machine.offline:hover { opacity: 1; } -.machine-head { padding: 0.85rem 0.95rem 0.3rem; } +.machine-head { + padding: 0.85rem 0.95rem 0.3rem; + display: grid; + grid-template-columns: 1fr; + gap: 0.55rem; + align-items: start; +} +.machine-head.has-cam { + grid-template-columns: 1fr auto; +} +.machine-head-main { min-width: 0; } +.wall-cam-thumb { + width: 4.75rem; + height: 3.35rem; + object-fit: cover; + border-radius: 9px; + border: 1px solid var(--line); + background: var(--card-2); + flex: none; + pointer-events: none; +} +.wall-cam-thumb[hidden] { display: none; } .machine-title { display: flex; align-items: center; @@ -1899,6 +1992,31 @@ body.desk-on main { font-size: var(--t-sm); } .desk-facts b { color: var(--ink); font-weight: 650; } +.desk-tech-facts { + gap: 0.35rem; +} +.fact-chip { + display: inline-flex; + align-items: center; + gap: 0.3rem; + padding: 0.2rem 0.5rem; + border-radius: 99px; + border: 1px solid var(--line); + background: rgba(255, 255, 255, 0.03); + font-size: 0.72rem; + color: var(--muted); + max-width: 100%; +} +.fact-chip .ico { width: 0.85em; height: 0.85em; opacity: 0.75; flex: none; } +.fact-chip-label { color: var(--faint); } +.fact-chip b { + color: var(--ink); + font-weight: 650; + font-variant-numeric: tabular-nums; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} .desk-foot { display: flex; flex-wrap: wrap; @@ -2320,16 +2438,33 @@ article.machine.flash { animation: flash 2s var(--ease); } .lib-tools { display: flex; flex-wrap: wrap; - gap: 0.45rem; - margin-bottom: 0.65rem; + align-items: center; + gap: 0.35rem; + margin-bottom: 0.55rem; position: sticky; top: var(--topbar-h); z-index: 5; background: linear-gradient(180deg, var(--card) 72%, transparent); - padding-bottom: 0.35rem; + padding-bottom: 0.3rem; +} +.lib-tools .search-wrap { + flex: 1 1 8rem; + min-width: 7rem; + max-width: 16rem; +} +.lib-tools input[type="search"] { flex: 1 1 auto; width: 100%; margin: 0; min-width: 0; } +.lib-tools select { + flex: 0 1 auto; + width: auto; + min-width: 6.5rem; + max-width: 9.5rem; + margin: 0; + padding: 0.35rem 0.5rem; +} +@media (min-width: 1100px) { + .lib-tools { flex-wrap: nowrap; } + .lib-tools .search-wrap { max-width: 12rem; } } -.lib-tools input[type="search"] { flex: 1 1 12rem; margin: 0; } -.lib-tools select { flex: 0 1 11rem; width: auto; margin: 0; } .lib-group { font-size: var(--t-xs); font-weight: 650; @@ -2486,6 +2621,27 @@ label.button { } label.button:hover { background: rgba(255, 255, 255, 0.04); } .page-head-side button { display: inline-flex; align-items: center; gap: 0.4rem; } +.proj-file-list { + display: grid; + gap: 0.35rem; +} +.proj-file, .proj-asset { + display: flex; + align-items: center; + gap: 0.55rem; + padding: 0.45rem 0.55rem; + border: 1px solid var(--line); + border-radius: var(--radius-sm); + background: rgba(255, 255, 255, 0.02); + min-width: 0; + text-decoration: none; + color: inherit; +} +.proj-file { flex-direction: column; align-items: flex-start; gap: 0.15rem; } +.proj-file:hover, .proj-asset:hover { border-color: var(--line-strong); } +.proj-asset a { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink); } +.proj-asset .tiny { flex: none; color: var(--muted); } + .lib-card { position: relative; transition: box-shadow 0.2s var(--ease); } .lib-card.drop { box-shadow: 0 0 0 2px var(--accent), var(--shadow); } .drop-hint { margin: 0.6rem 0 0; font-size: var(--t-xs); color: var(--faint); text-align: center; } @@ -3300,6 +3456,37 @@ td .cell-file { text-overflow: ellipsis; white-space: nowrap; } +.proto-table { + table-layout: fixed; + width: 100%; +} +.proto-table th:nth-child(1), +.proto-table td.proto-printer { width: 7.5rem; } +.proto-table th:nth-child(2), +.proto-table td.proto-file { width: auto; } +.proto-table th:nth-child(3) { width: 6.5rem; } +.proto-table th:nth-child(4), +.proto-table th:nth-child(5), +.proto-table th:nth-child(6) { width: 4.5rem; } +.proto-table th:nth-child(7) { width: 6.5rem; } +.proto-table th:nth-child(8), +.proto-table td.proto-spool { width: 10rem; } +.proto-table th:nth-child(9) { width: 5.5rem; } +.proto-table td.proto-file .cell-file { + max-width: 100%; +} +.proto-table td.proto-file .mono-badge { + margin-top: 0.15rem; + opacity: 0.85; +} +.proto-table td.proto-spool { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.proto-table td.proto-spool select { + max-width: 100%; +} td.actions-cell { white-space: nowrap; text-align: right; diff --git a/web/templates/bericht.html b/web/templates/bericht.html index 6a75832..30ca71e 100644 --- a/web/templates/bericht.html +++ b/web/templates/bericht.html @@ -29,7 +29,8 @@ function sourceLabel(source, estimate) { return source || ""; } -const ENERGY_PART_COLORS = { +const COST_PART_COLORS = { + filament: "#0d9488", base: "#94a3b8", nozzle: "#ea580c", bed: "#2563eb", @@ -37,19 +38,20 @@ const ENERGY_PART_COLORS = { }; function kwhLabel(value) { + if (value == null || value === "") return "—"; return (Number(value) || 0).toLocaleString("de-DE", { maximumFractionDigits: 3 }) + " kWh"; } -function energyPieSvg(parts) { - const items = (parts || []).filter((p) => Number(p.kwh) > 0); - const total = items.reduce((sum, p) => sum + Number(p.kwh || 0), 0); +function costPieSvg(parts) { + const items = (parts || []).filter((p) => Number(p.cost) > 0); + const total = items.reduce((sum, p) => sum + Number(p.cost || 0), 0); if (!items.length || total <= 0) return ""; const cx = 42; const cy = 42; const r = 36; let angle = -Math.PI / 2; const slices = items.map((p) => { - const share = Number(p.kwh) / total; + const share = Number(p.cost) / total; const sweep = share * Math.PI * 2; const x1 = cx + r * Math.cos(angle); const y1 = cy + r * Math.sin(angle); @@ -57,38 +59,48 @@ function energyPieSvg(parts) { const x2 = cx + r * Math.cos(angle); const y2 = cy + r * Math.sin(angle); const large = sweep > Math.PI ? 1 : 0; - const color = ENERGY_PART_COLORS[p.key] || "#64748b"; + const color = COST_PART_COLORS[p.key] || "#64748b"; if (share >= 0.999) { return ``; } return ``; }).join(""); - return `${slices}`; + return `${slices}`; } -function energyBreakdownHtml(energy) { - const parts = energy.parts || []; +function costBreakdownHtml(energy, filamentCost) { + const fil = Number(filamentCost) || 0; + const energyParts = (energy && energy.parts) || []; + const parts = []; + if (fil > 1e-6) { + parts.push({ key: "filament", label: "Filament", kwh: null, cost: fil }); + } + energyParts.forEach((p) => parts.push(p)); if (!parts.length) return ""; - const src = energy.source === "ed" ? "aus ED" : "pauschal"; - const rate = energy.euro_per_kwh != null + const energyEuro = Number((energy && energy.cost) || 0); + const totalEuro = fil + energyEuro; + const src = energy && energy.source === "ed" ? "Strom aus ED" : (energyParts.length ? "Strom pauschal" : ""); + const rate = energy && energy.euro_per_kwh != null ? Number(energy.euro_per_kwh).toLocaleString("de-DE", { style: "currency", currency: "EUR", maximumFractionDigits: 3 }) + "/kWh" : ""; const rows = parts.map((p) => { - const color = ENERGY_PART_COLORS[p.key] || "#64748b"; + const color = COST_PART_COLORS[p.key] || "#64748b"; return ` ${escapeHtml(p.label)} ${kwhLabel(p.kwh)} ${euro(p.cost)} `; }).join(""); + const sumKwh = energy && energy.kwh != null ? kwhLabel(energy.kwh) : "—"; + const hint = [src, rate].filter(Boolean).join(" · "); return `

-
${energyPieSvg(parts)}
+
${costPieSvg(parts)}
-

Strom

${escapeHtml(src)}${rate ? " · " + rate : ""}
+

Kosten

${escapeHtml(hint)}
${rows} - +
AnteilkWh€
Summe${kwhLabel(energy.kwh)}${euro(energy.cost)}
Summe${sumKwh}${euro(totalEuro)}
@@ -151,7 +163,7 @@ function render(data) { ${run.message ? "

" + escapeHtml(run.message) + "

" : ""} ${data.project ? "

Projekt: " + escapeHtml(data.project.name) + "

" : ""}
- ${energyBreakdownHtml(energy)} + ${costBreakdownHtml(energy, run.filament_cost)}

Spulen

diff --git a/web/templates/dateien.html b/web/templates/dateien.html index 3682bfa..cad156c 100644 --- a/web/templates/dateien.html +++ b/web/templates/dateien.html @@ -78,11 +78,10 @@
-

3Wann

+

3Start

- - - + +
@@ -189,7 +188,7 @@ let ablaufDefaults = { check: { max_delta_mm: 3.0, timeout_min: 2, x: 125, y: 125, ref_x: 25, ref_y: 125, z_clear: 20 }, }; let ablaufSteps = []; -const sel = { fileId: null, hosts: new Set(), mode: "now", tab: "send" }; +const sel = { fileId: null, hosts: new Set(), mode: "this", tab: "send" }; let aceHostsKey = ""; /* ---------- 3D-Pfad ---------- */ @@ -202,6 +201,25 @@ function ensureViewer() { return pathViewer; } +let toolpathGen = 0; + +function scheduleToolpath(file, progress, deferMs) { + const gen = ++toolpathGen; + const run = () => { + if (gen !== toolpathGen) return; + showToolpath(file, progress); + }; + if (deferMs > 0) { + if (window.requestIdleCallback) { + requestIdleCallback(run, { timeout: Math.max(800, deferMs) }); + } else { + setTimeout(run, deferMs); + } + return; + } + run(); +} + async function showToolpath(file, progress) { const card = document.getElementById("viewer-card"); const caption = document.getElementById("viewer-caption"); @@ -221,6 +239,7 @@ async function showToolpath(file, progress) { meta.textContent = "Pfad wird gelesen …"; try { const data = await api("/dateien/" + file.id + "/pfad?tp=8"); + if (String(sel.fileId) !== String(file.id)) return; viewer.setPath(data); viewerFileId = file.id; meta.textContent = (data.count || 0).toLocaleString("de-DE") + " Punkte"; @@ -262,7 +281,9 @@ function saveSendMemory() { function loadSendMemory() { try { const data = JSON.parse(localStorage.getItem(SEND_KEY) || "{}"); - if (["now", "free", "next"].includes(data.mode)) sel.mode = data.mode; + let mode = data.mode; + if (mode === "now" || mode === "free") mode = "this"; + if (["this", "next"].includes(mode)) sel.mode = mode; if (["send", "ablauf"].includes(data.tab)) sel.tab = data.tab; if (Array.isArray(data.hosts)) sel.hosts = new Set(data.hosts); } catch (error) { /* ignore */ } @@ -309,10 +330,22 @@ function slotTitle(host, slot) { return bits.join(" · "); } +function paintIndexOf(filament, fallback) { + const raw = filament && filament.paint_index; + if (raw === 0 || raw === "0") return 0; + const n = Number(raw); + return Number.isFinite(n) && n >= 0 ? n : fallback; +} + function filamentLabel(filament, index) { - const kind = filament.type || "Filament"; + const paint = paintIndexOf(filament, index); + const kind = filament.profile || filament.type || "Filament"; const color = filament.color ? " · " + filament.color : ""; - return "Farbe " + (index + 1) + " · " + kind + color; + return "T" + paint + " · " + kind + color; +} + +function canStartNow(p) { + return isFree(p) && !p.needs_clear; } function spoolsOnHost(host) { @@ -326,6 +359,19 @@ function spoolWarnText() { const maps = collectAceMaps(); const hosts = selectedHosts(); const fileTypes = ((file.filaments || []).map((f) => (f.type || "").toLowerCase()).filter(Boolean)); + const fileProfiles = ((file.filaments || []).map((f) => (f.profile || "").toLowerCase()).filter(Boolean)); + function profileOk(spoolRows) { + if (!fileProfiles.length) return true; + const names = spoolRows.map((s) => (s.name || "").toLowerCase()).filter(Boolean); + if (!names.length) return true; + return fileProfiles.every((want) => names.some((n) => { + if (n === want) return true; + if (!n.startsWith(want) || /[a-z0-9]/i.test(n.charAt(want.length) || "")) return false; + const rest = n.slice(want.length).replace(/^[\s·\-_|/]+/, ""); + const first = (rest.split(/\s+/)[0] || "").toLowerCase(); + return !["meta", "matte", "matt", "silk", "plus", "hf", "cf"].includes(first); + })); + } for (const host of hosts) { const name = (printers.find((p) => p.host === host) || {}).name || host; const mapping = maps[host] || []; @@ -334,18 +380,23 @@ function spoolWarnText() { let left = 0; let any = false; const types = []; + const mappedSpools = []; mapping.forEach((item) => { const slot = Number(item.ams_index) + 1; const spool = onHost.find((s) => Number(s.ace_slot) === slot); if (spool) { any = true; left += Number(spool.remaining_g) || 0; + mappedSpools.push(spool); if (spool.material_type) types.push(spool.material_type.toLowerCase()); } }); if (!any) return name + ": keine Grotti-Spule auf den gewählten ACE-Plätzen — Abzug entfällt."; if (left + 0.5 < need) return "ACE auf " + name + ": nur " + grams(left) + ", Datei braucht " + grams(need) + "."; - if (fileTypes.length && types.length && !fileTypes.some((t) => types.includes(t))) { + if (!profileOk(mappedSpools)) { + return name + ": Sorte passt nicht (Datei " + fileProfiles.join("/") + ")."; + } + if (!fileProfiles.length && fileTypes.length && types.length && !fileTypes.some((t) => types.includes(t))) { return name + ": Material passt nicht (Datei " + fileTypes.join("/") + ", ACE " + types.join("/") + ")."; } continue; @@ -359,8 +410,11 @@ function spoolWarnText() { if (left + 0.5 < need) { return name + ": nur " + grams(left) + ", Datei braucht " + grams(need) + "."; } + if (!profileOk(external)) { + return name + ": Sorte passt nicht (Datei " + fileProfiles.join("/") + ")."; + } const types = external.map((s) => (s.material_type || "").toLowerCase()).filter(Boolean); - if (fileTypes.length && types.length && !fileTypes.some((t) => types.includes(t))) { + if (!fileProfiles.length && fileTypes.length && types.length && !fileTypes.some((t) => types.includes(t))) { return name + ": Material passt nicht (Datei " + fileTypes.join("/") + ", Drucker " + types.join("/") + ")."; } } @@ -432,16 +486,17 @@ function renderAceMaps(force) { title.className = "ace-printer-name"; title.textContent = printer.name; block.appendChild(title); - paints.forEach((filament, paintIndex) => { + paints.forEach((filament, uiIndex) => { + const paint = paintIndexOf(filament, uiIndex); const row = document.createElement("label"); row.className = "ace-row"; const swatch = document.createElement("span"); swatch.className = "ace-swatch"; if (filament.color) swatch.style.background = filament.color; const text = document.createElement("span"); - text.textContent = filamentLabel(filament, paintIndex); + text.textContent = filamentLabel(filament, uiIndex); const select = document.createElement("select"); - select.dataset.paint = String(paintIndex); + select.dataset.paint = String(paint); select.innerHTML = ''; printer.slots.forEach((slot) => { const option = document.createElement("option"); @@ -452,16 +507,30 @@ function renderAceMaps(force) { const hex = (spool && spool.colors && spool.colors[0] && spool.colors[0].hex) || slot.color || ""; if (hex) option.style.borderLeft = "4px solid " + hex; const spoolType = (spool && spool.material_type) || slot.type || ""; - // Vorauswahl: gleiche Sorte (Grotti-Spule bevorzugt), sonst Platz in Reihenfolge - if (!select.dataset.picked && filament.type && spoolType + const spoolName = ((spool && spool.name) || "").toLowerCase(); + const wantProfile = (filament.profile || "").toLowerCase(); + // Vorauswahl: Katalog-Sorte exakt (Meta ≠ Matte), sonst grober Typ + let profileHit = false; + if (wantProfile && spoolName) { + if (spoolName === wantProfile) profileHit = true; + else if (spoolName.startsWith(wantProfile) && !/[a-z0-9]/i.test(spoolName.charAt(wantProfile.length) || "")) { + const rest = spoolName.slice(wantProfile.length).replace(/^[\s·\-_|/]+/, ""); + const first = (rest.split(/\s+/)[0] || "").toLowerCase(); + if (!["meta", "matte", "matt", "silk", "plus", "hf", "cf"].includes(first)) profileHit = true; + } + } + if (!select.dataset.picked && profileHit) { + option.selected = true; + select.dataset.picked = "1"; + } else if (!select.dataset.picked && !wantProfile && filament.type && spoolType && filament.type.toLowerCase() === String(spoolType).toLowerCase()) { option.selected = true; select.dataset.picked = "1"; } select.appendChild(option); }); - if (!select.dataset.picked && printer.slots[paintIndex]) { - select.value = String(printer.slots[paintIndex].index); + if (!select.dataset.picked && printer.slots[uiIndex]) { + select.value = String(printer.slots[uiIndex].index); } select.addEventListener("change", updateSpoolWarn); row.appendChild(swatch); @@ -638,8 +707,9 @@ function fillFolderFilter() { } function moveSelectHtml(file) { - if (!hasWatchDir || file.source !== "watch") return ""; - const current = folderKey(file); + if (!hasWatchDir) return ""; + if (file.source !== "watch" && file.source !== "upload") return ""; + const current = file.source === "upload" ? "" : folderKey(file); const opts = [ ``, ``, @@ -701,8 +771,8 @@ function sliceOf(file) { } function materialText(file) { - const types = [...new Set(((file && file.filaments) || []).map((f) => f.type).filter(Boolean))]; - return types.join("+"); + const labels = [...new Set(((file && file.filaments) || []).map((f) => f.profile || f.type).filter(Boolean))]; + return labels.join("+"); } function layerHeightText(file) { @@ -757,8 +827,7 @@ function thumbHtml(file) { function libItemHtml(file) { const project = projects.find((p) => String(p.id) === String(file.project_id)); - const del = file.source === "upload" - ? `` : ""; + const del = ``; return `
${thumbHtml(file)} ${escapeHtml(file.name)}${alwaysMeta(file, project)} @@ -882,17 +951,25 @@ function updateSendButton() { let note = ""; if (!file) problem = "Erst eine Datei wählen."; else if (file.kind === "3mf") problem = "Die 3MF hat keinen G-code. Mit G-code slicen."; - if (sel.mode === "now") { + if (sel.mode === "this") { if (!problem && !chosen.length) problem = "Einen oder mehrere Drucker wählen."; - const busy = chosen.filter((p) => !isFree(p)); - if (!problem && busy.length) note = namesText(busy) + " " + (busy.length === 1 ? "ist" : "sind") + " nicht frei. „Wenn frei“ reiht stattdessen ein."; - text = chosen.length ? "Jetzt starten auf " + namesText(chosen) : "Jetzt starten"; - } else if (sel.mode === "free") { - text = chosen.length ? "Einreihen für " + namesText(chosen) : "Einreihen: nächster freier " + familyOfSelection(); - note = chosen.length ? "Startet von selbst, sobald der Drucker bereit und die Platte frei ist." : "Ohne Drucker: der nächste freie passende übernimmt."; + const ready = chosen.filter(canStartNow); + const later = chosen.filter((p) => !canStartNow(p)); + if (ready.length && !later.length) { + text = "Jetzt starten auf " + namesText(ready); + note = ""; + } else if (!ready.length && later.length) { + text = "Einreihen für " + namesText(later); + note = "Startet, sobald der Drucker bereit und die Platte frei ist."; + } else if (ready.length && later.length) { + text = "Starten / einreihen auf " + namesText(chosen); + note = namesText(ready) + " sofort, " + namesText(later) + " in die Warteschlange."; + } else { + text = "Starten"; + } } else { - text = "Einreihen: nächster freier " + familyOfSelection(); - note = "Grotti nimmt den ersten freien Drucker mit passender Maschine."; + text = "Einreihen: nächster passender " + familyOfSelection(); + note = "Grotti wählt den nächsten freien Drucker mit passender Sorte (z. B. PLA Meta ≠ Matte)."; } const mismatch = chosen.filter((p) => familyMismatch(p, file)); if (!problem && mismatch.length && !document.getElementById("send-override").checked) { @@ -902,7 +979,7 @@ function updateSendButton() { button.disabled = !!problem || !!sendPoll; hint.textContent = problem || note; hint.classList.toggle("bad", !!problem); - targetHint.textContent = sel.mode === "next" && sel.tab === "send" ? "Ziel wählt Grotti." : chosen.length > 1 ? "Je Drucker ein eigener Auftrag." : ""; + targetHint.textContent = sel.mode === "next" && sel.tab === "send" ? "Ziel wählt Grotti nach Sorte." : chosen.length > 1 ? "Je Drucker ein eigener Auftrag." : ""; } function updateAblaufButton() { @@ -971,7 +1048,7 @@ function selectFile(id) { } document.querySelectorAll("#lib-list .lib-item").forEach((n) => n.classList.toggle("on", n.dataset.file === String(id))); refreshFlow(); - showToolpath(file, 0); + scheduleToolpath(file, 0, 0); if (window.matchMedia("(max-width: 1080px)").matches) { document.querySelector(".send-panel").scrollIntoView({ behavior: "smooth", block: "start" }); } @@ -1015,19 +1092,72 @@ async function load() { renderLibrary(); renderTabs(); refreshFlow(); - const file = selectedFile(); - if (file) showToolpath(file, 0); - // Deep-Link von Orca /orca_senden: Senden in den Blick, Optionen offen für ACE - if (new URLSearchParams(location.search).get("file") && file) { + let file = selectedFile(); + // Orca/Anycubic: Metadaten oft ohne paint_index → ACE zeigt T0/T1 statt T1/T10 + if (file && file.id && filamentsNeedPaintIndex(file.filaments)) { + try { + const fresh = await api("/dateien/" + file.id + "/meta", { method: "POST" }); + if (fresh && fresh.id) { + const idx = files.findIndex((row) => String(row.id) === String(fresh.id)); + if (idx >= 0) files[idx] = fresh; + else files.unshift(fresh); + file = selectedFile(); + renderLibrary(); + refreshFlow(); + } + } catch (error) { /* manuell: Ordner neu lesen */ } + } + const q = new URLSearchParams(location.search); + const deepLink = !!(q.get("file") || q.get("orca")); + // Deep-Link / Orca-App: ACE/Senden zuerst — 3D-Pfad friert sonst den Tab ein + if (file) scheduleToolpath(file, 0, deepLink ? 900 : 0); + if (deepLink) { sel.tab = "send"; renderTabs(); const opts = document.getElementById("options"); if (opts) opts.open = true; const panel = document.querySelector(".send-panel"); if (panel) panel.scrollIntoView({ behavior: "smooth", block: "start" }); + if (file) lastOrcaFocusId = String(file.id); + if (q.get("orca")) pullOrcaFocus(); } } +let lastOrcaFocusId = null; + +async function pullOrcaFocus() { + try { + const data = await api("/orca-fokus"); + if (!data || data.file_id == null) return; + const id = String(data.file_id); + const age = Date.now() / 1000 - Number(data.ts || 0); + if (age > 180) return; + const already = id === String(sel.fileId) && selectedFile(); + if (id === lastOrcaFocusId && already) return; + const switched = lastOrcaFocusId != null && id !== lastOrcaFocusId; + lastOrcaFocusId = id; + if (already) return; + let file = files.find((row) => String(row.id) === id); + if (!file) { + files = await api("/dateien"); + renderLibrary(); + file = files.find((row) => String(row.id) === id); + } + if (file) { + selectFile(file.id); + sel.tab = "send"; + renderTabs(); + if (switched) toast("Neue Slice-Datei ausgewählt.", "good"); + } + } catch (error) { /* nächster Tick */ } +} + +function filamentsNeedPaintIndex(filaments) { + const rows = filaments || []; + if (rows.length < 2) return false; + return rows.some((item) => !item || item.paint_index == null); +} + async function refreshPrinters() { if (document.hidden) return; try { @@ -1105,32 +1235,36 @@ async function watchJobs(ids) { await tick(); } +async function postSenden(hosts, whenFree) { + return api("/senden", { + method: "POST", + json: { + file_id: Number(sel.fileId), + hosts, + confirm: true, + override_machine: document.getElementById("send-override").checked, + bed_leveling: document.getElementById("send-bed-level").checked, + ace_maps: collectAceMaps(), + when_free: whenFree, + }, + }); +} + async function doSend() { const button = document.getElementById("send-go"); - const whenFree = sel.mode !== "now"; button.disabled = true; document.getElementById("send-note").textContent = ""; try { - const payload = await api("/senden", { - method: "POST", - json: { - file_id: Number(sel.fileId), - hosts: selectedHosts(), - confirm: true, - override_machine: document.getElementById("send-override").checked, - bed_leveling: document.getElementById("send-bed-level").checked, - ace_maps: collectAceMaps(), - when_free: whenFree, - }, - }); const projectId = document.getElementById("send-project")?.value; if (projectId && sel.fileId) { try { await api("/dateien/" + sel.fileId, { method: "PATCH", json: { project_id: Number(projectId) } }); } catch (error) { /* Projektbindung optional */ } } - const jobs = payload.jobs || []; - if (whenFree || payload.mode === "when_free") { + + if (sel.mode === "next") { + const payload = await postSenden([], true); + const jobs = payload.jobs || []; document.getElementById("send-progress").hidden = true; toast((jobs.length > 1 ? jobs.length + " Aufträge" : "Auftrag") + " eingereiht. Steht unter „Wartet“.", "good"); updateSendButton(); @@ -1138,9 +1272,35 @@ async function doSend() { refreshPrinters(); return; } - showJobsProgress(jobs); + + const chosen = selectedPrinters(); + const ready = chosen.filter(canStartNow).map((p) => p.host); + const later = chosen.filter((p) => !canStartNow(p)).map((p) => p.host); + let queued = 0; + let nowJobs = []; + if (later.length) { + const payload = await postSenden(later, true); + queued += (payload.jobs || []).length; + } + if (ready.length) { + const payload = await postSenden(ready, false); + nowJobs = payload.jobs || []; + } + if (queued && !nowJobs.length) { + document.getElementById("send-progress").hidden = true; + toast((queued > 1 ? queued + " Aufträge" : "Auftrag") + " eingereiht. Steht unter „Wartet“.", "good"); + updateSendButton(); + if (typeof refreshPrintQueue === "function") refreshPrintQueue(); + refreshPrinters(); + return; + } + if (queued && nowJobs.length) { + toast(queued + " eingereiht, " + nowJobs.length + " startet jetzt.", "good"); + if (typeof refreshPrintQueue === "function") refreshPrintQueue(); + } + showJobsProgress(nowJobs); if (typeof refreshSendQueue === "function") refreshSendQueue(); - await watchJobs(jobs.map((job) => job.id)); + await watchJobs(nowJobs.map((job) => job.id)); } catch (error) { updateSendButton(); toast(error.message || String(error), "bad"); @@ -1445,7 +1605,7 @@ document.getElementById("targets").addEventListener("click", (event) => { toast("Abläufe gehen nur auf Kobra 3.", "bad"); return; } - if (sel.tab === "send" && sel.mode === "next") sel.mode = "free"; + if (sel.tab === "send" && sel.mode === "next") sel.mode = "this"; if (sel.hosts.has(host)) sel.hosts.delete(host); else sel.hosts.add(host); saveSendMemory(); @@ -1479,19 +1639,11 @@ document.getElementById("send-bed-level").addEventListener("change", updateOptio document.getElementById("send-override").addEventListener("change", () => { refreshFlow(); }); document.getElementById("send-project")?.addEventListener("change", updateOptionsSummary); -document.getElementById("send-go").addEventListener("click", (event) => { - const button = event.currentTarget; - const warn = spoolWarnText(); - if (sel.mode === "now") { - armButton(button, warn ? "Warnung — trotzdem starten?" : "Wirklich starten? Nochmal klicken", doSend); - } else if (warn) { - armButton(button, "Warnung — trotzdem einreihen?", doSend); - } else { - doSend(); - } +document.getElementById("send-go").addEventListener("click", () => { + doSend(); }); -document.getElementById("ablauf-go").addEventListener("click", (event) => { - armButton(event.currentTarget, "Ablauf wirklich einreihen? Nochmal klicken", doAblauf); +document.getElementById("ablauf-go").addEventListener("click", () => { + doAblauf(); }); document.getElementById("ablauf-add-print").addEventListener("click", () => addStep(defaultPrintStep())); @@ -1591,5 +1743,6 @@ loadSendMemory(); loadAblaufMemory(); load().catch((error) => toast(error.message || String(error), "bad")); setInterval(refreshPrinters, 5000); +setInterval(pullOrcaFocus, 2000); {% endblock %} diff --git a/web/templates/doku.html b/web/templates/doku.html index 568d2cc..fe2e384 100644 --- a/web/templates/doku.html +++ b/web/templates/doku.html @@ -13,15 +13,14 @@
Datei Drucker - Jetzt · Wenn frei · Nächster freier + Dieser Drucker · Nächster passender Druck Platte räumen
  1. Datei wählen. Unter Dateien links in der Bibliothek. Der Slice-Ordner wird beobachtet, Hochladen geht per Knopf oder Ziehen.
  2. Drucker wählen. Die Kacheln zeigen live, was jeder Drucker gerade tut. Passt die Maschine nicht zur Datei, ist die Kachel blass.
  3. -
  4. Wann. Jetzt startet sofort. Wenn frei reiht für diesen Drucker ein. Nächster freier lässt Grotti den ersten freien passenden Drucker nehmen.
  5. -
  6. Starten. „Jetzt“ will zwei Klicks. Der zweite ist die Bestätigung.
  7. +
  8. Start. Dieser Drucker: frei → sofort, sonst Warteschlange. Nächster passender: Grotti wählt nach Maschine und Sorte (PLA Meta ≠ Matte).
  9. Während des Drucks zeigt die Wand Fortschritt, Fertig-Uhrzeit und Temperaturen. Ein Klick auf die Karte öffnet das Cockpit mit Kamera (falls vorhanden), Steuerung, Bettnetz und 3D-Pfad. Die Seitenleiste zeigt immer, was läuft und was wartet.
  10. Danach ist die Platte belegt. Ein wartender Auftrag startet erst, wenn auf der Karte Platte geräumt gedrückt wurde, oder ein Ablauf sie selbst freischiebt.
@@ -49,7 +48,8 @@
Auftrag
Ein Eintrag in der Warteschlange: eine Datei für einen Drucker oder für den nächsten freien.
Ablauf
Auftrag mit mehreren Schritten und Wiederholungen. Nur Kobra 3.
-
Wenn frei
Startet, sobald der Drucker bereit und die Platte geräumt ist.
+
Dieser Drucker
Frei und Platte klar → sofort; sonst Warteschlange für genau diesen Drucker.
+
Nächster passender
Grotti claimt den nächsten freien Drucker mit passender Katalog-Sorte und Restfilament.
Platte belegt
Nach jedem Druck gesetzt. Blockiert den nächsten Auftrag auf diesem Drucker.
Alert
Wichtiges Ereignis (Fertig, Fehler, Abbruch, offline). Steht in der Seitenleiste und geht auf Wunsch an Telegram.
Ereignis
Jeder Eintrag unter Aktivität. Ablauf-Schritte und Starts sind Ereignisse, aber keine Alerts.
diff --git a/web/templates/layout.html b/web/templates/layout.html index bbf9dbc..e497152 100644 --- a/web/templates/layout.html +++ b/web/templates/layout.html @@ -9,7 +9,7 @@ - + +{% endblock %} diff --git a/web/templates/protokoll.html b/web/templates/protokoll.html index 5a73a81..3e633ab 100644 --- a/web/templates/protokoll.html +++ b/web/templates/protokoll.html @@ -23,7 +23,7 @@

- +
DruckerDateiZustandDauerIstKostenUrteilSpule
@@ -65,6 +65,16 @@ function filteredRuns() { }); } +function shortRunName(filename) { + let name = (filename || "").split("/").pop() || ""; + if (!name) return ""; + name = name.replace(/^\d{8}-\d{6}-/, ""); + name = name.replace(/^\d{4}-\d{4}-/, ""); + name = name.replace(/\.gcode$/i, ""); + name = name.replace(/_\d+(?:\.\d+)?_(?:\d+h)?(?:\d+m)?(?:\d+s)?$/i, ""); + return name || (filename || "").split("/").pop() || ""; +} + function render() { fillPrinterFilter(); const body = document.getElementById("run-rows"); @@ -83,12 +93,15 @@ function render() { tr.className = "pick"; tr.innerHTML = ""; tr.children[0].textContent = run.printer_name; + tr.children[0].className = "proto-printer"; const fileCell = tr.children[1]; + fileCell.className = "proto-file"; + const fullName = (run.filename || "").split("/").pop() || ("#" + run.id); const link = document.createElement("a"); link.href = "/protokoll/" + run.id; link.className = "cell-file"; - link.title = (run.filename || "").split("/").pop() || ("#" + run.id); - link.textContent = (run.filename || "").split("/").pop() || ("#" + run.id); + link.title = fullName; + link.textContent = shortRunName(run.filename) || fullName; fileCell.appendChild(link); if (run.chargen_nr) { const code = document.createElement("div"); @@ -167,7 +180,10 @@ function render() { } else { const spool = spools.find((item) => item.id === run.spool_id); tr.children[7].textContent = spool ? spool.name : "abgezogen"; + tr.children[7].title = spool ? spool.name : ""; + tr.children[7].className = "proto-spool"; } + if (!tr.children[7].className) tr.children[7].className = "proto-spool"; if (run.file_id) { const again = document.createElement("button"); again.type = "button"; diff --git a/web/templates/statistik.html b/web/templates/statistik.html index 3079c9f..d03a67c 100644 --- a/web/templates/statistik.html +++ b/web/templates/statistik.html @@ -9,7 +9,7 @@ CSV
-
+
@@ -19,20 +19,24 @@
- - - - - -
+
+ + + + + + +

{% endblock %} {% block script %} diff --git a/web/templates/wand.html b/web/templates/wand.html index 5a411a5..5381e34 100644 --- a/web/templates/wand.html +++ b/web/templates/wand.html @@ -340,6 +340,14 @@ function flowHtml(printer) { return bits.join(""); } +function filamentPauseHint(printer) { + if (!printer) return ""; + if (printer.filament_empty) return printer.pause_reason || printer.message || "Filament leer"; + const text = String(printer.pause_reason || printer.message || ""); + if (/filament|spule|material.?leer|gerissen|runout|10107/i.test(text)) return text; + return ""; +} + function stripsHtml(printer, state) { const bits = []; const busy = state === "printing" || state === "paused" || state === "heating"; @@ -348,6 +356,14 @@ function stripsHtml(printer, state) { if (state === "error") { bits.push(`
${icon("alert")}${escapeHtml(printer.message || "Drucker meldet einen Fehler.")}
`); } + const filamentHint = state === "paused" ? filamentPauseHint(printer) : ""; + if (filamentHint) { + bits.push(`
+
${icon("alert")}Neue Rolle in Material dem Drucker zuweisen
+ ${escapeHtml(filamentHint)} — sonst zählt der Verbrauch weiter zur alten Spule. Ort = dieser Drucker, speichern, dann Weiter. + +
`); + } const failed = !queueBusy && printer.queue_failed; if (failed) { const step = failed.step_label_current || "Schritt " + ((Number(failed.step_index) || 0) + 1); @@ -417,7 +433,11 @@ function aceSlotLabel(slot) { function aceSteerHtml(printer) { const dryers = printer.dryers || []; - if (!dryers.length) return ""; + const loaded = wallSpools + .filter((s) => s.printer_host === printer.host && s.ace_slot) + .slice() + .sort((a, b) => Number(a.ace_slot) - Number(b.ace_slot)); + if (!dryers.length && !loaded.length) return ""; const bits = []; dryers.forEach((dryer) => { const id = Number(dryer.id); @@ -443,10 +463,6 @@ function aceSteerHtml(printer) {
`); }); - const loaded = wallSpools - .filter((s) => s.printer_host === printer.host && s.ace_slot) - .slice() - .sort((a, b) => Number(a.ace_slot) - Number(b.ace_slot)); if (loaded.length) { bits.push(`
PlätzeSorte und Farbe aus Grotti
`); loaded.forEach((spool) => { @@ -472,16 +488,22 @@ function steerPanelHtml(printer) { const fan = typeof printer.fan === "number" ? Math.round(printer.fan * 100) : 0; const aceKey = aceSteerKey(printer); return `
-
- - - - +
+
Heizung
+
+ + + + +
-
- - - +
+
Druck
+
+ + + +
${aceSteerHtml(printer)}

Enter im Feld setzt nur diesen Wert.

@@ -564,19 +586,24 @@ function factsHtml(printer) { `; } +function factChip(label, value, ico) { + if (!value && value !== 0) return ""; + return `${ico || ""}${escapeHtml(label)}${escapeHtml(String(value))}`; +} + function techFactsHtml(printer) { const part = fanPct(printer, "part") || fanPct(printer); const chamberFan = fanPct(printer, "chamber"); const auxFan = fanPct(printer, "aux"); const speed = speedPct(printer); - return ` - ${part ? "" + icon("fan") + " Teil " + part + "" : ""} - ${chamberFan ? "" + icon("fan-chamber") + " Bauraum " + chamberFan + "" : ""} - ${auxFan ? "" + icon("fan-aux") + " Aux " + auxFan + "" : ""} - ${speed ? "Speed " + speed + "" : ""} - ${anycubicTaskLine(printer)} - Host ${escapeHtml(printer.host)} - `; + return [ + part ? factChip("Teil", part, icon("fan")) : "", + chamberFan ? factChip("Bauraum", chamberFan, icon("fan-chamber")) : "", + auxFan ? factChip("Aux", auxFan, icon("fan-aux")) : "", + speed ? factChip("Speed", speed) : "", + anycubicTaskLine(printer), + factChip("Host", printer.host), + ].filter(Boolean).join(""); } function anycubicTaskLine(printer) { @@ -584,13 +611,11 @@ function anycubicTaskLine(printer) { if (!task || typeof task !== "object") return ""; const state = String(task.state || "").trim(); const tid = task.task_id != null && String(task.task_id).trim() ? String(task.task_id) : ""; - const file = task.filename ? String(task.filename) : ""; - if (!state && !tid && !file) return ""; + if (!state && !tid) return ""; const bits = []; if (state) bits.push(state); - if (tid) bits.push("task " + tid); - if (file) bits.push(file); - return "Anycubic " + escapeHtml(bits.join(" · ")) + ""; + if (tid) bits.push("#" + tid); + return factChip("Anycubic", bits.join(" · ")); } function jobLineVisible(printer, state, filename) { @@ -626,12 +651,15 @@ function buildCard(printer) { article.dataset.needsClear = needsClear(printer) ? "1" : ""; article.innerHTML = `
-
-

- ${badge} - +
+
+

+ ${badge} + +
+
-
+
@@ -655,6 +683,59 @@ function buildCard(printer) { return article; } +/** Wand-Thumb immer Snapshot — nie Stream, unabhängig von der Browser-Einstellung. */ +function wallCamSnapshotUrl(printer) { + if (!printer || !printer.camera || printer.has_camera === false) return ""; + return withToken(printer.camera) + "&t=" + Math.floor(Date.now() / 3000); +} + +function patchWallCam(article, printer) { + const img = article.querySelector('[data-role="wall-cam"]'); + const head = article.querySelector(".machine-head"); + if (!img) return; + const next = wallCamSnapshotUrl(printer); + if (!next) { + img.hidden = true; + img.removeAttribute("src"); + img.dataset.srcKey = ""; + img.dataset.pending = ""; + if (head) head.classList.remove("has-cam"); + return; + } + img.alt = "Kamera " + (printer.name || printer.host || ""); + if (img.dataset.srcKey === next) { + img.hidden = false; + if (head) head.classList.add("has-cam"); + return; + } + img.dataset.pending = next; + const probe = new Image(); + probe.onload = () => { + if (img.dataset.pending !== next) return; + img.dataset.srcKey = next; + img.dataset.pending = ""; + img.src = next; + img.hidden = false; + if (head) head.classList.add("has-cam"); + }; + probe.onerror = () => { + if (img.dataset.pending !== next) return; + img.dataset.pending = ""; + img.hidden = true; + img.removeAttribute("src"); + img.dataset.srcKey = ""; + if (head) head.classList.remove("has-cam"); + }; + probe.src = next; +} + +function refreshWallCams() { + for (const [host, article] of cardNodes.entries()) { + const printer = printerCache.get(host); + if (printer) patchWallCam(article, printer); + } +} + function steerHasFocus(root) { const active = document.activeElement; return !!(active && root.contains(active) && active.matches("input, select, textarea, button")); @@ -772,6 +853,7 @@ function patchCard(article, printer) { setHtml(role("flow"), flowHtml(printer)); setHtml(role("strips"), stripsHtml(printer, state)); setHtml(role("controls"), actionsHtml(state, printer)); + patchWallCam(article, printer); } function refreshDeskCamera(printer) { @@ -1618,6 +1700,12 @@ async function handleWallClick(event) { } button.disabled = true; try { + if (button.dataset.cmd === "fortsetzen") { + const card = printerCache.get(host); + if (card && filamentPauseHint(card)) { + toast("Neue Spule unter Material dem Drucker zuweisen — sonst bleibt der Abzug bei der alten Rolle."); + } + } await api("/drucker/" + encodeURIComponent(host) + "/" + button.dataset.cmd, { method: "POST", json: {} }); showNote("note", null); refresh(); @@ -1665,6 +1753,7 @@ deskEl.addEventListener("keydown", async (event) => { refresh().catch((error) => showNote("note", error)); createPoller(() => refresh().catch((error) => showNote("note", error)), 12000).start(); +setInterval(refreshWallCams, 3000); setInterval(() => { api("/dateien").then((rows) => { libraryFiles = rows || []; }).catch(() => {}); api("/spulen").then((rows) => { wallSpools = rows || []; }).catch(() => {});