Skip to content

fix: healthcheck no longer requires curl#12

Open
koen01 wants to merge 140 commits into
mainfrom
fix/healthcheck-no-curl
Open

fix: healthcheck no longer requires curl#12
koen01 wants to merge 140 commits into
mainfrom
fix/healthcheck-no-curl

Conversation

@koen01

@koen01 koen01 commented Jun 26, 2026

Copy link
Copy Markdown
Owner

Problem: The healthcheck in docker-compose.yml used curl, but the python:3.12-slim base image does not include curl. This caused the healthcheck to always report unhealthy.

Solution: Changed the healthcheck to use Python's built-in urllib.request module instead of curl.

Fixes #11

koen01 and others added 30 commits February 28, 2026 14:58
- Drop slot_history, spool_ref_*, spool_epoch_consumed_g_total,
  remaining_g, last_accounted_* from schema and state
- Remove _hist_push, _hist_upsert_by_src, _inc_slot_epoch_consumed,
  _apply_job_usage, _slot_consumed_g_epoch helpers
- Poll loop finalization now only calls _spoolman_report_usage per slot
- Remove POST /api/spool/reset, /apply_usage, /ui/slot/reset,
  /ui/spool/set_remaining endpoints
- Add idempotency guard to /api/moonraker/allocate (prevents double
  Spoolman sync); stored value drops alloc_g
- Add GET /api/ui/spoolman/spool_detail?slot= proxy endpoint (graceful
  error, never HTTP 502)
- Replace renderHistory() with fetchAndRenderSpoolmanStatus() in UI;
  right-side panel now shows live Spoolman spool status for active slot
- Remove Istgewicht/Übernehmen form from spool modal; roll-change
  increments epoch and auto-unlinks Spoolman spool
- Update i18n keys: remove history/spool-stats keys, add spoolman
  status keys (DE + EN)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add websockets>=12.0 dependency
- Remove all Moonraker polling code (~490 lines): _moonraker_fetch_history,
  _moonraker_build_url, _moonraker_list_objects, _extract_cfs_slot_data,
  _walk, moonraker_poll_loop
- Add _printer_ws_url, _normalize_ws_color, _parse_ws_cfs_data,
  _ws_connect_and_run, printer_ws_loop (exponential backoff 2→60s)
- Config: printer_url replaces moonraker_url; backward compat migrates
  hostname from old moonraker_url automatically
- Spoolman usage reported incrementally via usedMaterialLength deltas
  (m → mm → g via mm_to_g); roll change clears ws_slot_length_m baseline
- Remove moonraker_history, moonraker_allocations, cfs_raw, job_track_*,
  current_job* from AppState; add ws_slot_length_m
- Remove allocation + job set/update endpoints
- Frontend: remove renderMoonHistory, captureUiState/restoreUiState,
  buildSlotIds, jobKeyFromMoon; add spoolPct percent badge on slot tiles
- i18n: remove moon.* and assign.* keys; add slot.percent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The printer pushes an unsolicited JSON status message immediately on
connect. This caused _ws_connect_and_run to receive the status dump
instead of the heartbeat "ok" reply, crashing the loop.

Fix: drain all messages (1.5s timeout loop) after connecting, then
send the heartbeat. Also downgrade unexpected heartbeat replies from
errors to warnings so a timing quirk doesn't drop the connection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WS fix: disable websockets keepalive pings (ping_interval=None,
ping_timeout=None) — the printer doesn't respond to WebSocket ping
frames, causing the library to drop the connection after ~20s.

Slot chip display:
- Line 2 (.slotSub): shows "{manufacturer} {name}" when CFS/Spoolman
  data is available; falls back to "MATERIAL · #COLOR" otherwise
- Line 3 (.slotDetail): shows material type + "SP #{id}" when a
  Spoolman spool is linked
- Add .slotDetail and .spoolPct CSS rules

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On manual spool link: write the slot's CFS RFID to the Spoolman spool's
extra.cfs_rfid field via PATCH.

