Commit Graph

93 Commits

Author SHA1 Message Date
1c18a85021 refactor(bridge): extract SpoolmanMixin (stage 3, mixin 1/6) 2026-08-04 20:10:48 +02:00
ebb48aac82 refactor(bridge): extract printer credential fetch/decrypt into credentials.py (stage 2)
Moves the coherent printer-credential unit (_kx_generate_signature,
_kx_decrypt_info, _kx_fetch_credentials plus the pycryptodome import guard)
out of the facade into credentials.py, re-exported for the "add printer"
flow. The remaining free functions (build_app, main, _build_per_printer_args,
_default_data_dir, _mqtt_error_msg) stay in the facade - they're entrypoint
logic or travel more naturally with the core mixin in the next stage.

All 184 tests green, no behavior change.
2026-08-04 19:43:25 +02:00
cdf11f6bfe refactor(bridge): extract self-contained classes/helpers into modules (stage 1)
First stage of splitting the 6368-line kobrax_moonraker_bridge.py monolith.
The low-coupling, self-contained pieces move into their own modules;
kobrax_moonraker_bridge.py re-exports them so every caller (12 test files,
the PyInstaller spec) keeps working unchanged - not a single test or the
spec needed editing.

Extracted:
- spoolman_client.py  <- SpoolmanClient (zero coupling)
- gcode_store.py      <- GCodeStore (stdlib only)
- gcode_meta.py       <- _parse_gcode_*/_extract_* metadata helpers
- camera.py           <- CameraCache + _find_ffmpeg
- bridge_logging.py   <- _BrowserLogHandler, the log ring buffer + SSE
                         queues, _set_verbose_http_log (the shared mutable
                         buffer/queues are re-imported so the log endpoints
                         still operate on the same objects the handler writes)

Facade shrinks from 6368 to 5566 lines. All 184 tests green after each
extraction. Verified the re-exported names resolve and the shared log
objects are identical by reference across modules. No behavior change.
2026-08-04 19:41:17 +02:00
5eeed97514 fix(bridge): camera process-race and unhandled JSON errors in two handlers
Found during a targeted code review, not from a user report:

- _run_jpeg_loop()/_run_h264_loop() operated directly on the shared
  self._proc_jpeg/self._proc_h264 instance attributes in their cleanup,
  unlike _run_mjpeg_loop() (already fixed for exactly this) which uses a
  local `proc` reference. If a loop's task is cancelled - e.g. by
  CameraCache.reset() after the printer rotates its stream URL on reboot -
  while a new task has already started and assigned its own process to the
  shared attribute, the cancelled task's cleanup killed the NEWER process
  instead of its own, leaking its own ffmpeg child as an orphan. Applied
  the same local-variable + identity-check pattern already used by
  _run_mjpeg_loop.

- handle_api_settings_post and handle_api_update_apply were the only two
  of ~84 handlers that called `await request.json()` without a try/except -
  every other handler follows the established pattern of returning a clean
  400 for a malformed body. A trivial malformed request to either endpoint
  produced an unhandled 500 with a full traceback instead.

New tests in tests/test_camera_process_race.py,
tests/test_settings.py, and tests/test_update_check.py cover both.
2026-08-04 15:31:36 +02:00
5a44d0abab feat(print): add option to delete file from printer after successful print
Frees up the printer's own limited storage automatically once a print
finishes, while keeping the file safely in the bridge's own GCode store.

Deliberately scoped to files uploaded through the bridge itself only
(matched via the GCode store, same lookup the job-history feature already
uses) - a print started directly from the printer or Anycubic Slicer has
no backup anywhere else, so it's never touched regardless of the setting.
Only triggers on a clean "finished" state, not on stopped/canceled prints,
since the user may want to retry those.

