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>
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>