From ae44441cfbe465f34ba9c6370041dc9d978966f4 Mon Sep 17 00:00:00 2001 From: Jonas Menzel Date: Sun, 25 Oct 2026 19:55:07 +0100 Subject: [PATCH] New web UI with iPhone web app Redesigned status, log, tag generator and settings pages: spool rings in filament colour, live job card (progress and time left read from CC2 Control's /api/printer while a job runs), log page with filters, iOS-style settings with a write/dry-run switch. On phones: bottom tab bar, printer switcher, slot details in a bottom sheet. Dark mode follows the system. Web app manifest and home screen icons for "Add to Home Screen". Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014GcEk5tkfFSYD6NTF66NkC --- README.md | 9 +- app/bridge.py | 29 +++ app/static/icon-180.png | Bin 0 -> 2768 bytes app/static/icon-192.png | Bin 0 -> 3038 bytes app/static/icon-512.png | Bin 0 -> 10005 bytes app/static/icon.svg | 1 + app/templates/base.html | 294 ++++++++++++++++++++------- app/templates/log.html | 78 ++++++++ app/templates/settings.html | 210 +++++++++++++------- app/templates/status.html | 385 +++++++++++++++++++++++++++++------- app/templates/tags.html | 207 ++++++++++++------- app/web.py | 17 ++ tests/fakes.py | 5 + tests/test_bridge.py | 14 ++ tests/test_web.py | 11 +- tools/mock_env.py | 7 + 16 files changed, 982 insertions(+), 285 deletions(-) create mode 100644 app/static/icon-180.png create mode 100644 app/static/icon-192.png create mode 100644 app/static/icon-512.png create mode 100644 app/static/icon.svg create mode 100644 app/templates/log.html diff --git a/README.md b/README.md index b609971..121258a 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Bridge zwischen der Canvas eines oder mehrerer **Elegoo Centauri Carbon 2** (Com | System | Zugriff | |---|---| | Drucker | **nur lesen**: `cat /opt/usr/canvas_record_info.json` per SSH. Kein MQTT, keine Druckerbefehle, keine Registrierung als MQTT-Client. | -| CC2 Control | Spulenverwaltung: `GET /api/spools`, `POST /api/spools/save`, `/assign`, `/enable` (nur nach Bestätigung) | +| CC2 Control | Spulenverwaltung: `GET /api/spools`, `POST /api/spools/save`, `/assign`, `/enable` (nur nach Bestätigung); während eines Drucks zusätzlich `GET /api/printer` (nur für Fortschritt und Restzeit in der Oberfläche) | | Spoolman | `GET /spool`, `PATCH /spool/{id}` (`location`), `PUT /spool/{id}/use` | **Dry-Run ist Standard.** Solange er aktiv ist, werden alle Schreibaktionen nur als „würde …“ protokolliert. @@ -70,6 +70,13 @@ Ein Update von v1 übernimmt Einstellungen, Zuordnungen, Buchungsstände und den Drucker heißt danach „CC2“ (Ort bleibt „CC2 Slot N“) und kann in den Einstellungen umbenannt werden – Ort und Felder in Spoolman ziehen dann mit. +### Am iPhone als Web-App + +`http://10.10.6.10:5066` in Safari öffnen → *Teilen* → **Zum Home-Bildschirm**. Die Bridge startet dann ohne +Safari-Leiste mit eigenem Icon. Unten liegen die Tabs Status, Protokoll, Tags und Einstellungen; auf *Status* +wechselt man oben zwischen den Druckern, ein Tipp auf einen Slot zeigt Details und die Aktionen. Hell/Dunkel folgt +der Systemeinstellung. Offline-Modus und Push-Mitteilungen gibt es nicht (dafür bräuchte es HTTPS). + ### Umgebungsvariablen Siehe [`.env.example`](.env.example). `PRINTER_HOST`, `SSH_PORT`, `SSH_USER`, `CC2_URL`, `SPOOLMAN_URL`, diff --git a/app/bridge.py b/app/bridge.py index 3ee5146..a1f1586 100644 --- a/app/bridge.py +++ b/app/bridge.py @@ -198,6 +198,7 @@ class PrinterWorker: self.tray_status: dict[int, int] = {} self.record: dict | None = None self.cc2_data: dict | None = None + self.print_info: dict | None = None # CC2 Control /api/printer, only read while a job runs @property def id(self) -> str: @@ -250,6 +251,12 @@ class PrinterWorker: self._error("cc2", None) except CC2Error as e: self._error("cc2", str(e)) + self.print_info = None + if self.cc2_data is not None and (self.cc2_data.get("job") or {}).get("active"): + try: + self.print_info = self.cc2.printer() + except CC2Error: + pass # progress is only shown in the UI; the spool list above is what counts if self.record is not None: self._process_record(self.record) self._process_scans() @@ -693,9 +700,31 @@ class PrinterWorker: "cc2_enabled": None if cc2 is None else bool(cc2.get("enabled")), "job_active": None if cc2 is None else bool((cc2.get("job") or {}).get("active")), "host_key": self.bridge.keys.host_key(self.id) is not None, + "job": self._job_summary(), "slots": slots, } + def _job_summary(self) -> dict | None: + job = (self.cc2_data or {}).get("job") or {} + if not job.get("active"): + return None + used = [s for s in job.get("slots") or [] if isinstance(s, dict)] + info = self.print_info or {} + pr, machine = info.get("print") or {}, info.get("machine") or {} + progress = machine.get("progress") + return { + "file": str(job.get("file") or pr.get("filename") or "").rsplit("/", 1)[-1], + "started": job.get("started"), + "grams": round(sum(_num(s.get("grams")) for s in used), 2), + "slots": [{"slot": i, "grams": round(_num(s.get("grams")), 2)} for i, s in enumerate(used) + if _num(s.get("grams")) > 0], + "progress": progress if isinstance(progress, (int, float)) else None, + "layer": pr.get("current_layer"), + "layers": pr.get("total_layers"), + "remaining": pr.get("remaining"), + "state": pr.get("state"), + } + class Bridge: def __init__(self, data_dir: Path, settings: SettingsStore, keys: KeyStore, *, diff --git a/app/static/icon-180.png b/app/static/icon-180.png new file mode 100644 index 0000000000000000000000000000000000000000..083571468137f619090b00c4cae00c30b6d35ae7 GIT binary patch literal 2768 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 literal 0 HcmV?d00001 diff --git a/app/static/icon-192.png b/app/static/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..c540ee9a0cf4882f06add3dc72f0e62510b059ee GIT binary patch literal 3038 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 cLC6BK-SNixH$m0qIJxfDc7LkRo70 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 UQE4t4>J%9KV{-X}w*BM(1LFHYcK`qY literal 0 HcmV?d00001 diff --git a/app/static/icon.svg b/app/static/icon.svg new file mode 100644 index 0000000..51ed50d --- /dev/null +++ b/app/static/icon.svg @@ -0,0 +1 @@ + diff --git a/app/templates/base.html b/app/templates/base.html index ab8e2cf..6e04714 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -2,98 +2,223 @@ - + {% block title %}CC2 Bridge{% endblock %} + + + + + + + + + + + + + +{% block head %}{% endblock %} -
-

CC2 ↔ Spoolman

-