On each WS snapshot: if an RFID-tagged spool (state==2) appears on an
unlinked slot and the RFID is new since last seen, search Spoolman for a
spool with matching extra.cfs_rfid and auto-link it.

On roll change: clear the RFID cache for the slot so re-inserting any
spool (even the same one) triggers auto-link again.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If the CFS RFID changes on a slot that already has a Spoolman link,
treat it as an implicit spool swap: clear the old spoolman_id and
ws_slot_length_m baseline, then try to auto-link to the new spool via
extra.cfs_rfid — same as if the slot had been unlinked first.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously cfs_active_slot was only written when selected==1 was found,
so an old value (e.g. 2A from a previous session) would persist
indefinitely. Always write the value — None when the printer is idle —
so the Active section stays blank when nothing is printing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Spoolman requires extra field values to be double-encoded — the value
itself must be a JSON string. Sending "06001" caused a 400 Bad Request;
the correct form is json.dumps("06001") → "\"06001\"".

Also decode the stored value (json.loads) when comparing RFID during
auto-link lookup so the match works correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The printer never sets selected==1 on any slot (all 0 in WS data), so
cfs_active_slot is always null. The JS was falling back to state.active_slot
which defaulted to "2A" from the Pydantic schema, permanently showing
"Box 2 · Slot A" as active.

- JS: remove || state.active_slot fallbacks in render() and
  fetchAndRenderSpoolmanStatus() — cfs_active_slot is the only source
- Schema: AppState.active_slot default changed from "2A" to None
- Migration: clear existing "2A" values from state.json on load

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… cases

Optional[Literal[...]] = None can behave unexpectedly across Pydantic
versions. active_slot is now driven by WS only and not used by the
frontend, so the Literal constraint adds no value.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drain loop: replaced per-message 1.5s timeout with a 2-second total
deadline. The printer sends status messages continuously, so the old
loop never timed out and the WS was stuck in drain forever, never
reaching printer_connected = True.

State protection: load_state() now sets _state_load_failed when it
falls back to default_state(). save_state() checks this flag and
refuses to write, preventing a failed load from wiping real spool data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace WS-delta Spoolman reporting with Moonraker-driven end-of-job
  attribution: snapshot ws_slot_length_m at job start, then at job
  complete proportionally split filament_used across linked slots using
  WS deltas, and report to Spoolman via _spoolman_report_usage()
- Add _moonraker_base_url(), _moon_report_job_usage(),
  moonraker_job_poll_loop() to main.py; launch loop in _startup()
- Remove WS Spoolman delta block from _parse_ws_cfs_data() (ws_slot_length_m
  still updated for attribution tracking)
- Delete static/i18n.js; hardcode English throughout app.js and index.html
- Remove language switcher buttons and .langSwitch/.langBtn CSS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace native <select> with a custom scrollable list so each spool
entry can display a colored swatch next to its label. Color comes from
filament.color_hex already returned by /api/ui/spoolman/spools.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace flat list cards with a horizontal row per box: narrow box
header on the left (number + temp/humidity), then 4 slot pods showing
a colored SVG spool disk, material name, percent, and edit/active icon.
Empty slots render as a dark disk with a diagonal slash. Active slot
gets a green border and eye icon. All existing click/modal functionality
preserved.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WS fix: replace drain+request/response pattern with a continuous read
loop. Old code drained 2s of real CFS data, then only read ONE message
per request cycle — causing "ok" heartbeat acks to land as the "response"
and parse-fail, while the actual boxsInfo reply went unread. New loop:
- Minimal drain (5 msgs × 0.15s) so real data is not lost
- Heartbeat handshake scans up to 10 messages for the "ok" ack
- Continuous recv() loop processes every incoming message in order
- "ok" and "heart_beat" strings handled inline, not mistaken for JSON
- Re-requests boxsInfo every 5s; re-requests on 6s recv timeout