Off by default. The delete request is fire-and-forget, sent directly from
_on_print() (which runs on the MQTT reader thread) rather than through the
existing _wait_for_file_action() helper - that helper blocks waiting for a
reply dispatched from that same thread, which would deadlock if called
from within it.
2026-08-03 13:12:47 +02:00
23e3831232 fix(filament): wire ACE-RFID auto-matching into the real MQTT status path (Issue #101)
_parse_combined_rfid_type()/_match_profile_by_vendor_family() (added in an
earlier nightly) were only ever invoked inside _build_lane_data(), which is
only reached when OrcaSlicer actively polls the Moonraker lane_data endpoint.
The actual MQTT receive path (_on_multicolor_box -> self._ams_slots ->
_push_status_update -> dashboard, and the /kx/filament/slots dashboard API,
and Happy-Hare gate data) never touched this logic at all, so a combined
RFID string like "GEEETECH PLA Bas" kept showing up unmatched everywhere
except the one endpoint nobody was looking at - exactly what @Blaim's
extensive debugging in the issue thread demonstrated.

Centralized the matching into _effective_slot_profile() itself, since all
three real consumers already call it. This fixes the dashboard and gate
data with a single change instead of duplicating the matching logic per
caller (which is what caused the gap in the first place).

While centralizing, found and fixed a related edge case: the stale-profile
guard compared a manual override's material family directly against the
raw combined RFID string, which _material_family() can't parse past the
vendor prefix - a valid manual override on an RFID-tagged slot would have
been incorrectly treated as stale and dropped. Now resolves the plain
material family through the RFID parser first for that comparison.

Also added variant-token disambiguation to _match_profile_by_vendor_family()
per @Blaim's follow-up request: when a vendor has multiple profiles of the
same material family (e.g. "Geeetech PLA Basic" vs. "Geeetech PLA Matte"),
the RFID string's truncated third token ("Bas") is now scored against
candidate profile names instead of always picking the first match.

New tests cover the actual runtime path end-to-end (a realistic
multiColorBox/report payload through _on_multicolor_box, verified against
the /kx/filament/slots response the dashboard consumes) - the previous
test suite only exercised the helper functions and _build_lane_data() in
isolation with hand-set state, which is how this gap went unnoticed.
2026-08-03 12:33:47 +02:00
ecc53cd7cb feat(printer): add smart-plug power switch button (Issue #103); fix(update): stable update check missing behind prereleases (Issue #104)
Issue #103: the printer has no MQTT-level command to power off or enter
standby, so users on a separate-room setup have to physically walk over
or use a smart plug (e.g. Tasmota) manually. Added per-printer
power_on_url/power_off_url/power_status_url config (Settings > Power
Switch) and a power button with a live on/off indicator on each printer's
card in the Printers grid - plain HTTP GET calls, no Moonraker
device_power dependency.

Issue #104: the stable-release update check only ever requested the
single newest Gitea release (limit=1) regardless of type. Since
nightly/dev prereleases publish far more often than stable ones, that
newest release is almost always a prerelease, so the "not a prerelease"
filter found nothing and reported "no stable releases found" even
though a newer stable release existed further back in the list.
Fixed by requesting enough releases (limit=20) to look past a run of
prereleases.

Also fixes a related pre-existing bug surfaced while testing #103's
config: configparser's default string interpolation rejected any
config.ini value containing a literal '%' (ValueError: invalid
interpolation syntax) - this broke saving Tasmota-style power URLs
(cmnd=Power%20on) and would have broken any other value with a '%'
character. Fixed globally with interpolation=None on every
ConfigParser() instantiation in config_loader.py and
kobrax_moonraker_bridge.py.
2026-08-02 15:59:35 +02:00
0a9bf6def6 fix(mqtt): resolve reconnect deadlock after printer disconnect (Issue #105)
The bridge could get permanently stuck after a printer went offline and
came back, even with the printer confirmed reachable via ping/nc - only
a full container restart recovered it. Two compounding bugs:

1. Two independent code paths could trigger _reconnect() concurrently
   with no coordination: the reader thread (on a failed keepalive ping)
   and publish()/publish_web() (on a failed sendall(), which happens
   constantly once the socket is dead, since the poll loop calls
   query_info() every poll_interval). Both would race into their own
   _do_connect(), each opening a competing TLS handshake against a
   printer that likely only accepts one mTLS session at a time - so
   neither converges, and the failure repeats every ~3s instead of
   backing off. Added a lock so a second _reconnect() call waits for
   the first to finish instead of starting a competing handshake.

2. publish() swallows send/reconnect failures internally and returns
   None instead of raising - so _poll_loop's `if info: ...` branch was
   silently skipped on failure, but the surrounding except-block (which
   would have triggered the existing, correct offline/reconnect
   transition) was never reached, since no exception was ever thrown.
   The poll loop had no way to tell "printer sent nothing this tick"
   apart from "the MQTT session is dead". Added client.is_connected()
   and check it explicitly when query_info() returns falsy, routing a
   dead session into the same clean offline branch already used for a
   TCP-unreachable printer.

Verified: bridge continued printing normally throughout (live print
in progress on the real printer during this fix), full test suite
green (111 tests), new tests cover the concurrent-reconnect lock and
the poll-loop offline transition on a swallowed send failure.
2026-08-02 13:15:23 +02:00
36cb97038f feat(moonraker): use buried/report as size/duration/layer fallback (Issue #102)
server/files/metadata returned broken placeholders (size: 1,
estimated_time: null) for any file that wasn't uploaded through the
bridge's own GCode store - e.g. prints started directly from Anycubic
Slicer Next.

Verified live against a real Kobra X (see memory
reference_buried_report_trigger.md) that the printer sends a
previously-unused MQTT topic, buried/report, exactly once per print
start - fires identically whether the print was started via Anycubic
Slicer Next or via OrcaSlicer/the bridge itself. It carries gcode_size,
estimate_duration, and total_layers: precisely the fields the metadata
endpoint was missing.

Add _on_buried() (registered alongside the existing file/report
callback) that caches the single most recent buried/report payload.
_build_file_metadata() now tries this cache - matched by task_name -
as a third fallback, between the existing GCodeStore lookup and the
final size:1 hardcoded placeholder. Ordering is deliberate: live
tracked-job state and the file's own GCodeStore row (if the file was
uploaded through the bridge) still take priority; buried/report only
fills the gap for files the bridge has no other record of.

Also surfaces the printer's own storage usage (storage_total_mb/
storage_used_mb from the same payload) in /api/state, previously not
exposed anywhere in the bridge.

Verified end-to-end against the real printer: after a print start,
server/files/metadata for that file returned real size (9573908),
estimated_time (3203s), and layer_count (497) instead of the
placeholders, and /api/state reported real storage_total_mb/
storage_used_mb.
2026-08-02 13:03:11 +02:00
1f1d60d571 feat(browser): show real GCode thumbnails in the "On Printer" tab
All checks were successful
Nightly Build / build (push) Successful in 6m53s
The printer's file/fileDetails MQTT action extracts and base64-encodes
the embedded "; thumbnail begin" block from a GCode file's header on
demand and returns it inline as data.file_details.thumbnail - verified
live against a real Kobra X (a valid 230x110 PNG came back for an
existing print file).

New endpoint GET /kx/printer-files/{filename}/thumbnail wraps this via
the existing _wait_for_file_action() helper (same fire-and-forget +
file/report-callback pattern as listLocal/deleteBatch). Results are
cached in-memory per filename (self._printer_thumbnail_cache) - a
file's thumbnail never changes while it exists on the printer, and the
tab can list 100+ files at once.

Frontend fetches thumbnails lazily through a small sequential queue
(_printerThumbQueue/_pumpPrinterThumbQueue) after rendering the file
list, one request at a time rather than firing 100+ concurrent MQTT
roundtrips, and swaps each card's placeholder printer icon for the
real <img> once its thumbnail arrives. Client-side cache
(_printerThumbCache) avoids re-fetching on re-render (e.g. after a
selection change).

Verified end-to-end against the real printer and visually via
Playwright: all 10 listed files rendered their actual, distinct print
preview thumbnails instead of the generic icon.
2026-07-27 14:25:50 +02:00
cbcb17f45a feat(browser): add second tab for files on the printer's own storage
All checks were successful
Nightly Build / build (push) Successful in 6m50s
The GCode browser previously only showed files the bridge itself had
stored (its own SQLite GCodeStore, uploaded through the bridge). Files
printed directly via Anycubic Slicer Next (bypassing the bridge) land
on the printer's internal storage instead, and were only visible/
manageable from the printer's own display.

Adds a second sub-tab ("On Printer") using the same master-detail
tab pattern already used for Settings categories (showSettingsCat),
backed by the printer's file/listLocal and file/deleteBatch MQTT
actions - verified live against a real Kobra X, see memory
reference_mqtt_listlocal.md.

Key implementation detail: publish()'s own return value for these
actions is just a generic immediate ACK skeleton (code=0, empty
fields) - the real response arrives asynchronously via the file/report
callback (_on_file), same as the existing fileDetails fire-and-forget
pattern. Added _wait_for_file_action() as a small reusable bridge
between that async callback delivery and the synchronous HTTP handler,
via a per-action threading.Event registered in _on_file.

New endpoints: GET /kx/printer-files, POST /kx/printer-files/delete
(single endpoint for both single- and multi-select delete, since
deleteBatch natively accepts a filename list).

Frontend mirrors the existing store multi-select pattern (Issue #94):
select mode, select-all (scoped to what's loaded), bulk delete with
confirmation. No print/download actions in this tab for now - printing
a file already on the printer without re-uploading needs its own MQTT
schema that hasn't been verified yet.

Verified end-to-end against the real printer: listed 146 files,
deleted one, confirmed via a follow-up list that it was gone and
nothing else was affected. Also verified visually (Playwright):
tab switching, card rendering, multi-select mode, and cancel all work
as expected.
2026-07-27 14:04:22 +02:00
0ca7618c85 fix(moonraker): metadata state-leak + terminal-state reset gaps (Issue #102)
All checks were successful
Nightly Build / build (push) Successful in 7m9s
Three related gaps found via careful moonraker-obico observation:

1. _build_file_metadata() read layer_height/total_layers/estimated_time
   from live self._state before falling back to the queried file's own
   GCodeStore row - so querying metadata for any file other than the
   currently/last tracked job leaked that job's values into the response.
   Live state is now only used when the query targets that same tracked
   file; any other filename relies solely on its own stored row.

2. curr_layer/total_layers were never reset at print end in either
   _on_print or _on_info - only explicitly overwritten if a later
   payload happened to carry those keys, otherwise stuck indefinitely.
   Additionally, a successful "finished" print only ever cleared
   file_ready (Issue #29's fix), while stoped/canceled reset every
   other per-job field (progress, filename, duration, ...) - finished
   now gets the same full reset. Found and fixed a knock-on bug this
   surfaced: the unconditional `self._state["filename"] = d.get(...)`
   right after the reset block would immediately undo the filename
   reset, since the printer's own finished/stoped/canceled payload
   still carries the just-ended job's filename - now only applied
   outside terminal states.

3. The printer's own "progress" during pre-print phases (auto_leveling/
   preheating/checking/updated/init) was forwarded as-is to
   virtual_sdcard.progress/display_status.progress, causing a
   non-monotonic jump-then-reset once real printing began. These phases
   no longer update the tracked progress value.

Reported by @fmontagna, who correctly identified all three as genuine
gaps rather than intentional behavior.
2026-07-26 23:29:32 +02:00
6d6df59ff2 feat(filament): auto-match combined ACE-RFID vendor+type strings (Issue #101)
Anycubic's ACE RFID system concatenates vendor + material + a truncated
serial into one `type` string for custom (third-party) RFID tags, e.g.
"GEEETECH PLA Bas" for a Geeetech PLA spool. The bridge previously
treated this whole string as an unknown material and fell back to a
neutral "Generic <type>" profile, even when the user had already
imported a matching OrcaSlicer profile via the ZIP import feature
(Issue #41) - forcing a manual per-slot reassignment every time that
spool was loaded. Anycubic Slicer Next resolves the same tag correctly.

Add two helpers next to the existing _normalize_material/_material_family:
- _parse_combined_rfid_type(): splits the raw type string, recognizes a
  known vendor as the first token (checked against the merged
  system+user filament library, so custom vendors like "Geeetech" that
  only exist in the user's imported profiles are included), and
  extracts the material family from the remainder via the existing
  _material_family() prefix search. Returns ("", "") for a vendorless
  string like plain "PLA", leaving normal spool reports untouched.
- _match_profile_by_vendor_family(): looks up an imported/system profile
  by (vendor, family) rather than exact name, since the truncated RFID
  string never contains the full profile name verbatim.

Wired into _build_lane_data() as a third resolution layer, after the
existing manual per-slot override and before the Generic-library
fallback - not persisted to config.ini, so it re-derives fresh on every
call and can't go stale if a differently-tagged spool is loaded later.
2026-07-26 23:19:36 +02:00
16c1a8ee73 fix(camera): recover automatically after printer reboot rotates stream token (Issue #99)
The printer rotates its FLV/RTSP stream token on reboot. CameraCache.set_url()
was a bare assignment, so the three running ffmpeg loops never noticed - they
only re-read self._url at the top of their outer loop, which they never reach
while permanently blocked in a stdout read on the now-silent, stale-token
connection (TCP stays ESTABLISHED with no data and no FIN, so a passive
reader can't tell "peer is quiet" from "peer is gone").

Three-part fix, all per the excellent root-cause analysis and reproduction
in the issue (thanks @fmontagna):

1. set_url() now detects a URL change and calls reset() to tear down the
   stale ffmpeg loops, so the next ensure_running() respawns them against
   the new URL.
2. ffmpeg's -timeout option (10s, applies to both RTSP and HTTP-FLW since
   both share _input_args) as a second line of defense for a source that
   goes silent while the URL stays the same (network loss, camera hang).
3. handle_camera_stream now waits up to 5s for the first frame BEFORE
   calling resp.prepare() and returns 503 on timeout. Previously it had no
   first-frame timeout at all, and prepare() commits the response status
   to 200 - so a stalled source could never surface as an error to the
   client, only as an infinite hang.
2026-07-24 14:10:54 +02:00
884bdfc0c3 fix(ams): log rejected multiColorBox setInfo with the triggering request
The printer's failure payload for a rejected slot assignment carries
no slot/type/color info of its own, only an opaque
["multi_color_box", [{"filaments": {"id": N}, "id": box_id}]] shape -
useless for diagnosing why an assignment was rejected.

Remember the last setInfo request (_last_ams_set_request) and log it
alongside the failure so bridge logs alone can answer "what exact
type/color was sent, and to which slot" without asking the user to
dig it out manually or run extra reproduction steps.
2026-07-24 14:01:04 +02:00
8f14580e30 fix(ams): don't crash on rejected multiColorBox setInfo (Issue #100)
The printer replies with state="failed" and data as a 2-element list
(["multi_color_box", [...]]) instead of the usual dict when it rejects
a manual ACE slot filament assignment (e.g. custom-RFID/third-party
material). _on_multicolor_box called data.get(...) unconditionally,
crashing with AttributeError and silently dropping the report -
including the regular slot-state update that would otherwise follow.

Add a state="failed" guard plus a defensive isinstance check, and
surface the rejection via _state["last_ams_set_error"] so the caller
of handle_api_ams_set_slot's optimistic cache update isn't left
showing a false success.

Note: this fixes the crash, not necessarily the underlying rejection
itself - the printer/ACE may still refuse assignments for material
types it doesn't recognize.
2026-07-24 13:57:17 +02:00
2f222a0b93 fix(ams): point gap placeholders at a loaded tray, not an empty one
Some checks failed
PR Check / lint-and-test (pull_request) Failing after 1s
Printing via OrcaSlicer "Upload and print" failed or fed the wrong spool
when the slot below the used filament was empty (e.g. Filament 4 with slot
3 empty); all-slots-full worked.

_start_print -> _build_auto_ams_box_mapping keeps the AMS mapping positional
(entry N = TN) by inserting a placeholder at each gap. The placeholder's
ams_index pointed at the gap's own index, which for an empty slot references
a physically empty tray. The printer rejects a mapping entry aimed at an
empty tray even for a tool the GCode never calls, so the print broke.

Point gap placeholders (ams_index/color/material) at a definitely-loaded
fallback tray (the highest loaded slot) instead. Positional alignment is
preserved; the all-full path is unchanged.

Adds tests/test_auto_ams_box_mapping_empty_slot.py (invariant: every mapping
entry references a loaded/status-5 tray).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 18:31:10 -07:00
6e72346129 fix(ams): support multiple daisy-chained ACE units (Issue #95)
All checks were successful
Nightly Build / build (push) Successful in 7m17s
ace_direct mode (no toolhead buffer) only kept the first reported ACE
unit and silently dropped any others. Affects e.g. the Kobra S1 with two
ACE Pro units - the dashboard and OrcaSlicer sync only ever saw 4 of 8
slots. Global slot index is now box_id * 4 + local slot across all
units, matching the existing //4-%4 fallback in _global_to_box_slot.
Single-ACE (Kobra X) behavior is unchanged (unit id 0 -> same indices
as before).

feat(store): multi-select + bulk delete in the GCode browser (Issue #94)

Checkbox on every file card enters select mode; clicking anywhere on a
selected-mode card toggles it, existing per-file actions keep working via
stopPropagation. 'Select All' only affects the currently filtered/visible
files. Bulk delete is N parallel calls to the existing single-file DELETE
endpoint (no new backend route) with one confirmation dialog.
2026-07-19 18:44:46 +02:00
99bc1797c8 fix(camera): share one ffmpeg connection for /api/camera/stream
All checks were successful
Nightly Build / build (push) Successful in 9m34s
Manually integrated from PR #91 (Pavulon87) - the PR branch itself had
unresolved git conflict markers in handle_camera_stream from a stale
rebase against nightly, so it couldn't be merged directly.

/api/camera/stream previously spawned a dedicated ffmpeg process and
printer connection per HTTP client. The printer only tolerates a very
limited number of concurrent camera connections, so two simultaneous
viewers (dashboard + a Moonraker client, two browser tabs, ...) could
already exhaust that limit and cause intermittent 429/"stream
unavailable" failures.

Adds a third CameraCache fanout channel (mjpeg_subscribers), mirroring
the existing h264 pattern, so all /api/camera/stream consumers share one
ffmpeg process. Also carries over two related fixes found in the PR:
ensure_running() checked self._proc_* instead of self._task_*, leaving a
race window where two callers could each spawn a duplicate ffmpeg
process before the first task got scheduled; and reset() didn't cancel
the owning task, so a loop stuck in its exponential backoff sleep (up to
300s) wouldn't restart immediately.
2026-07-16 22:33:50 +02:00
e1b9480098 Merge branch 'pr92' into nightly
Some checks failed
Nightly Build / build (push) Has been cancelled
2026-07-16 21:37:09 +02:00
03089db6af fix: self._db did not exist, use self._store
Some checks failed
Nightly Build / build (push) Has been cancelled
PR Check / lint-and-test (pull_request) Has been cancelled
2026-07-16 14:48:23 +02:00
cd4a8ce48e feat(dashboard): show printer pause reason on the dashboard
Some checks failed
PR Check / lint-and-test (pull_request) Failing after 2s
2026-07-16 14:42:48 +02:00
b9594d4a22 feat(dashboard): free drag+resize grid with custom presets (Issue #89)
All checks were successful
Nightly Build / build (push) Successful in 13m35s
Replace the fixed dashboard layout with a fully customizable GridStack.js
grid (12-col snap grid, vendored + inlined so it also works in OrcaSlicer's
embedded webview). Cards can be dragged, resized, hidden and rearranged;
layout persists per browser. Includes two built-in presets (Standard,
Wide desktop per the original Issue #89 proposal) plus the ability to
save/apply/delete named custom presets.

fix(camera): stream freeze after ~15-30min from non-monotonic FLV
timestamps — ffmpeg's realtime pacing stalls on PTS jumps in the printer's
stream. Fixed with -use_wallclock_as_timestamps on both ffmpeg call sites
(CameraCache._input_args, _run_h264_loop). Issue #90.

test: fix tests/conftest.py referencing a stale bridge/ subfolder path and
missing args.data_dir (pre-existing breakage, unrelated to this feature);
rewrite the two test_settings.py cases that still mocked the removed
_find_env_path from the old .env-based settings storage.
2026-07-16 11:34:34 +02:00
bf94a8f563 fix(filament): suppress stale slot profile when AMS material family changes
Some checks failed
PR Check / lint-and-test (pull_request) Has been cancelled
Nightly Build / build (push) Successful in 14m49s
A per-slot filament profile override (config.ini [filament_profiles]) stores
only {vendor, name, id} and is sticky: swapping the physical filament updates
the AMS colour + type live, but the saved profile persisted. Loading yellow PLA
into a slot that held "KINGROON PETG Basic" kept showing/sending PETG in the
panel and the OrcaSlicer lane hint, and survived restarts (config.ini).

Resolve the effective profile per slot as the saved override only when its
material *family* still matches the loaded AMS material — PLA / PLA+ / PLA SILK /
PLA MATTE are one family, so within-family swaps never invalidate a valid
profile (guards against the earlier over-strict material compare). On a family
change the override is suppressed (slot falls back to the generic default) but
NOT deleted, so re-loading the original material reactivates it. Profile
material is resolved from the Orca filament library; unknown → never suppress.

Wired into the three resolution sites: handle_kx_filament_slots (panel),
_build_lane_data (OrcaSlicer AMS array), _build_mmu_object (gate_filament_name).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 23:09:16 -07:00
d2c92c2deb fix(settings): stale env vars survived bridge restart, reverting saved changes
_restart_bridge() cleared a hardcoded, manually-maintained list of env
keys before restarting — every setting added since (vibration_compensation,
host_ip, poll_interval, verbose_http_log) was missing from it. The old
process's env var therefore survived into the new process and silently
overrode the freshly written config.ini value, making toggles appear to
revert right after saving.

Fixed at the root: config_loader.CONFIG_ENV_MAPPING is now the single
source of truth for which env keys back which config.ini options, and
_restart_bridge() derives its cleanup list from it. A newly added
setting can't be forgotten here again.
2026-07-07 01:23:01 +02:00
b541aafc74 feat(settings): make the HTTP access log toggleable in the UI
Previous commit hard-disabled aiohttp's per-request access log via
setLevel(WARNING), with no way to turn it back on. Added a
'verbose_http_log' setting (default off) — toggle in Settings, persisted
to config.ini, applied on bridge start via _set_verbose_http_log().
2026-07-07 01:20:34 +02:00
f2f4447809 chore: quiet aiohttp per-request access log
Every HTTP request logged an INFO line via aiohttp's access logger,
drowning out the bridge's own logs given the frontend's 2s poll
interval. Raised the aiohttp.access logger threshold to WARNING.
2026-07-07 01:12:30 +02:00
a3deb33b97 fix(spoolman): status dot showed green when Spoolman was unreachable
/kx/spoolman/status only exposed 'configured' (server URL is set),
not actual reachability — health_check() ran once at boot and was
only logged, never surfaced to the API or rechecked afterwards.

Now the poll loop rechecks reachability every 30s, the status endpoint
returns 'reachable', and the frontend dot shows red + '(unreachable)'
when Spoolman is configured but not responding.
2026-07-07 01:08:41 +02:00
2e2061a269 fix(settings): poll_interval was saved but never applied
The setting was persisted to config.ini and returned by /api/settings,
but --poll-interval was never registered as an argparse argument and
POLL_INTERVAL was missing from config_loader's env mapping, so
self._args.poll_interval never existed. The poll loop used a hardcoded
3.0s wait regardless of the configured value.
2026-07-07 01:04:44 +02:00
aea6e457f3 refactor: dedupe upload response, log user-relevant swallowed errors
- OctoPrint upload response was built twice in handle_file_upload
- gcode_filaments cache load failure on dashboard reprint now logs a
  warning (silent failure would degrade the Issue #84 fix unnoticed)
- filament metadata backfill failures now log at debug level
2026-07-06 22:39:09 +02:00
0ae8ae59be refactor: extract shared print payload builder and skip-state reset
The print/start payload was duplicated in three places (upload path,
KX store, Moonraker API) and had drifted: the settings-based
vibration_compensation value was missing in the upload path. All three
paths now use _build_print_payload() and _reset_skip_state().
2026-07-06 22:35:56 +02:00
0322ade606 refactor: translate all logs, comments and API error strings to English
Logs are user-facing across all locales; comments and docstrings switch
to English for external contributors. No behavior change.
2026-07-06 22:33:25 +02:00
786fa08ca0 fix: dashboard reprint slot-shift, startup IP log, resonance compensation toggle
Some checks failed
Nightly Build / build (push) Failing after 1m5s
- Dashboard reprint now delegates to _start_print with gcode_filaments from DB
  so the used_paint_indices filter applies correctly (Issue #84)
- Startup log no longer shows 0.0.0.0 — actual LAN IP is displayed (Issue #86)
- New vibration_compensation setting: toggle in Settings UI activates resonance
  compensation before each print, follows exact auto_leveling pattern (Issue #85)
2026-07-06 13:54:44 +02:00
cd11542352 fix(filament): PLA-Varianten (PLA+, Silk, Matte) korrekt erkennen und an OrcaSlicer übermitteln
All checks were successful
Nightly Build / build (push) Successful in 12m36s
- _normalize_material() normalisiert Drucker-Typen auf kanonische Keys
- _TRAY_INFO_IDX erweitert um Silk/Matte/CF-Varianten und Schreibweisen
- _default_filament_name() mappt Varianten auf korrekte Generic-Profile
- Filament-Dropdown zeigt Hersteller-Profile der jeweiligen Variante
- Material-Buttons: PLA+, PLA Silk, PLA Matte hinzugefügt

Fixes #82
2026-07-02 21:54:42 +02:00
2a13f1f0dd fix(spoolman): repair dead slot-map persistence + isolate it per printer
The AMS-slot -> Spoolman-spool persistence never worked: KobraXBridge
referenced `config_loader` in both the load (__init__) and save
(handle_kx_spoolman_set_active) paths, but the module alias is `env_loader`
(kobrax_moonraker_bridge.py:32). The resulting NameError was swallowed by a
bare `except`, so the map was neither loaded on startup nor written on change
- it only appeared to persist.

The map also lived in a single global `[spoolman] slot_spools` key, so on a
multi-printer bridge two AMS units clobbered each other's mapping (same class
of bug as #74/#75 for filament profiles).

- config_loader: add list_spool_map()/save_spool_map(printer_id) using a
  per-printer `[spoolman_<id>]` section with read-fallback to the legacy
  global key, mirroring _filament_section/list_filament_profiles. The global
  `[spoolman]` section keeps server/sync_rate.
- bridge: load via config_loader.list_spool_map(self._printer_id); persist via
  save_spool_map(..., self._printer_id); surface failures via log.warning
  instead of a silent except.
- _build_mmu_object: emit real gate_spool_id from the per-printer map (was
  hardcoded [-1]*num_gates) so Happy-Hare/OrcaSlicer can show the bound spool.
- config.ini.example: document the [spoolman] section.
- tests: tests/test_spoolman_slot_map.py (per-printer isolation, persistence
  round-trip, server/sync_rate preservation, parser robustness).

Verified on a 2-printer bridge: after restart KX1 loads its spools and KX2
loads its own, isolated; a real multicolor print deducted per slot (white spool
1.02g vs 0.98g slicer estimate) against the correct printer's spools.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 21:42:40 -07:00
a16062f44f fix(ams): ams_box_mapping mit Platzhaltern für fehlende Paint-Indizes auffüllen
All checks were successful
Nightly Build / build (push) Successful in 11m17s
Drucker interpretiert ams_box_mapping als geordnete Liste (Eintrag N = TN).
Bei Drucken die T0 nicht nutzen wurden die Einträge um 1 verschoben,
sodass T2 (rot) auf den Slot von T3 (weiß) zeigte.

Fixes #78
2026-07-01 20:51:12 +02:00
4f5aa8d126 Revert "fix(ams): paint_index im auto-mapping auf global_index setzen statt enumerate-Zähler"
All checks were successful
Nightly Build / build (push) Successful in 10m12s
This reverts commit c313e014ad.
2026-06-30 23:01:03 +02:00
c313e014ad fix(ams): paint_index im auto-mapping auf global_index setzen statt enumerate-Zähler
Some checks failed
Nightly Build / build (push) Has been cancelled
Bei Multicolor-Drucken mit nicht bei 0 startenden Paint-Indizes (T2, T3...)
wurde paint_index als 0,1,2... statt als tatsächlicher GCode-T-Index gesendet.
Drucker hat dadurch die falschen Slots für die falschen Farben verwendet.

Fixes #78
2026-06-30 22:56:34 +02:00
6e9ba0672f fix(spoolman): Slot-Spool-Zuordnung in config.ini persistieren + beim Start laden; API-Feldname-Kompatibilität (slot_spools/slot_map)
All checks were successful
Nightly Build / build (push) Successful in 10m48s
2026-06-30 15:43:20 +02:00
74fc2ddab0 feat: color picker, unified UI styling, filament mismatch detection, Spoolman slot assignment
All checks were successful
Nightly Build / build (push) Successful in 4m27s
- Slot color editor: Pickr HSV color picker (offline, served from lib/),
  recent swatches (up to 16, localStorage), copy color from other slot
- Unified axes control panel: XY+Z merged, shared step size + custom mm input
- Language selector moved from header to Settings → Appearance
- Filament mismatch detection blocks Upload-and-Print on material mismatch,
  slot mapper opens automatically
- Spoolman spool-per-slot assignment in AMS status tab and Filaments settings
- Fix: Spoolman sync rate label — 0=end of print, not disabled (Issue #76)
- Fix: lib/ assets served by bridge static handler for offline use
- UI: global unified select + input styling, set-row labels match modal-field
2026-06-30 11:13:34 +02:00
0e1d46ee7f fix: isolate filament profiles per printer in multi-printer bridge (#74)
Some checks failed
PR Check / lint-and-test (pull_request) Has been cancelled
Per-printer [filament_profiles_<id>] sections so configuring one printer no
longer overwrites another (read-fallback to the legacy global section keeps
single-printer setups unchanged). Dropdown/switch links now navigate to each
printer's own bridge_url. Adds pytest coverage and a CHANGELOG entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 07:13:10 +02:00
cfe70430d3 feat: unified axes panel, language selector cleanup, filament mismatch detection, Spoolman slot assignment
Some checks failed
Nightly Build / build (push) Failing after 5m11s
- Merged XY + Z axis cards into single Axes Control panel with shared step
  size row and free-text mm input; Home XY/Z placed below respective pads;
  Home All + Motors Off centred at bottom
- Language selector removed from header bar, now only in Settings → Appearance
- Upload-and-Print blocked on GCode/AMS material mismatch; slot mapper dialog
  opens automatically with mismatch warning highlighting affected slots
- Spoolman spool-per-slot assignment: dropdown in AMS status tab per kachel,
  dedicated card in Filaments settings tab with save button
- nightly.yml: NIGHTLY_CHANGELOG.md takes precedence over auto-generated
  commit log when present; CI uses it as release body verbatim
2026-06-28 23:17:58 +02:00
3531cad0ef nightly: 0.9.27-nightly7
All checks were successful
Nightly Build / build (push) Successful in 4m4s
2026-06-26 22:51:32 +02:00
2f59a2b02b nightly: 0.9.27-nightly5
Some checks failed
Nightly Build / build (push) Failing after 3m48s
2026-06-25 23:29:34 +02:00
ac309d5d3d nightly: 0.9.27-nightly4
Some checks failed
Nightly Build / build (push) Failing after 3m37s
2026-06-25 23:01:34 +02:00
e59550b5a0 nightly: 0.9.27-nightly3 + Kamera-Fix
All checks were successful
Nightly Build / build (push) Successful in 3m33s
2026-06-25 22:38:45 +02:00
e70e9c82d7 nightly: 0.9.27-nightly3 2026-06-25 22:37:39 +02:00
72f77d92af feat: Integrationen-Tab + KobraX Full Stack Compose + CI-Workflow
Some checks failed
Docker Publish / build-push (push) Failing after 13s
- Settings-Tab "Integrationen": Spoolman URL/Sync-Rate konfigurierbar,
  Obico Read-only Hinweis auf moonraker-obico.cfg
- docker-compose-KX.yml: Portainer-kompatibler Full Stack (KX-Bridge +
  Obico Self-Hosted + Spoolman + moonraker-obico Plugin)
- moonraker-obico.cfg.example: Verbindungsvorlage für Obico-Integration
- .gitea/workflows/docker-publish.yml: Push auf nightly → :nightly Image,
  v*-Tag → :latest + :<VERSION>
2026-06-24 12:58:46 +02:00
d20308cf2c nightly: 0.9.27-nightly1 2026-06-23 15:05:40 +02:00
5bff7adad0 build: sources for v0.9.26 2026-06-21 21:46:38 +02:00