diff --git a/CHANGELOG.md b/CHANGELOG.md index d062243a1..348a3c349 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,10 @@ All notable changes to Bambuddy will be documented in this file. - **NTP-gate state exposed on the appliance endpoint** — `GET /api/v1/system/appliance` gains a `time_synced` field returning `"ok"`, `"warning"`, or `null`. Source: `/run/bambuddy/time-synced`, written by the appliance's `ntp-gate.sh` once chronyd reports sync (or after a 3-minute timeout with a `"warning"` marker). The RPi 5 has no battery-backed RTC, so on a fresh boot the system clock is wrong until NTP catches up — JWT expiries and TLS certificate validity windows depend on this being right. New `backend/app/core/local_config.py::read_ntp_gate` is defensive on every failure mode (file absent → `None`, OSError → `None` + warning log, empty / unknown content → `None`, binary garbage survives via `errors="replace"`). The endpoint stays no-auth; the SPA can use the field to render a "time not synced" badge on a fresh appliance before swapping to normal status once `"ok"` comes through. 8 new unit cases for `read_ntp_gate` (absent / ok / warning-suffixed / warning-only / empty / unknown-marker / leading-whitespace / binary-garbage) and 3 new integration cases for the endpoint field (ok / warning / absent). On Docker / manual installs the gate file doesn't exist so this is a no-op (`time_synced` is `null`) — the appliance is the only consumer for now. - **Appliance locale defaults endpoint** — `GET /api/v1/system/appliance` returns the hostname/timezone/locale the Bambuddy Appliance setup wizard collects into `/etc/bambuddy/local.toml` during firstboot. New `backend/app/core/local_config.py::read_local_toml` parses the file defensively (missing file → empty dict, invalid TOML → empty dict + warning, non-string values dropped with a warning), so a malformed file never blocks startup. Endpoint returns `{hostname, timezone, locale}` with `null` for any field not present, requires no auth (the frontend i18n bootstrap fetches it before auth might be set up, and the contents are user-set defaults, not secrets). On the frontend, `i18n/index.ts` runs a one-shot `applyApplianceLocale()` hook after init: gated by a `bambuddy_appliance_locale_consumed` localStorage flag so it runs exactly once per appliance, fetches the endpoint, and `i18n.changeLanguage(...)`s if the returned locale is in the supported set. Non-appliance installs (Docker, manual) silently no-op when the file or endpoint is absent. The appliance writes the file via its setup wizard (separate repo: `bambuddy-appliance`); this PR closes the loop for the locale field — hostname and timezone are still applied by the appliance's firstboot.sh via `hostnamectl`/`timedatectl` and don't need a main-app reader. Backend test coverage: 9 unit cases for the reader (missing/empty/comment-only/full/partial/invalid/non-string/unknown-keys/escaped-quotes), 4 integration cases for the endpoint (nulls when no file, full values, partial values, no-auth-required). +- **Unified print dispatch through the queue scheduler (#1625, by @EdwardChamberlain)** — Every print Bambuddy starts now goes through the print queue's scheduler rather than the standalone `background_dispatch.py` path that previously ran in parallel for File Manager prints, archive reprints, and printer-card upload-and-print. Same end-state (a print on the printer), one code path. **Effect on users:** every print is now queueable, cancellable, visible on the queue page, attributable to the user that started it, and runs through the existing filament-deficit check and print-queue ownership model. The "stealth print" that didn't show up in the queue because it bypassed the scheduler is gone. **Architecture.** File Manager **Print**, archive **Reprint**, and printer-card **upload-and-print** all now POST to the existing queue routes (`POST /api/v1/queue/items` with an immediate ASAP scheduled_time) — the scheduler picks it up on the next tick and runs the same dispatch path the existing queue used to. The retired `background_dispatch.py` route + `services/background_dispatch.py` worker + their two test files are removed. The scheduler already had every feature `background_dispatch` did (per-printer locking, status broadcast, error path) plus the deficit / ownership / queue-position machinery, so this is consolidation rather than a rewrite. **Permission scope changes.** Documented in the Security section below (#1625 introduced the `queue:create` requirement on File Manager / archive reprint / upload-and-print). **i18n.** New `queue.actions.startPrint` key added across all 11 locales (the FileManagerPage button's accessible-name on the new path). Parity check holds. **Tests.** All `background_dispatch` test files removed (the routes they covered no longer exist); `test_dispatch_force_timelapse.py`, `test_scheduler_force_timelapse_wiring.py`, and `test_cleanup_forced_timelapse.py` consolidated onto the scheduler since `force_timelapse` now lives there exclusively. **Followup #1625-followup (this drop, listed under Fixed below)** caught three issues from the post-merge audit — ownership gate mismatch on `/queue/{id}/start` and `/queue/{id}/stop`, an ASAP TOCTOU race on empty-scope inserts, and a missing duplicate-position validator on `/queue/reorder` — none of which were introduced by this PR but all of which became more impactful once every print routed through the queue. **Scope.** No DB migration. No new permission (`queue:create` already existed; this PR widens its surface). No frontend behaviour change for users with full permissions — the queue surface absorbs prints that previously skipped it. + +- **HMS error actions — Resume / Stop / Check Assistant from the dashboard (#1743, by @Ichicoro, requested in #1419 by @Ichicoro)** — Bambu's HMS error dialog goes from read-only to actionable. The error modal on the printer card now renders the same Resume / Stop / Continue / Retry / Check Assistant / Don't Remind Me etc. buttons that BambuStudio and Bambu Handy show, and each click sends the matching MQTT command back to the printer. Closes the long-standing UX gap that forced users to physically walk to the printer (or open Bambu Handy) just to acknowledge a paused print. **Data source.** A bundled `backend/app/data/hms_actions.json` maps every known printer-model + error code to its list of allowable actions; populated from Bambu's public `e.bambulab.com/hms/GetActionImage.php` endpoint via `scripts/update_hms_actions.py`. The action-ID-to-name mapping (RESUME_PRINTING, CHECK_ASSISTANT, FILAMENT_EXTRUDED, …) matches BambuStudio's open-source enum verbatim — including the `CANCLE` typo, kept on purpose because Bambu's catalog spells it that way and silently fixing it would break the lookup. **Backend.** New `backend/app/services/hms_actions.py` defines an `HMSAction` `StrEnum` and `get_actions_for_error_code(device, error_code)` lookup; loaded once at module import via `Path(__file__).resolve().parent.parent / "data" / ...` so the JSON resolves regardless of CWD (systemd unit, Docker entrypoint, pytest from `backend/`). `BambuMQTTClient._parse_data` looks up the action list at HMS-parse time on both error sources — the structured `hms[]` branch and the per-print `print_error` short-code branch — and attaches it to `HMSError.actions` together with a `job_id` snapshot from `self.state.subtask_id` so the action survives a subsequent job change. New `POST /api/v1/printers/{id}/hms/execute-action` route (`HmsActionBody` schema; permission `PRINTERS_CONTROL`) dispatches the click. **Dispatcher (`BambuMQTTClient.execute_hms_action`).** A `match` statement maps each `HMSAction` to its MQTT command — `resume` / `stop` with `err`+`param=reserve`+`job_id` for the HMS-aware actions; `idle_ignore` with `type=0` (one-time) vs `type=1` (persistent) so Bambu's "Don't Remind Me" / "No Reminder Next Time" actually disable the warning across prints; `ams_control` with `param=done` / `resume` / `abort` for filament-load dialogs; bare `clean_print_error` (matches the existing `clear_hms_errors` shape — no leaked `print_error` body field); `clean_print_error` + `uiop` chained for `DBL_CHECK_OK`; `refresh_nozzle`, `buzzer_ctrl mode=0` (fire alarm), `auto_stop_ams_dry`, `close_air_filt` for the standalone actions. UI-only actions (`CHECK_ASSISTANT`, `JUMP_TO_LIVEVIEW`, `OK_JUMP_RACK`, `REMOVE_CLOSE_BTN`, `LOAD_VIRTUAL_TRAY`, `CANCLE`, `DBL_CHECK_CANCEL`) intentionally publish nothing — they exist for label parity with BambuStudio's modal where the printer's own screen drives them. Unknown actions fall through to `return False` + warn log so the route surfaces them as 4xx rather than silently no-opping. Every command pairs with a `pushing.pushall` echo so the state stream refreshes on the next tick and the modal closes correctly. **Schema hardening.** `HmsActionBody.print_error` validated as `min/max_length=8` + `pattern=r"^[0-9A-Fa-f]{8}$"`; `action` and `job_id` length-capped. Stray input can't reach the dispatcher's `match`. **Frontend.** `HMSErrorModal.tsx` renders a wrap-flex row of buttons under each error description, sized to fit on the printer-card panel without overflowing on narrow viewports. The mutation calls the new endpoint, invalidates the printerStatus query, and shows the new `hmsErrors.actionSuccess` / `hmsErrors.actionFailed` toast. The button label is the translated action name from `hmsErrors.actions.` — never the raw enum — so a forgotten translation falls back to the English action name rather than a key string. **i18n.** 33 action labels + 2 toast keys translated in all 11 locales (`de / en / es / fr / it / ja / ko / pt-BR / tr / zh-CN / zh-TW`). No English fallback per [[feedback_translate_dont_fallback]]. Parity check 5381 leaves per locale. **Tests.** 31 new cases in `test_hms_actions.py` — 5 catalog-lookup (known A1 error returns actions; unknown device → empty; unknown error → empty; underscore-form `0300_8070` doesn't match the catalog's no-separator key; enum StrEnum value contract holds including the `CANCLE` typo) and 26 dispatcher cases that pin every `HMSAction` branch to its exact MQTT payload — resume carries err+param+job_id; `IGNORE_RESUME` / `NO_REMINDER_NEXT_TIME` use `idle_ignore type=0`; `IGNORE_NO_REMINDER_NEXT_TIME` / `DONT_REMIND_NEXT_TIME` use `type=1` (persistent variants — were incorrectly bucketed together in an earlier draft); `clean_print_error` body is bare; `DBL_CHECK_OK` chains clean + uiop_close; uiop's `err` is the already-string short code (not `f"{x:08X}"` against a str, which would TypeError); plain resume vs HMS-aware resume distinguished; UI-only actions publish nothing; unknown action returns False; `pushing.pushall` echo fires after every command. Full `pytest -n 30` green (6471/6471). Backend `ruff` clean. Frontend `npm run build` clean. **Scope.** No DB migration. No new permission. The HMS modal is opt-in by user click — installs with no HMS errors see no behaviour change. The 9009-line catalog is shipped as a single static JSON; regenerating it later is a `python scripts/update_hms_actions.py` run away. + ### Security - **Print permission scope change for queue-only dispatch** — All UI print-entry points now route through the print queue instead of direct background dispatch, which changes the permissions needed for some actions. File Manager **Print** now requires `queue:create` (previously `printers:control`). Printer-card upload-and-print now requires both `library:upload` and `queue:create`. Archive reprint buttons now require `queue:create` plus the existing archive reprint ownership permission (`archives:reprint_own` or `archives:reprint_all`). Installations with custom groups/API keys that granted `printers:control` for immediate printing but did not grant `queue:create` must add `queue:create` to keep those print actions available. Grant `queue:create` carefully: ASAP queue items are eligible for immediate dispatch, so it is now the permission that authorizes starting queued prints from File Manager, Archives, and upload-and-print flows. - **Vite 7 → 8 major bump** — Bambuddy's frontend now builds with Vite 8 (`^7.3.2` → `^8.0.16`) and the matching plugin-react release (`@vitejs/plugin-react` `^5.1.1` → `^5.2.0`). Headline architectural change: Vite 8 swaps Rollup for **Rolldown** as the default bundler — same plugin contract, Rust-backed core, slightly different chunk layout / output bytes (no functional regression). The bump also lifts the transitive `esbuild` floor to 0.28.1, which closes the last open advisory in the audit chain. **Bambuddy-side surface audited:** `vite.config.ts` uses only stable contracts that survived the v8 cut — `defineConfig`, the `Connect` type, the custom `serveGcodeViewer` `configureServer` middleware plugin (proxies `/gcode-viewer/*` to the repo's sibling `gcode_viewer/` directory in dev), the `server.proxy` with WebSocket upgrade for `/api/v1/ws`, `build.outDir`/`emptyOutDir`/`chunkSizeWarningLimit`, and `resolve.alias` for `@`. `base: '/'` regression guard from #1221 is unaffected. No SSR, no library mode, no CSS preprocessors, no exotic plugins. `vitest@4.1.8` already accepts vite 8 in its peer range (`^6 || ^7 || ^8`); no test-runner bump required. **Node:** vite 8 requires `^20.19.0 || >=22.12.0`; CI Node 20.x line satisfies this. **What this is NOT:** plugin-react v6 — that line requires `babel-plugin-react-compiler` + `@rolldown/plugin-babel` as peers and is a separate scope. `npm run build`, `npm run lint`, `npx vitest run` all clean; `npm audit` clean.