Branding: rename to CFSync, add Creality triangle logo (logo.png),
update page title and header. FastAPI title updated to match.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add printer_name and printer_firmware fields to AppState
- Add _parse_ws_printer_info(): tries common Creality WS key names
  (machineName, softVersion, etc.) and persists any found values;
  also logs all newly-seen top-level message keys once per session
  so the exact field names can be identified from logs
- Call _parse_ws_printer_info() for every parsed JSON WS message
- Header subtitle (#printerSubtitle) updated dynamically by render()
  to show "PrinterName · FirmwareVersion"; empty until WS delivers data

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
RFID slots (state=2) already report a real sensor percent from the WS.
Manual slots (state=1) always report 100% from the WS (no sensor),
which is misleading. Fix:

- state=2: use WS percent as-is (unchanged)
- state=1 + Spoolman link: calculate remaining/nominal*100 from
  Spoolman filament.weight; falls back to remaining/(remaining+used)
  if nominal weight is not set; stored in _spoolman_manual_pct cache
- state=1 without link: show no percent (None)
- state=0 (empty): show no percent

_refresh_manual_slot_pcts() is an async task triggered after each
boxsInfo parse; per-slot TTL of 60s prevents hammering Spoolman.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CFSync now generates a bookmarklet (footer "Fluidd panel" link) that,
when clicked in Fluidd, replaces the Runout Sensors card with a live
CFS slot grid pulled from CFSync's /api/ui/state API.

- static/fluidd-panel.js: self-contained IIFE; finds Vuetify card by
  title text (Runout Sensors / Filament Sensor), replaces content with
  colour-coded slot grid; falls back to floating panel after 15 s;
  polls every 3 s; handles both Vuetify v2 and v3 DOM structures
- static/app.js: initFluiddBookmarklet() generates javascript: href
  with CFSYNC_URL embedded from window.location.origin; copy button
  fallback via navigator.clipboard
- static/index.html: footer bookmarklet link + copy button
- static/style.css: footer link / copy button styles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two fixes for the 'no WS slot deltas to attribute' failure:

1. Re-snapshot on next tick if WS was empty at job start
   When the service starts while a print is already running, the
   Moonraker poll may take the ws_slot_length_m snapshot before the
   WebSocket loop has received any boxsInfo data. Now we set
   _moon_snapshot_pending=True in that case and retry the snapshot
   on each subsequent 5 s tick until WS data arrives.

2. Active slot fallback when no WS deltas at completion
   If deltas are still zero at job end (snapshot was empty or WS
   never updated usedMaterialLength), fall back to attributing the
   full filament_used to the active CFS slot (cfs_active_slot /
   active_slot) if it has a Spoolman link. This covers the common
   single-spool restart-mid-print scenario.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Fluidd panel bookmarklet fetches from CFSync (different host/port)
so the browser blocks it without an Access-Control-Allow-Origin header.
Allow all origins since CFSync is a local-network-only service.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
rsync --delete was wiping the venv/ directory because it was not in
the exclude list. Then pip ran against the system Python which is
PEP 668 externally-managed and rejects installs without a venv.

- Add --exclude "venv/" to rsync so the existing venv is preserved
- Recreate venv with python3 -m venv if it is missing (safety net)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
--exclude "venv/" is supposed to protect the venv from rsync --delete,
but rsync still descends into the excluded directory and deletes the
individual files inside it (only the empty directory shells remain),
leaving venv/bin/activate missing and the pip step broken.

An update script only needs to overwrite/add files — removing --delete
is the correct fix. Files deleted from the repo accumulate on existing
installs but that is harmless for this app.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Empty slots (present=false) have no filament so showing a material
type or percentage carried over from previous state is misleading.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
koen01 and others added 29 commits March 30, 2026 07:24
Track open/closed state per printer in a module-level Set (cameraOpen)
that persists across render() calls. On rebuild, restore the stream
state and re-set img.src so the feed continues uninterrupted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace wrap.innerHTML="" with a reconciler that keeps live DOM nodes:

- _renderedPrinters Map: tracks each printer block + structural fingerprint
- _renderedJobsCard: tracks recent jobs card + history fingerprint
- _printerStructFingerprint: captures slot states, active slot, box metadata
  (excludes temps/progress so normal polling never triggers a rebuild)
- _jobsFingerprint: captures job history timestamps

On each tick:
  - If structural fingerprint unchanged → _patchPrinterBlock() updates
    badges, temps, and progress bar in-place via data-live attributes
  - If fingerprint changed → replaceChild() for that printer only
  - Recent jobs card only rebuilt when job history actually changes

This eliminates scroll jumps, stops the camera stream from being
interrupted on every refresh, and reduces DOM work to a few text
and style updates per tick in steady state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
During multi-color prints the firmware briefly sends selected=0 for all CFS
slots while switching, which was triggering the SP fallback and calling
SET_ACTIVE_SPOOL with SP's linked spool ID.

- _parse_boxs_info: only call SET_ACTIVE_SPOOL when firmware explicitly
  selects a CFS slot (active_slot non-None); never use SP as a fallback here
- moonraker_job_poll_loop: at job start, if tracking slot is SP, activate
  SP's linked spool — this is the only place SP activation now happens

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Full style.css rewrite with MD3 color token system (tonal surfaces,
  green/teal primary palette, shape scale, state layers)
- Roboto + Roboto Mono fonts loaded via Google Fonts
- Spool SVG hardcoded colors updated to match new green-tinted surfaces

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Left navigation drawer (hamburger + gear icon) holds Settings and
  Completed Jobs, moving both off the main page
- Camera card now uses a fixed 16:9 placeholder so show/hide no longer
  causes any layout reflow in the printer block
- Completed Jobs rendered into the drawer instead of the main scroll
  area, keeping the printer block height stable across ticks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Nav drawer now contains three destination items with Material Symbols
  icons: Dashboard, Completed Jobs, Settings
- Each destination is a full page rendered in <main>; navigateTo()
  switches pages, updates active indicator, and closes the drawer
- Gear icon in topbar goes directly to Settings page (no drawer)
- Settings form and Completed Jobs list are no longer inline in the
  drawer; the drawer is purely navigation
- Page fade-in animation, MD3 tonal active indicator on nav items

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Status/stats are CFS-related, so they belong in the left column
alongside the box grid and active slot. Right column is now purely
printer hardware: temps, progress, and camera.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- isCameraEnabled/setCameraEnabled helpers persist state in localStorage
- Settings page gets a Cameras card listing all printers with a webcam
  URL, each with an MD3 switch toggle
- Camera card respects the toggle: when disabled it shows a compact
  one-line hint instead of reserving the full 16:9 space
- Toggling rebuilds the affected printer block on the next tick

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace dark surface tokens with light-mode palette: white/grey surfaces,
dark-green primary (#006B4F), light primary container (#89F8C7). Update all
hardcoded rgba color values throughout style.css and spool SVG fill colours
in app.js to match the new light palette.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drop --md-bg from #F5FBF5 to #B8C4BA so the page background is clearly
distinct from the white/light-grey card surfaces.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MD3 Expressive relies on spacing and typography hierarchy instead of
hairline separators. Remove border-bottom rules from cardHead, spoolStatRow,
cfsBox, spoolStatusFilament, settingsCardHead, modalHead, and navDrawerHead.
Settings items within cards retain their separators for form legibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace #89F8C7 / rgba(137,248,199) with #A3CEBA / rgba(163,206,186) —
same hue, much lower saturation, so active chips/nav items/buttons read
as green without the neon quality.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three new breakpoints plus modifications to the existing 600px one:

768px — topbar wraps to two rows, topbarRight fills second row,
  container/card padding tightens, brand + logo shrink slightly.

480px — subtitle hidden, refresh label hidden, spoolman link hidden,
  topbar badges compact; printerHead stacks vertically; CFS box header
  narrows to 40px with vertical-rl text, spool pods shrink to 44px;
  stats grid columns tighten; jobs spool rows wrap.

360px — grams column hidden in stats table to preserve layout on the
  smallest Android screens.

JS: badge text shortened at <480px ("Printer: connected" → "Connected",
  "Printers: 2/2 online" → "2/2", "CFS: 1 detected" → "CFS: 1").

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MD3-compliant dark colour tokens (green/teal primary) via [data-theme="dark"]
  on <html>, overriding all surface/primary/error/warning CSS variables
- Targeted overrides for hardcoded rgba() colours (badge states, slot outlines,
  hover inks, modal backdrops, spoolman list selection, etc.)
- Inline <script> in <head> applies saved theme before first paint (no FOUC);
  defaults to system prefers-color-scheme if no manual preference is stored
- Appearance card in Settings page with Dark mode mdSwitch toggle
- Toggle wired in _initSettingsHandlers(); preference persisted in localStorage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The struct fingerprint only covered cfs_slots (WS data), so changes to
local slot fields (spoolman_id, material, name, color) never triggered a
DOM rebuild. After unlinking, tick() fetched fresh state but the stale
slot pods remained, making the UI appear unresponsive to unlink actions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of closing the modal on unlink, switch it to the "not linked"
state in-place and load the Spoolman dropdown so the user can
immediately select a replacement spool.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Surface API errors via alert instead of silently doing nothing,
which should expose why unlink fails on the spool holder modal.
Also captures slot/printer IDs before awaits and awaits the
dropdown fetch before tick() to avoid any race conditions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_moonraker_set_active_spool used asyncio.ensure_future which requires
a running event loop in the current thread.  Sync FastAPI endpoints run
in an AnyIO worker thread that has no event loop, so unlinking the SP
slot (which triggers CLEAR_ACTIVE_SPOOL in moonraker mode) crashed with
RuntimeError: There is no current event loop in thread.

Replace the async wrapper + ensure_future with a plain daemon thread.
_moonraker_send_gcode is already a blocking HTTP call, so no executor
indirection is needed — a thread achieves the same fire-and-forget goal
while working from both async and sync call sites.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Creality shares the same RFID material-type code across all spools of the
same filament type, making cfs_rfid matching unreliable when multiple spools
of the same type are in use. SSH serialNum lookup is spool-unique and is the
only safe auto-link method.

- Remove _spoolman_autolink_by_rfid and _spoolman_set_extra (dead code)
- WS handler now only uses RFID-change detection to unlink on spool swap
- SSH-based _ssh_fetch_and_apply flow unchanged

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Document why RFID material codes are not used (shared per filament type),
explain the SSH serialNum mechanism, add auto-unlink conditions, and
clarify the full CFTag → Spoolman → auto-link workflow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each box (Box 1–4) and the Spool holder are now proper Material cards
with a cardHead (title + active slot badge + env sensor chips) and the
four slot pods arranged in a horizontal row as the card body — matching
the physical left-to-right order of rolls in the box.

- Remove .boxRow / .boxHeader sidebar layout in favour of .card structure
- Active slot indicator shows "Slot A active" badge in the card header
- Spool card shows "Active" badge when the holder is the active input
- Env chips move from sidebar column into the card header meta row
- Responsive overrides updated; dead .boxHeader CSS removed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
print_stats.filament_used is 0 during homing, probing and bed mesh —
it only rises when actual extrusion happens. Expose it as
moon_filament_used_mm in the live status and gate the SP-active logic
on it so the spool holder card no longer shows the active highlight
until the printer is genuinely extruding filament.

This also prevents purge moves at the very start of a print from being
attributed to the spool holder before the CFS has selected a slot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces direct write_text with a write-to-tmp + rename pattern.
rename(2) is atomic on Linux, eliminating the window where a crash
or concurrent write could produce a truncated or double-JSON file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Docker and Docker compose deployment
The healthcheck used curl which is not installed in the python:3.12-slim base image.
Using Python's built-in urllib.request instead to check the health endpoint.
@FUCHS-HB

Copy link
Copy Markdown

I edited the docker-compose health check command to your new change and it works. Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants