The Archives banner picks its wording from a priority list of the causes it
knows. REASON_FTPS_COOLOFF was added by #2957 and never put in that list, so an
install whose empty archives all came from a printer refusing the TLS handshake
matched nothing, got reason: null, and fell to the original wording: the slicer
did not leave the .gcode.3mf on the card, switch on "Store sent files on
external storage", here is installation step 4.
Every clause of that is wrong for this cause. The slicer did write the file --
reason he read the whole thing as Bambuddy being broken. The setting was
already on. And there is nothing on his side to change: the printer's file
service answered port 990 with something that is not TLS, so no lookup ever
ran and where the file went was never tested. It is #2899's mistake -- an
error message describing a cause that was ruled out before it was printed --
in a surface that did not get that pass.
The slug now leads the list rather than joining the end of it. The other three
describe an install working as configured and each ends in something the
operator can change; this one reports a fault nobody can yet explain, which is
both the more urgent thing to say and the thing that produces a useful report.
The banner also dismisses one-shot into localStorage, so a reason ranked below
another is not deferred to next time -- it is never shown to that user again.
Ranking it first cannot bury a permanent cause in exchange: a successful
recovery clears the row's markers (#2957), so a row still carrying this slug is
one whose retry failed too, days after the print.
New wording in all fourteen languages says the printer refused the connection,
that this is not a slicer setting and not something the operator did, that
Bambuddy comes back for the file when the five-minute pause clears so a brief
episode fills itself in, and that a card still empty means the refusal outlasted
the retry. It links to the handshake entry in the troubleshooting guide instead
of to the installation guide.
The client's getNo3MFWarning type still declared the old three-slug union, which
made all three new comparisons provably dead -- caught by tsc, not by any test.
Four tests. One pins the slug reaching the banner, one pins it outranking the
three settled causes, one pins those three keeping their order behind it, and
one asserts the rendered wording carries no slicer advice at all.
Also corrects the wiki page these reports are pointed at. It said to power-cycle
the printer; the reporter who prompted that advice power-cycled both of his and
the failure continued unchanged, and bambu_ftp.py has carried the retraction in
a comment since. The page now states what was actually measured -- that a
version mismatch reports itself differently, that every printer probed refuses
TLS 1.3 and completes on 1.2 so there is no version to fall back from, and that
three P2S units failed while three more on the same switch never did -- says
plainly that the trigger is unknown, and names the one cleartext-probe line
worth collecting.
disconnect() and _abandon_connection() logged nothing, at any level. A
session closed cleanly and a socket genuinely abandoned therefore produced
identical output -- none -- and the only way to tell them apart was to read
the source.
That is how #3009 was filed. Its trace shows a print completion opening two
FTP connections, deleting one file, and then nothing until the printer was
powered off 21 minutes later, read as connections left open and offered as a
mechanism for the 0500-C010 SD-card error that #645 has been chasing since
April. The two connections are the post-print SD cleanup in main.py walking
its candidate filenames, each through delete_file_async, which closes in a
finally; running that against the mock FTPS server shows the server logging
"FTP session closed (disconnect)" for both the 250 and the 550, holding zero
sessions afterwards. Nothing in a support bundle could have shown that.
Both close paths now log one DEBUG line: the printer, whether QUIT was
acknowledged or the socket had to be dropped without it, why, and how long
the session was held. Every connect in a debug log now has a matching close.
The duration comes from a stamp taken when the control socket opens rather
than after login, so a session that dies during login is accounted for too;
where no socket was ever established the line says "held unknown" rather
than claiming a number. The four connect() failure paths pass their own
reason, so a close line stands on its own next to the warning above it.
Nine tests, seven of which fail against the unlogged version. The other two
assert silence -- a bare disconnect(), and a connect skipped by the handshake
cool-off -- where no socket was opened and a close line would pair with no
connect.
OrcaCloudService owns an httpx client from construction, and every path in
_build_authenticated_service after that point can raise: no stored refresh
token, a rejected refresh, an unreachable Orca, and the token-rotation write.
On success the caller closes the client. On failure nobody is ever handed it,
so all four paths leaked one into the connection pool.
That went unnoticed while the only callers were routes, where the trigger is a
person retrying a broken sign-in a handful of times. It stopped being harmless
in 9434875f, which added a caller in spool assignment -- one build per
Orca-referenced spool, failing on every assignment for as long as the stored
credentials cannot be refreshed.
The unwind guard catches BaseException rather than Exception: a cancelled
request leaks the client just as surely as a failed refresh, and cancellation
during shutdown is exactly when dangling sockets are least welcome. The close
inside it is guarded in turn, so a failing cleanup cannot replace the error the
caller needs to see -- least of all a CancelledError, which has to keep
propagating for cancellation to work at all.
Six tests. Four fail against the unguarded builder, verified by reverting the
guard and re-running; the other two pin the surrounding contract (a failing
close must not mask the real error, and a successful build must leave the
client open for its caller) and pass either way. The shared _expired_service
helper now gives the mock an awaitable close(), so the four pre-existing
refresh tests exercise the same path.
Also corrects two comments and the changelog entry from 9434875f, which
overstated what the captures support. They claimed Bambu Cloud returns a
preset's filament_id in either of two places and only one was read. The
responses recorded in #1053 show something narrower: a Studio-created preset
carries it on the envelope, and an Orca-created one has none at all -- the
envelope says null and `setting` is a delta from the base. The `setting` lookup
stays as belt-and-braces for a shape no captured response has needed yet, but
it is not why a custom profile reached the slicer as its base. That is the
OrcaSlicer preset format having no filament_id field, filed upstream as
OrcaSlicer PR #13315.
The eight-character truncation is now evidenced across three models rather than
one -- an A1 storing PFUS9DDC of PFUS9DDC938FE3AB8F, a P1S storing PFUS7A65 of
PFUS7A65290D3DADC4, and an H2D storing 8219C45D of an Orca profile UUID.
A custom filament profile reaches an AMS slot as itself through exactly one
field, tray_info_idx, and every source we can read that id from was reading it
from the wrong place or not reading it at all.
Bambu Cloud returns a preset's own filament_id either on the response envelope
or inside the preset JSON under `setting`, and only the envelope was read.
Presets of the second shape fell through to the base_id branch and reached the
slicer as the Bambu filament they inherit from. filament_type next door already
handled both spreads; filament_id now does too.
Orca Cloud was absent from the resolver entirely. A spool stores the bare
profile UUID, which matched no branch and fell through normalize_slicer_filament
-- a function that passes anything it does not recognise straight through -- so
a 36-character UUID went into the field. Orca profiles carry their own
filament_id in the slicer JSON that OrcaProfileDetail already exposes under
`setting`, so the lookup is the same one the Bambu branch does. It is
best-effort: no pairing, a dead token or a missing orca_cloud:auth permission
degrades to the fallback rather than failing the assignment, and it passes
clear_on_auth_failure=False because a background caller cannot tell a real
revocation from a lost refresh-rotation race.
configure_ams_slot sent the cloud setting_id as tray_info_idx when it found no
real filament id. That field is 8 characters on the printer -- exactly the width
of a local preset id, less than half a cloud one. Measured on the reporter's A1:
sent PFUS9ddc938fe3ab8f, the tray read back PFUS9DDC, acknowledged as a success.
The slot then resolved to nothing, so the slicer showed Generic anyway and the
calibration table, keyed by the same field, lost the slot. It now falls back to
the slot's existing filament id or the generic for the material, and the route's
guard was aligned with the resolver's so both refuse the same four shapes from
one shared definition.
This reverses the contract #1053 pinned. Six tests asserted that the PFUS
belonged in tray_info_idx; the A1 capture shows it never worked, so they were
rewritten with the measurement in their docstrings.
Verified against 874 AMS trays across twelve models in the support archive: 92
already carry a custom "P" + 7 hex filament id, which is what confirms the
mechanism works and this is a lookup failure rather than a platform limit. No
tray on any model carries a setting_id, so a profile with no filament_id of its
own still cannot be told apart from its base.
GHSA-cp6q-959q-f8rh: @tiptap/core's mergeAttributes() copies keys out of
Object.entries() with plain bracket assignment, so an own __proto__ key
from JSON hits the legacy prototype setter rather than writing a
property. The result carries an attacker-controlled prototype while
Object.keys() and own-property checks show nothing, and ProseMirror's
DOMSerializer.renderSpec() enumerates attribute objects with for...in --
so inherited src and onerror land on a rendered <img> and execute.
Medium, CVSS 4.0 6.4, fixed in 3.30.4.
Not reachable here. The advisory needs an untrusted object arriving at
mergeAttributes(), or a custom or dynamic extension that preserves the
attribute object. Nothing under frontend/src calls mergeAttributes or
defines an extension, and RichTextEditor builds a fixed schema from
StarterKit plus six stock extensions whose HTMLAttributes are static
literals. Content crosses as an HTML string rather than JSON, so no own
__proto__ key reaches an attrs object at all -- the DOM parser only
fills attributes the schema declares -- and every read-only render is
sanitized.
Lockfile only: package.json already declared ^3.11.1, so the patched
line was inside the range and only the stale lock held 3.19.0. No
overrides entry needed.
@tiptap/pm has narrowed its dependency set, so prosemirror-markdown,
prosemirror-menu, prosemirror-collab, prosemirror-schema-basic,
prosemirror-trailing-node, markdown-it and linkify-it leave the tree --
16 packages, none imported by this repo. That retires the reachability
note carried for linkify-it in 1.2.5.
eslint, build with the Safari 16 baseline check, i18n parity and 3514
frontend tests across 256 files all pass. npm audit --omit=dev, which
is what CI gates on, reports zero vulnerabilities.
Backend Tests (shard 1/4) timed out after 10 minutes on e8b901f54
("Updated BACKERS"), a docs-only commit. The step was not hung: it
reached 98%, every test passing, and was killed about 12 seconds short
of finishing.
pytest-split balances by duration only when it has a durations file,
and there was none. Without one it splits by test COUNT -- 2926 / 2926
/ 2926 / 2925, exactly 25% each, which is what the comment here claimed
was good enough. Count is not time. Measured over the full suite
(11703 tests, 1045.6s):
shard 1 2926 tests 658.2s
shard 2 2926 tests 173.1s
shard 3 2926 tests 113.3s
shard 4 2925 tests 101.1s
Shard 1 was carrying 63% of the suite's runtime -- a 6.5x spread -- and
had been walking toward the cap for weeks: 475s, 404s, 445s, then 587s
on 08-30, thirteen seconds under it, and 613s here. CI printed the
reason in its own log on every run: "[pytest-split] No test durations
found."
Commit tests/.test_durations and pass --durations-path explicitly:
shard 1 1273 tests 261.6s
shard 2 1070 tests 261.5s
shard 3 1522 tests 262.0s
shard 4 7838 tests 260.6s
A 1.01x spread. The lopsided test counts are the point: shard 4 takes
thousands of fast unit tests, shard 1 keeps the slow integration ones.
The four groups still partition the suite exactly -- union is 11703
with nothing dropped or duplicated, and all four pass.
--durations-path has to be explicit because pytest-split defaults it to
$CWD/.test_durations, and the two matrices run from different
directories: the native job from backend/, the Docker job from the
image root. Left implicit, the Docker one finds nothing and silently
falls back to the count split. Node IDs match across both because
backend/tests/pytest.ini pins rootdir to backend/tests either way, so a
single file serves them both; verified the file clears .dockerignore
and lands at /app/backend/tests/.test_durations at full size.
timeout-minutes 10 -> 15 for headroom, since the file goes stale as
tests are added. Staleness degrades slowly rather than breaking --
unknown tests are treated as average.
The Archives banner picks its wording from a priority list of the causes it
knows. REASON_FTPS_COOLOFF was added by #2957 and never put in that list, so an
install whose empty archives all came from a printer refusing the TLS handshake
matched nothing, got reason: null, and fell to the original wording: the slicer
did not leave the .gcode.3mf on the card, switch on "Store sent files on
external storage", here is installation step 4.
Every clause of that is wrong for this cause. The slicer did write the file --
reason he read the whole thing as Bambuddy being broken. The setting was
already on. And there is nothing on his side to change: the printer's file
service answered port 990 with something that is not TLS, so no lookup ever
ran and where the file went was never tested. It is #2899's mistake -- an
error message describing a cause that was ruled out before it was printed --
in a surface that did not get that pass.
The slug now leads the list rather than joining the end of it. The other three
describe an install working as configured and each ends in something the
operator can change; this one reports a fault nobody can yet explain, which is
both the more urgent thing to say and the thing that produces a useful report.
The banner also dismisses one-shot into localStorage, so a reason ranked below
another is not deferred to next time -- it is never shown to that user again.
Ranking it first cannot bury a permanent cause in exchange: a successful
recovery clears the row's markers (#2957), so a row still carrying this slug is
one whose retry failed too, days after the print.
New wording in all fourteen languages says the printer refused the connection,
that this is not a slicer setting and not something the operator did, that
Bambuddy comes back for the file when the five-minute pause clears so a brief
episode fills itself in, and that a card still empty means the refusal outlasted
the retry. It links to the handshake entry in the troubleshooting guide instead
of to the installation guide.
The client's getNo3MFWarning type still declared the old three-slug union, which
made all three new comparisons provably dead -- caught by tsc, not by any test.
Four tests. One pins the slug reaching the banner, one pins it outranking the
three settled causes, one pins those three keeping their order behind it, and
one asserts the rendered wording carries no slicer advice at all.
Also corrects the wiki page these reports are pointed at. It said to power-cycle
the printer; the reporter who prompted that advice power-cycled both of his and
the failure continued unchanged, and bambu_ftp.py has carried the retraction in
a comment since. The page now states what was actually measured -- that a
version mismatch reports itself differently, that every printer probed refuses
TLS 1.3 and completes on 1.2 so there is no version to fall back from, and that
three P2S units failed while three more on the same switch never did -- says
plainly that the trigger is unknown, and names the one cleartext-probe line
worth collecting.
disconnect() and _abandon_connection() logged nothing, at any level. A
session closed cleanly and a socket genuinely abandoned therefore produced
identical output -- none -- and the only way to tell them apart was to read
the source.
That is how #3009 was filed. Its trace shows a print completion opening two
FTP connections, deleting one file, and then nothing until the printer was
powered off 21 minutes later, read as connections left open and offered as a
mechanism for the 0500-C010 SD-card error that #645 has been chasing since
April. The two connections are the post-print SD cleanup in main.py walking
its candidate filenames, each through delete_file_async, which closes in a
finally; running that against the mock FTPS server shows the server logging
"FTP session closed (disconnect)" for both the 250 and the 550, holding zero
sessions afterwards. Nothing in a support bundle could have shown that.
Both close paths now log one DEBUG line: the printer, whether QUIT was
acknowledged or the socket had to be dropped without it, why, and how long
the session was held. Every connect in a debug log now has a matching close.
The duration comes from a stamp taken when the control socket opens rather
than after login, so a session that dies during login is accounted for too;
where no socket was ever established the line says "held unknown" rather
than claiming a number. The four connect() failure paths pass their own
reason, so a close line stands on its own next to the warning above it.
Nine tests, seven of which fail against the unlogged version. The other two
assert silence -- a bare disconnect(), and a connect skipped by the handshake
cool-off -- where no socket was opened and a close line would pair with no
connect.
OrcaCloudService owns an httpx client from construction, and every path in
_build_authenticated_service after that point can raise: no stored refresh
token, a rejected refresh, an unreachable Orca, and the token-rotation write.
On success the caller closes the client. On failure nobody is ever handed it,
so all four paths leaked one into the connection pool.
That went unnoticed while the only callers were routes, where the trigger is a
person retrying a broken sign-in a handful of times. It stopped being harmless
in 9434875f, which added a caller in spool assignment -- one build per
Orca-referenced spool, failing on every assignment for as long as the stored
credentials cannot be refreshed.
The unwind guard catches BaseException rather than Exception: a cancelled
request leaks the client just as surely as a failed refresh, and cancellation
during shutdown is exactly when dangling sockets are least welcome. The close
inside it is guarded in turn, so a failing cleanup cannot replace the error the
caller needs to see -- least of all a CancelledError, which has to keep
propagating for cancellation to work at all.
Six tests. Four fail against the unguarded builder, verified by reverting the
guard and re-running; the other two pin the surrounding contract (a failing
close must not mask the real error, and a successful build must leave the
client open for its caller) and pass either way. The shared _expired_service
helper now gives the mock an awaitable close(), so the four pre-existing
refresh tests exercise the same path.
Also corrects two comments and the changelog entry from 9434875f, which
overstated what the captures support. They claimed Bambu Cloud returns a
preset's filament_id in either of two places and only one was read. The
responses recorded in #1053 show something narrower: a Studio-created preset
carries it on the envelope, and an Orca-created one has none at all -- the
envelope says null and `setting` is a delta from the base. The `setting` lookup
stays as belt-and-braces for a shape no captured response has needed yet, but
it is not why a custom profile reached the slicer as its base. That is the
OrcaSlicer preset format having no filament_id field, filed upstream as
OrcaSlicer PR #13315.
The eight-character truncation is now evidenced across three models rather than
one -- an A1 storing PFUS9DDC of PFUS9DDC938FE3AB8F, a P1S storing PFUS7A65 of
PFUS7A65290D3DADC4, and an H2D storing 8219C45D of an Orca profile UUID.
A custom filament profile reaches an AMS slot as itself through exactly one
field, tray_info_idx, and every source we can read that id from was reading it
from the wrong place or not reading it at all.
Bambu Cloud returns a preset's own filament_id either on the response envelope
or inside the preset JSON under `setting`, and only the envelope was read.
Presets of the second shape fell through to the base_id branch and reached the
slicer as the Bambu filament they inherit from. filament_type next door already
handled both spreads; filament_id now does too.
Orca Cloud was absent from the resolver entirely. A spool stores the bare
profile UUID, which matched no branch and fell through normalize_slicer_filament
-- a function that passes anything it does not recognise straight through -- so
a 36-character UUID went into the field. Orca profiles carry their own
filament_id in the slicer JSON that OrcaProfileDetail already exposes under
`setting`, so the lookup is the same one the Bambu branch does. It is
best-effort: no pairing, a dead token or a missing orca_cloud:auth permission
degrades to the fallback rather than failing the assignment, and it passes
clear_on_auth_failure=False because a background caller cannot tell a real
revocation from a lost refresh-rotation race.
configure_ams_slot sent the cloud setting_id as tray_info_idx when it found no
real filament id. That field is 8 characters on the printer -- exactly the width
of a local preset id, less than half a cloud one. Measured on the reporter's A1:
sent PFUS9ddc938fe3ab8f, the tray read back PFUS9DDC, acknowledged as a success.
The slot then resolved to nothing, so the slicer showed Generic anyway and the
calibration table, keyed by the same field, lost the slot. It now falls back to
the slot's existing filament id or the generic for the material, and the route's
guard was aligned with the resolver's so both refuse the same four shapes from
one shared definition.
This reverses the contract #1053 pinned. Six tests asserted that the PFUS
belonged in tray_info_idx; the A1 capture shows it never worked, so they were
rewritten with the measurement in their docstrings.
Verified against 874 AMS trays across twelve models in the support archive: 92
already carry a custom "P" + 7 hex filament id, which is what confirms the
mechanism works and this is a lookup failure rather than a platform limit. No
tray on any model carries a setting_id, so a profile with no filament_id of its
own still cannot be told apart from its base.
GHSA-cp6q-959q-f8rh: @tiptap/core's mergeAttributes() copies keys out of
Object.entries() with plain bracket assignment, so an own __proto__ key
from JSON hits the legacy prototype setter rather than writing a
property. The result carries an attacker-controlled prototype while
Object.keys() and own-property checks show nothing, and ProseMirror's
DOMSerializer.renderSpec() enumerates attribute objects with for...in --
so inherited src and onerror land on a rendered <img> and execute.
Medium, CVSS 4.0 6.4, fixed in 3.30.4.
Not reachable here. The advisory needs an untrusted object arriving at
mergeAttributes(), or a custom or dynamic extension that preserves the
attribute object. Nothing under frontend/src calls mergeAttributes or
defines an extension, and RichTextEditor builds a fixed schema from
StarterKit plus six stock extensions whose HTMLAttributes are static
literals. Content crosses as an HTML string rather than JSON, so no own
__proto__ key reaches an attrs object at all -- the DOM parser only
fills attributes the schema declares -- and every read-only render is
sanitized.
Lockfile only: package.json already declared ^3.11.1, so the patched
line was inside the range and only the stale lock held 3.19.0. No
overrides entry needed.
@tiptap/pm has narrowed its dependency set, so prosemirror-markdown,
prosemirror-menu, prosemirror-collab, prosemirror-schema-basic,
prosemirror-trailing-node, markdown-it and linkify-it leave the tree --
16 packages, none imported by this repo. That retires the reachability
note carried for linkify-it in 1.2.5.
eslint, build with the Safari 16 baseline check, i18n parity and 3514
frontend tests across 256 files all pass. npm audit --omit=dev, which
is what CI gates on, reports zero vulnerabilities.
Backend Tests (shard 1/4) timed out after 10 minutes on e8b901f54
("Updated BACKERS"), a docs-only commit. The step was not hung: it
reached 98%, every test passing, and was killed about 12 seconds short
of finishing.
pytest-split balances by duration only when it has a durations file,
and there was none. Without one it splits by test COUNT -- 2926 / 2926
/ 2926 / 2925, exactly 25% each, which is what the comment here claimed
was good enough. Count is not time. Measured over the full suite
(11703 tests, 1045.6s):
shard 1 2926 tests 658.2s
shard 2 2926 tests 173.1s
shard 3 2926 tests 113.3s
shard 4 2925 tests 101.1s
Shard 1 was carrying 63% of the suite's runtime -- a 6.5x spread -- and
had been walking toward the cap for weeks: 475s, 404s, 445s, then 587s
on 08-30, thirteen seconds under it, and 613s here. CI printed the
reason in its own log on every run: "[pytest-split] No test durations
found."
Commit tests/.test_durations and pass --durations-path explicitly:
shard 1 1273 tests 261.6s
shard 2 1070 tests 261.5s
shard 3 1522 tests 262.0s
shard 4 7838 tests 260.6s
A 1.01x spread. The lopsided test counts are the point: shard 4 takes
thousands of fast unit tests, shard 1 keeps the slow integration ones.
The four groups still partition the suite exactly -- union is 11703
with nothing dropped or duplicated, and all four pass.
--durations-path has to be explicit because pytest-split defaults it to
$CWD/.test_durations, and the two matrices run from different
directories: the native job from backend/, the Docker job from the
image root. Left implicit, the Docker one finds nothing and silently
falls back to the count split. Node IDs match across both because
backend/tests/pytest.ini pins rootdir to backend/tests either way, so a
single file serves them both; verified the file clears .dockerignore
and lands at /app/backend/tests/.test_durations at full size.
timeout-minutes 10 -> 15 for headroom, since the file goes stale as
tests are added. Staleness degrades slowly rather than breaking --
unknown tests are treated as average.
asyncio's Server has a __dict__ and uvloop's, a Cython cdef class, does
not, so the attribute added in 1.2.5.4 raised AttributeError under uvloop.
Every RTSP camera failed before opening a socket, which is the
diagnostic's capture_exception at 0 ms.
Our own unit files all pin --loop asyncio for #1896 and were never
affected. The reports come from units we do not write: the Proxmox VE
Helper-Scripts LXC pins no loop, and installs predating that fix never
gained the flag because update.sh does not rewrite unit files. The loop
is not ours to assume, so fix the code rather than add another flag.
The set moves to a module-level WeakKeyDictionary, keyed weakly so an
abandoned proxy retires its own entry rather than leaking one and later
handing a new server a dead one's handlers.
Pinned on a real uvloop loop and, for hosts without uvloop, against a
__slots__ server; conftest builds its loop from the default policy, so
nothing in the suite had ever run the branch that broke.
Also routes the two external-camera teardowns through close_tls_proxy,
which #2968 introduced and left them out of.
-----
Say so at startup when running on uvloop (issue #3001)
An install on the wrong loop had no way to find out it was. #3001 was
loud enough to notice; the #1896 upload truncation it is also exposed to
is silent, and shows up as a print failing from a file that was corrupt
on arrival.
One WARNING in the lifespan naming the loop, the risk and the flag to
add. A warning and not a refusal: uvicorn has already chosen its loop by
the time any application code runs, and a server that answers requests
beats one that will not boot.
Asks the running loop what it is rather than whether uvloop imports --
uvicorn[standard] installs uvloop everywhere, so its presence says
nothing -- and matches on the module name so the question never imports
uvloop on a host without it.
-----
Repair a service file written before the --loop asyncio pin (issue #3001)
install.sh has pinned the loop since #1896, but nothing has ever
rewritten an existing service file, so every native install created
between 2025-11-28 (when uvicorn[standard] brought uvloop into the venv)
and 2026-07-05 still runs on uvloop no matter how often it is updated.
Both update scripts now add the flag themselves while the service is
stopped, so it takes effect on the same restart -- systemd via sed,
launchd via PlistBuddy, each backing the file up first and inserting
nothing but the flag.
Refuses to edit and explains instead when the shape is not a plain
single-line uvicorn unit: a wrapper script, a continued ExecStart,
several of them, a read-only file, or a service with drop-ins, since a
drop-in may be what defines ExecStart and editing the fragment would
change nothing while reporting success. A deliberate --loop uvloop is
left alone. Reads the effective ExecStart from systemd rather than the
file, so it is idempotent.
CodeQL reports the context as allowing TLS 1.0 and 1.1, and it is right
about the mechanism: create_default_context() leaves minimum_version at
MINIMUM_SUPPORTED, which is the build's floor rather than a guarantee.
That is the reason every context in backend/app pins it, the reason
bambu_ftp.py carries a comment saying so, and the reason this same file
already pins it for the TLS-1.3 case further down. Line 127 was the one
that did not.
The floor cannot change what the test measures. The fixture answers with
a plain FTP banner and speaks no TLS, so the handshake still fails as
WRONG_VERSION_NUMBER, which is the assertion this test exists to make.
The PR gate reported four new alerts, all in test code. A fixture's
/tmp/x.3mf is a column value the migration under test UPDATEs, not a
path anything opens. Two f-string statements interpolate column names
from a literal list declared two lines above, with the id bound - a
column name cannot be a bind parameter, which is why it is written into
the string at all. The two joins move onto their own lines because a
trailing marker would have taken line 64 past the 120-character limit.
The fourth is a near miss rather than a finding: the line below it
already carries the marker, as do four other wildcard sites in the same
file. The wildcard is what that test exists to assert about.
The line carried a noqa for ruff's S608 but nothing bandit reads, so the
same rule was silent in one tool and reported as a medium SQL-injection
finding in the other.
Nothing is interpolated but `table`, which the loop takes from a literal
tuple on the next line; the key and the label list are both bound
parameters. A table name cannot be one, which is why it is written into
the string at all.
An archive that showed the green GCODE badge could re-import into the
File Manager as a source-only project with no Print button, seemingly at
random.
Nothing was ever lost from the file. The download serves the stored
bytes verbatim and the G-code was still in the zip; the two sides simply
asked different questions. Archives looked inside the file. The library
looked at the filename. So a sliced 3MF stored as Foo.3mf rather than
Foo.gcode.3mf earned the badge and lost the Print button, and which one
you got depended on how the print had reached the printer -- a slicer's
LAN send names it .gcode.3mf, a per-plate export or a cloud-dispatched
print does not.
Both sides now ask one shared predicate about the zip itself, and every
route into the library classifies on content. Only the central directory
is read, and only when the name has not already settled it, so ingest
costs nothing extra -- the external scan opens each 3MF for its
thumbnail regardless. Rows already stored are re-checked once, internal
ones only: an external row points at a mount that may be slow or absent,
and startup is the worst place to discover that.
The Slice action moves with it. Its refusal to slice an output was as
name-bound as the Print gate, and without that a file that correctly
gained a Print button would have offered to re-slice its own G-code.
Pull a Bambu ABS Orange out of A1, put a PLA Matte Dark Blue in, and the
slot card still read "Bambu ABS" against the new colour until the page
was reloaded.
Three things stood between the swap and a correct card.
The RFID auto-assign rewrites the slot's slot_preset_mappings row and
then broadcast an event that refreshed everything except the query that
reads it. Only the manual assign path invalidated that one.
Those queries then sat behind the 3s cascade debounce, which exists for
print completion, where one event fans out across half the app. A swap
touches one slot and the user is standing at the printer looking at the
card; worse, the timer restarts on every further event, so a busy moment
could defer it indefinitely. Slot changes now invalidate immediately.
And the card trusted the stored preset over live telemetry outright.
That priority is why a hand-picked preset name stays on a slot, but it
also let a cached row outrank what the printer was reporting. The row is
now skipped when it names a different official Bambu filament than the
tray does, so the card is right from the status push alone. User and
local presets carry ids that genuinely cannot be compared and are left
exactly as they were.
Spoolman mode was the worse half of the same bug: its AMS sync writes
the same row but announced nothing at all, so there was no event to
refresh on. It now reports each slot it changed or cleared.
A project with sub-projects drew every one of them expanded, at every
level, with nothing to shut. Over a three-level hierarchy and a couple of
hundred archives that makes the page one long scroll.
Each group's caption is now a chevron that folds that group, and a
Collapse pill next to the status filter tabs sets the default for the
page and is remembered across reloads. A shut group takes one grid cell
rather than a full-width row, so folding a deep tree actually gets the
page back.
The count on the caption is of the cards nested there, not the parent
card's badge: the API counts sub-projects across every status on purpose,
so under the Active filter the badge can legitimately say 2 where one
card unfolds. A count that disagrees with what unfolds is worse than no
count at all.
When a print's own 3MF cannot be fetched the usage tracker borrows one from the
library or a previous archive, matching on the filename stem. That is far weaker
evidence than it looks: Bambu Studio writes the printer-side filename from the
project's Title metadata, so every plate of a project reaches the printer under
one name however the file was renamed on disk.
The reporter's single-filament job was handed a previous archive's three-filament
plate. Three spools were debited for material that was never extruded, and
nothing on the archive said the numbers were someone else's.
A candidate is now rejected when it positively contradicts the print - a
different plate, or a filament count the slicer's ams_mapping disagrees with -
and the accepted one is logged with both expectations. Only on a contradiction:
the plate needs firmware that echoes it and the count needs a print command
Bambuddy saw, and refusing everything uncorroborated would retire the fallback
recovery this same issue asked for.
The count is scoped to one plate or not compared at all. Unscoped, the filament
reader collects every <filament> in the file, and that sum against one plate's
count would reject every multi-plate library upload on exactly the firmwares
that cannot tell us the plate.
-----
Give a download the time the file needs, and one printer at a time (issue #2957)
ftp_timeout is handed to every download as both the socket inactivity timeout
and the whole-transfer deadline, which makes its 30s default a cap on how big a
file a printer may serve. The reporter measured one 5.4 MB 3MF at 45s off a worn
P1S SD card and 25s off a new one, and a 15.15 MB 3MF at 105s. None of those
links were broken - they were slow, which is what the inactivity timeout exists
to tell apart.
download_to_file already asks for SIZE. It now reports it, and the total
deadline follows the file at the 25 KB/s floor _upload_deadline has used since
do, so #2572's cap on the executor queue wait is untouched. Capped at 300s for a
reason that is not about FTP: on_print_start holds a pooled DB connection across
its whole 3MF hunt.
Downloads also take turns per printer now. He watched Bambu Studio lose its own
connection while Bambuddy pulled a 12 MB 3MF, and a later log caught two
Bambuddy downloads of the same file overlapping at print start. The gate is
soft - whoever cannot have it within 30s goes anyway, because a print losing its
3MF to queueing is worse than the contention, and a soft gate cannot deadlock.
It is also meaningful for the first time. The 90s cap on a multi-path lookup
returned while its worker kept walking the remaining paths, still on the
printer's socket; that walk is now cancelled and waited out before the printer
is handed on.
-----
Look in the shared 3MF cache again before each cover retry (issue #2957)
The cover endpoint and the print-start archive flow share a cache so whichever
fetches the 3MF first hands it to the other (#972). The cover consulted it once
on the way in, then retried for up to two and a half minutes without looking
again.
In the reporter's log the archive flow published the file 42 seconds into that
sequence and the cover's third attempt still pulled its own 5,250,969-byte copy,
off a printer that was mid-print on the same SD card.
A file picked up that way is left alone rather than re-registered under this
endpoint's own name or deleted on the way out. It is the archive flow's.
The forward fix reads the array the fitted plate points at, but only for
archives made after it. Everything already in the library stays blank, and
preheat keeps falling back to the keep-warm bed temperature whenever one of
those jobs is reprinted from the queue - 0 of 455 real 3MFs had resolved.
A one-shot pass re-reads the 3MF already on disk, gated by a settings flag the
way #2614's repair is: the rows it cannot fill are exactly the ones it would
reopen every boot. It fills NULLs only. Nothing recorded is overwritten, an
archive whose file is gone stays NULL, and a corrupted 3MF is skipped rather
than failing startup.
The plate mapping moves to threemf_tools.bed_temperature_from_config so the
ingest path and the repair cannot read a 3MF differently - the same drift
move.
_extract_settings_from_content is deleted. Nothing called it anywhere in the
repo, and it carried the old bed_temperature mapping this issue fixed.
ffmpeg opens every run with ~20 lines of version and build banner and prints
its diagnosis last, so the stderr[:200] eight of the nine call sites used kept
the banner and threw the error away. The reporter's twelve capture failures all
read "ffmpeg version 7.1.4 ... configuration: --prefix=/usr --extra-version=",
identical on every install; the exit code was the only usable byte.
The banner-stripping summariser written for #925 lived private to the camera
route. It now lives in backend/app/utils/ffmpeg_output.py and every ffmpeg and
ffprobe stderr goes through it. Two things the scattered copies also got wrong:
four logged the input URL unmasked, publishing a printer access code or camera
password, and four called a bare .decode() on bytes ffmpeg copies stream
fragments into.
-----
Delete the files a no-3MF archive owns, without taking a printer folder
Both delete paths derived the directory from file_path, which such an archive
does not have, so they removed nothing and logged it at ERROR under a SECURITY
banner. That was true when the archive was an empty row and stopped being true
once one could hold a timelapse and finish photos in <archive_dir>/<id>/ and an
uploaded source in archive/no_source/<id>/.
The two are cleaned up by different means, because <archive_dir>/<id> shares a
namespace with the per-printer folders: a normal archive lives at
<archive_dir>/<printer_id>/<timestamp>_<name>/, so archive/1 is printer 1's
folder and also the directory the shared helper hands archive id 1. Ids come
from unrelated sequences, so the first few archives collide with the printers
on every install, and an rmtree there takes every print that printer made --
measured on a scratch tree. no_source/<id> is a level deeper under a name no
printer id can take and is removed whole; the id-named directory gives up only
its photos subdirectory and the video the row records, then goes only if that
left it empty. The depth guard moves from one to two for the same reason: a
file_path that lost a path component could point the delete at a printer
folder, and no archive directory has been one level deep since the first
commit.
Hard delete had its own copy of these rules, which the helper's docstring says
it exists to prevent, and it had diverged -- it skipped the print-log thumbnail
cleanup whenever a guard tripped.
-----
Stop the RTSPS proxy leaving a handler behind at shutdown
asyncio.start_server keeps only a weak reference to the connection callback's
task, so a handler still awaiting its forwarders could be collected while
pending -- "Task was destroyed but it is pending!", at ERROR with a traceback
into camera.py, once every few hundred snapshots. Teardown had the matching
gap: server.close() leaves established connections running, so the close waited
on a handler that only finishes when the peer drops, and ffmpeg has already
been reaped by then.
Handlers are held for as long as they run and cancelled at shutdown, which is
Server.close_clients() by hand -- that landed in 3.13 and Bambuddy supports
3.10. Both the snapshot path and the streaming endpoint share the shutdown.
BambuStudio writes no bed_temperature key. It stores a per-filament array per
plate type and names the fitted plate in curr_bed_type; bed_temperature is the
Orca/Prusa spelling, so the lookup matched nothing and every archive from a
Bambu slice stored NULL - 0 of 455 real 3MFs resolved. Preheat then fell back
to the keep-warm bed temperature on jobs that had one all along.
Plate names and the plate-to-key mapping are BambuStudio's own get_bed_temp_key
and get_bed_temp_1st_layer_key. First-layer value preferred, highest entry in
the per-filament array taken, an all-zero array left unrecorded.
Manual flow dynamics has two shapes and each reports under its own name with
no auto_ prefix. pa_pattern_calib_mode was already filtered; pa_line_calib_mode
was not, so it still swept FTP for a 3MF that cannot exist and wrote a no-3MF
archive named after the calibration.
An archive that showed the green GCODE badge could re-import into the
File Manager as a source-only project with no Print button, seemingly at
random.
Nothing was ever lost from the file. The download serves the stored
bytes verbatim and the G-code was still in the zip; the two sides simply
asked different questions. Archives looked inside the file. The library
looked at the filename. So a sliced 3MF stored as Foo.3mf rather than
Foo.gcode.3mf earned the badge and lost the Print button, and which one
you got depended on how the print had reached the printer -- a slicer's
LAN send names it .gcode.3mf, a per-plate export or a cloud-dispatched
print does not.
Both sides now ask one shared predicate about the zip itself, and every
route into the library classifies on content. Only the central directory
is read, and only when the name has not already settled it, so ingest
costs nothing extra -- the external scan opens each 3MF for its
thumbnail regardless. Rows already stored are re-checked once, internal
ones only: an external row points at a mount that may be slow or absent,
and startup is the worst place to discover that.
The Slice action moves with it. Its refusal to slice an output was as
name-bound as the Print gate, and without that a file that correctly
gained a Print button would have offered to re-slice its own G-code.
Pull a Bambu ABS Orange out of A1, put a PLA Matte Dark Blue in, and the
slot card still read "Bambu ABS" against the new colour until the page
was reloaded.
Three things stood between the swap and a correct card.
The RFID auto-assign rewrites the slot's slot_preset_mappings row and
then broadcast an event that refreshed everything except the query that
reads it. Only the manual assign path invalidated that one.
Those queries then sat behind the 3s cascade debounce, which exists for
print completion, where one event fans out across half the app. A swap
touches one slot and the user is standing at the printer looking at the
card; worse, the timer restarts on every further event, so a busy moment
could defer it indefinitely. Slot changes now invalidate immediately.
And the card trusted the stored preset over live telemetry outright.
That priority is why a hand-picked preset name stays on a slot, but it
also let a cached row outrank what the printer was reporting. The row is
now skipped when it names a different official Bambu filament than the
tray does, so the card is right from the status push alone. User and
local presets carry ids that genuinely cannot be compared and are left
exactly as they were.
Spoolman mode was the worse half of the same bug: its AMS sync writes
the same row but announced nothing at all, so there was no event to
refresh on. It now reports each slot it changed or cleared.
A project with sub-projects drew every one of them expanded, at every
level, with nothing to shut. Over a three-level hierarchy and a couple of
hundred archives that makes the page one long scroll.
Each group's caption is now a chevron that folds that group, and a
Collapse pill next to the status filter tabs sets the default for the
page and is remembered across reloads. A shut group takes one grid cell
rather than a full-width row, so folding a deep tree actually gets the
page back.
The count on the caption is of the cards nested there, not the parent
card's badge: the API counts sub-projects across every status on purpose,
so under the Active filter the badge can legitimately say 2 where one
card unfolds. A count that disagrees with what unfolds is worse than no
count at all.
When a print's own 3MF cannot be fetched the usage tracker borrows one from the
library or a previous archive, matching on the filename stem. That is far weaker
evidence than it looks: Bambu Studio writes the printer-side filename from the
project's Title metadata, so every plate of a project reaches the printer under
one name however the file was renamed on disk.
The reporter's single-filament job was handed a previous archive's three-filament
plate. Three spools were debited for material that was never extruded, and
nothing on the archive said the numbers were someone else's.
A candidate is now rejected when it positively contradicts the print - a
different plate, or a filament count the slicer's ams_mapping disagrees with -
and the accepted one is logged with both expectations. Only on a contradiction:
the plate needs firmware that echoes it and the count needs a print command
Bambuddy saw, and refusing everything uncorroborated would retire the fallback
recovery this same issue asked for.
The count is scoped to one plate or not compared at all. Unscoped, the filament
reader collects every <filament> in the file, and that sum against one plate's
count would reject every multi-plate library upload on exactly the firmwares
that cannot tell us the plate.
-----
Give a download the time the file needs, and one printer at a time (issue #2957)
ftp_timeout is handed to every download as both the socket inactivity timeout
and the whole-transfer deadline, which makes its 30s default a cap on how big a
file a printer may serve. The reporter measured one 5.4 MB 3MF at 45s off a worn
P1S SD card and 25s off a new one, and a 15.15 MB 3MF at 105s. None of those
links were broken - they were slow, which is what the inactivity timeout exists
to tell apart.
download_to_file already asks for SIZE. It now reports it, and the total
deadline follows the file at the 25 KB/s floor _upload_deadline has used since
do, so #2572's cap on the executor queue wait is untouched. Capped at 300s for a
reason that is not about FTP: on_print_start holds a pooled DB connection across
its whole 3MF hunt.
Downloads also take turns per printer now. He watched Bambu Studio lose its own
connection while Bambuddy pulled a 12 MB 3MF, and a later log caught two
Bambuddy downloads of the same file overlapping at print start. The gate is
soft - whoever cannot have it within 30s goes anyway, because a print losing its
3MF to queueing is worse than the contention, and a soft gate cannot deadlock.
It is also meaningful for the first time. The 90s cap on a multi-path lookup
returned while its worker kept walking the remaining paths, still on the
printer's socket; that walk is now cancelled and waited out before the printer
is handed on.
-----
Look in the shared 3MF cache again before each cover retry (issue #2957)
The cover endpoint and the print-start archive flow share a cache so whichever
fetches the 3MF first hands it to the other (#972). The cover consulted it once
on the way in, then retried for up to two and a half minutes without looking
again.
In the reporter's log the archive flow published the file 42 seconds into that
sequence and the cover's third attempt still pulled its own 5,250,969-byte copy,
off a printer that was mid-print on the same SD card.
A file picked up that way is left alone rather than re-registered under this
endpoint's own name or deleted on the way out. It is the archive flow's.
The forward fix reads the array the fitted plate points at, but only for
archives made after it. Everything already in the library stays blank, and
preheat keeps falling back to the keep-warm bed temperature whenever one of
those jobs is reprinted from the queue - 0 of 455 real 3MFs had resolved.
A one-shot pass re-reads the 3MF already on disk, gated by a settings flag the
way #2614's repair is: the rows it cannot fill are exactly the ones it would
reopen every boot. It fills NULLs only. Nothing recorded is overwritten, an
archive whose file is gone stays NULL, and a corrupted 3MF is skipped rather
than failing startup.
The plate mapping moves to threemf_tools.bed_temperature_from_config so the
ingest path and the repair cannot read a 3MF differently - the same drift
move.
_extract_settings_from_content is deleted. Nothing called it anywhere in the
repo, and it carried the old bed_temperature mapping this issue fixed.
ffmpeg opens every run with ~20 lines of version and build banner and prints
its diagnosis last, so the stderr[:200] eight of the nine call sites used kept
the banner and threw the error away. The reporter's twelve capture failures all
read "ffmpeg version 7.1.4 ... configuration: --prefix=/usr --extra-version=",
identical on every install; the exit code was the only usable byte.
The banner-stripping summariser written for #925 lived private to the camera
route. It now lives in backend/app/utils/ffmpeg_output.py and every ffmpeg and
ffprobe stderr goes through it. Two things the scattered copies also got wrong:
four logged the input URL unmasked, publishing a printer access code or camera
password, and four called a bare .decode() on bytes ffmpeg copies stream
fragments into.
-----
Delete the files a no-3MF archive owns, without taking a printer folder
Both delete paths derived the directory from file_path, which such an archive
does not have, so they removed nothing and logged it at ERROR under a SECURITY
banner. That was true when the archive was an empty row and stopped being true
once one could hold a timelapse and finish photos in <archive_dir>/<id>/ and an
uploaded source in archive/no_source/<id>/.
The two are cleaned up by different means, because <archive_dir>/<id> shares a
namespace with the per-printer folders: a normal archive lives at
<archive_dir>/<printer_id>/<timestamp>_<name>/, so archive/1 is printer 1's
folder and also the directory the shared helper hands archive id 1. Ids come
from unrelated sequences, so the first few archives collide with the printers
on every install, and an rmtree there takes every print that printer made --
measured on a scratch tree. no_source/<id> is a level deeper under a name no
printer id can take and is removed whole; the id-named directory gives up only
its photos subdirectory and the video the row records, then goes only if that
left it empty. The depth guard moves from one to two for the same reason: a
file_path that lost a path component could point the delete at a printer
folder, and no archive directory has been one level deep since the first
commit.
Hard delete had its own copy of these rules, which the helper's docstring says
it exists to prevent, and it had diverged -- it skipped the print-log thumbnail
cleanup whenever a guard tripped.
-----
Stop the RTSPS proxy leaving a handler behind at shutdown
asyncio.start_server keeps only a weak reference to the connection callback's
task, so a handler still awaiting its forwarders could be collected while
pending -- "Task was destroyed but it is pending!", at ERROR with a traceback
into camera.py, once every few hundred snapshots. Teardown had the matching
gap: server.close() leaves established connections running, so the close waited
on a handler that only finishes when the peer drops, and ffmpeg has already
been reaped by then.
Handlers are held for as long as they run and cancelled at shutdown, which is
Server.close_clients() by hand -- that landed in 3.13 and Bambuddy supports
3.10. Both the snapshot path and the streaming endpoint share the shutdown.