Files
bambuddy/backend
maziggy 73db4f174e Register a Spoolman extra field with its own write (issue #2903)
Spoolman rejects a spool whose extra dict carries a key it has not been
    told about, answering 400 "Unknown extra field tag.". Bambuddy keeps the
    tray UUID in extra.tag, so that key has to exist before the first spool
    is created. Registration ran from three hand-maintained lists that fire
    when the integration is set up -- the connect route, startup, and two
    inline blocks in the inventory routes. Enabling Spoolman from the
    Settings page reaches none of them, so the first AMS sync on a fresh
    Spoolman failed on every slot while vendor and filament creation
    succeeded.

    Neither fix suggested on the issue is quite the right shape. Adding the
    block to PUT /settings/spoolman fixes this path and makes a third copy
    of a list that has already drifted -- it would still omit
    bambu_color_name. Ensuring at the sync entry point leaves the other four
    tag writers alone: linking and unlinking a tag, and both inventory edit
    paths.

    So the registration moved to the write. create_spool, update_spool and
    update_spool_full each register the keys of the extra dict they are
    about to send, once per client, before sending. Every tag writer funnels
    through one of the three, merge_spool_extra included. This closes the
    class rather than the instance: a write that carries a key is a write
    that registers it, and bambu_color_name shows why that matters -- it
    never made it into the connect or startup lists at all, and works today
    only because two call sites remembered it by hand.

    Best-effort, deliberately. ensure_extra_field already logs and returns
    False rather than raising, so a registration that fails leaves the write
    to be attempted and to report exactly what it reported before. Failures
    are not memoised either, so a Spoolman that was merely restarting gets
    another try on the next write.

    The older blocks stay. They are redundant now, but the inventory routes'
    inline calls are pinned by tests that assert them against a mocked
    client, where the funnel cannot run.

    The status endpoint is the other half. The Connect button would have
    registered the fields, and the reason nobody reaches it is that
    GET /spoolman/status reported "connected" whenever an earlier request
    had left a client object behind. Roughly twenty call sites build one
    lazily, and saving the Settings page builds one as a side effect of
    syncing locations, so the flag turned on which page had been opened
    rather than on anything about Spoolman. The UI reads it twice -- Connect
    only while disconnected, the sync section only while connected -- so
    those two controls landed in states the user cannot explain. It now asks
    the Spoolman that is configured, including the stale-URL check every
    other route already does, and does not probe at all when the integration
    is switched off, which used to let a leftover client report a disabled
    Spoolman as connected.

    That leaves nothing for Disconnect to do, so it is gone. Spoolman is a
    stateless HTTP API with no session to close; the button dropped the
    client object, the next request rebuilt it lazily, and the status
    flipped back on its own within the 30s poll -- an action that looked
    like it worked and then quietly undid itself. The enable toggle owns
    turning the integration off. Connect stays as what it always was in
    practice, a way to re-check a Spoolman that is not answering, and is
    shown only then. Its translation keys are left in place; only the
    control is removed.

    Resolving the client there means the status poll can now fail in ways a
    read-only check could not, so it no longer reports failure by failing.
    Replacing a client closes the previous one and httpx's aclose() is not
    guaranteed not to raise; a poll that runs every 30 seconds answering 500
    is worse than one answering what is true either way, which is that
    Spoolman could not be reached. The SSRF rejection keeps its own message
    rather than folding into the general one -- a URL the guard refuses is
    the admin's to correct, and that is only actionable if the log says so.

    Tests drive the real client against a fake Spoolman that enforces the
    unknown-extra-field rule rather than mocking it away, so each one fails
    against the old code for the reason the reporter's install did. The
    concurrency test needed the fake to be async: MockTransport answers
    without suspending, so the first version ran each request to completion
    in turn and passed just as happily with the lock removed.
2026-08-23 12:44:45 +02:00
..