Post work PR #2853

This commit is contained in:
maziggy
2026-08-23 12:45:52 +02:00
5 changed files with 163 additions and 154 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ All notable changes to Bambuddy will be documented in this file.
## [1.2.5.4] - Unreleased
### Added
- **Printer file downloads can be selected in ranges and print-history videos can be downloaded (#2850)** — The printer file browser's multi-select download now prepares large selections on the app data volume instead of buffering them in server and browser memory, uses per-file compression (videos stored, G-code/3MF compressed), reports partial results and preparation progress, supports cancellation, rejects over-large or under-space selections, and preserves the legacy API contract. Shift-click selects a contiguous visible range and hidden selections are discarded when navigating or filtering. Print History now offers attached timelapses, matching printer timelapses, and `/ipcam` chunks when available; offline or unreadable storage is distinguished from an empty directory. Download tokens are single-use and resource-bound, API-key printer allowlists are enforced, FTP short reads are rejected, and abandoned staging is pruned. Translated in all locales; wiki updated. Covered by backend and frontend regression tests.
- **Printer file downloads can be selected in ranges and print-history videos can be downloaded (#2850, requested and contributed by @logikal in #2853)** — The printer file browser's multi-select download now prepares large selections on the app data volume instead of buffering them in server and browser memory, uses per-file compression (videos stored, G-code/3MF compressed), reports partial results and preparation progress, supports cancellation, rejects over-large or under-space selections, and preserves the legacy API contract. Shift-click selects a contiguous visible range and hidden selections are discarded when navigating or filtering. Print History now offers attached timelapses, matching printer timelapses, and `/ipcam` chunks when available; offline or unreadable storage is distinguished from an empty directory. Download tokens are single-use and resource-bound, API-key printer allowlists are enforced, FTP short reads are rejected, and abandoned staging is pruned. Translated in all locales; wiki updated. Covered by backend and frontend regression tests.
- **Bambuddy now asks a printer that refuses FTPS what it actually said (#2780, measured by @grolmus)** — When a printer's file service answers port 990 with something that is not TLS, Python reports `[SSL: WRONG_VERSION_NUMBER]` and the bytes that caused it are gone, consumed by the TLS layer before the error surfaces. That has left #2780 open on a theory rather than a finding. The client now opens one plain connection straight afterwards and reads what the printer says, so the log carries the printer's own words — an FTP refusal such as `421 Too many connections` would identify the fault outright — and the line is marked as the one to quote in a report. Reading nothing is informative too, and says so: a healthy implicit-FTPS service stays silent until it gets a handshake, so silence means the refusal had already passed. It asks once per cool-off window rather than once per attempt, which keeps it to one extra connection per printer per five minutes — the suspected fault is a printer running out of connections, so the diagnosis must not add to it. What made this worth doing is a measurement from a nine-printer farm, reproduced here: a cleartext banner on the TLS port produces exactly the error the field reports, a genuine TLS version mismatch produces a different one, and a client with no version cap reaches a TLS-1.2-only peer unaided. So this failure was never a TLS-version problem, and the per-model `cap_tls_v1_2` knob cannot affect it. Two of the three entries carrying that knob were added on the belief that it could; they are kept, since their reporters saw the symptom clear and nobody here has the hardware to re-test on, but they are now marked for re-test and the reasoning recorded next to them is what was measured rather than what was assumed. Both measurements are pinned by tests, so the explanation stays falsifiable.
- **The K value is on the AMS slot itself, not only in the popover (#2532, requested and contributed by @gyrene2083)** — Reading back a slot's pressure-advance value meant hovering it: the K factor lived in the filament popover alone, so checking whether a calibration had actually taken across four slots was four hovers, and comparing two of them side by side was not possible at all. Every slot card now carries the value under the material name, the way Bambu Studio shows it per slot — on regular AMS units, on AMS-HT, and on the external spool of a dual-nozzle machine. Only a value the printer actually reported is shown: a loaded but never-calibrated slot stays blank rather than inheriting the 0.020 that fills the popover's own field, and a slot the firmware reports as exactly 0 counts as uncalibrated the same way the stored K-profiles do. The label is shortened to **K** with the full localized name on hover, because "K Factor", "K-Faktor" and "Facteur K" ate the value itself — the whole point of the line — on cards under about 350px, and the figure is set in tabular numerals so it measures the same in Safari as in Chromium. Where one slot of a unit is calibrated and its neighbours are not, the neighbours hold the same row open so the fill bars stay level across the card.
- **Filament Track Switch: the inlet each AMS feeds, and K-profiles that follow it** — With a switch fitted an AMS is not wired to a nozzle any more. It is plumbed into one of the switch's two inlets and reaches both hotends through it, so every unit reports its extruder as "not fixed" and `ams_extruder_map` comes back empty. Bambuddy had nothing to fall back on but the AMS unit number, so AMS-A was badged R and AMS-B was badged L purely because their ids are 0 and 1, a third unit got no badge at all, and every one of those labels was wrong; the SpoolBuddy assign modal had the same fallback in a worse form, mapping anything that was not extruder 1 to R. The binding needed no new telemetry — it sits in bits 24-27 of the same AMS info string Bambuddy already parses for the unit's type and extruder id, and is read only when a switch is installed, because without one "not fixed" really does mean an uninitialised unit. The badge keeps L and R, in its own colour, with the inlet named in full in the tooltip, since the letter is the inlet's position and not a claim about which nozzle that AMS feeds: the switch can route either inlet to either outlet. Both views update live, which took adding the switch fields to the WebSocket payload and to the broadcast dedup key — the binding is not part of the AMS change hash and must stay out of it, because that hash drives Spoolman sync. The calibration half is where it bites. K-profiles are numbered per nozzle, so the same index means a different profile on each hotend, and a tray holds exactly one index: move an AMS to the other inlet and every configured slot silently kept pointing at the old hotend's table. Measured on an H2C, a black PLA calibrated 0.018 left and 0.020 right stayed on the left profile after the move, and a manual RFID re-read only re-asserted the same wrong one. Three separate copies of "which extruder is this slot on" each ended in `else 0`, which on a switch machine filed every profile under the right-hand nozzle; they now share one resolver that returns unknown as its own answer, because unknown and extruder 0 are very different things on a dual-nozzle machine. Moving an AMS now re-selects each configured slot's counterpart profile for the nozzle it has arrived on, and only for spools that already have one there — a slot nobody has configured, or a spool calibrated on one hotend only, is left exactly as the operator set it. Configure Slot resolves against the slot's own nozzle throughout: options name the hotend, a filament calibrated on both gives two distinguishable entries, matches are scoped to the nozzle the slot feeds, and the other hotend's profiles stay reachable under Other K profiles. The print dialog's slot dropdown picks up the same inlet labelling and notes when every filament a print needs sits behind one inlet, which is legal but slow — a change between two spools on the same inlet retracts all the way back to the AMS, where a change across the two only retracts as far as the switch. Assigning an AMS to an inlet stays on the printer: Bambu Studio can read that binding and has no command to write it.
@@ -346,6 +346,14 @@ describe('ArchivesPage', () => {
});
describe('timelapse management', () => {
// A card reaches print-video downloads through its context menu; only the
// list row keeps an action-bar button for it (#2853).
const openCardPrintVideos = async (cardIndex = 0) => {
const triggers = await screen.findAllByTitle('Right-click for more options');
fireEvent.click(triggers[cardIndex]);
return screen.findByRole('button', { name: 'Download print videos' });
};
it('keeps an attached timelapse available without printer-file permission', async () => {
setAuthToken('archive-only-token', 'session');
server.use(
@@ -377,12 +385,16 @@ describe('ArchivesPage', () => {
render(<ArchivesPage />);
const mediaButton = (await screen.findAllByTitle('Download print videos'))[0];
expect(mediaButton).toBeEnabled();
const deniedButtons = screen.getAllByTitle('You do not have permission to access printer files');
expect(deniedButtons.every(button => button.hasAttribute('disabled'))).toBe(true);
// One of the two archives has no attached copy, so without printer-file
// permission there is nothing left for the action to offer.
const deniedItem = await openCardPrintVideos(0);
expect(deniedItem).toBeDisabled();
expect(deniedItem).toHaveAttribute('title', 'You do not have permission to access printer files');
fireEvent.keyDown(document, { key: 'Escape' });
fireEvent.click(mediaButton);
const mediaItem = await openCardPrintVideos(1);
expect(mediaItem).toBeEnabled();
fireEvent.click(mediaItem);
expect(await screen.findByText('attached.mp4')).toBeInTheDocument();
expect(screen.getByText('You do not have permission to access printer files')).toBeInTheDocument();
});
@@ -409,8 +421,7 @@ describe('ArchivesPage', () => {
);
render(<ArchivesPage />);
const mediaButtons = await screen.findAllByTitle('Download print videos');
fireEvent.click(mediaButtons[0]);
fireEvent.click(await openCardPrintVideos());
expect(await screen.findByText('Print videos')).toBeInTheDocument();
expect(await screen.findByText('ipcam-record.2024-01-01_10-05-00.1.mp4')).toBeInTheDocument();
@@ -443,7 +454,7 @@ describe('ArchivesPage', () => {
);
render(<ArchivesPage />);
fireEvent.click((await screen.findAllByTitle('Download print videos'))[0]);
fireEvent.click(await openCardPrintVideos());
fireEvent.click(await screen.findByRole('button', { name: /Download selected \(1\)/i }));
expect(await screen.findByText('Download failed: Not enough app data volume space')).toBeInTheDocument();
+12 -14
View File
@@ -33,6 +33,7 @@ import {
AlertCircle,
Copy,
Film,
FileVideo,
ScanSearch,
QrCode,
Camera,
@@ -619,6 +620,17 @@ function ArchiveCard({
onClick: () => setShowTimelapse(true),
disabled: !archive.timelapse_path,
},
{
label: t('archives.media.download'),
icon: <FileVideo className="w-4 h-4" />,
onClick: () => setShowPrinterMedia(true),
disabled: !archive.timelapse_path && (
!hasPermission('printers:files') || !archive.printer_id || !archive.started_at
),
title: !archive.timelapse_path && !hasPermission('printers:files')
? t('printers.permission.noFiles')
: undefined,
},
{
label: t('archives.menu.scanForTimelapse'),
icon: <ScanSearch className="w-4 h-4" />,
@@ -1383,20 +1395,6 @@ function ArchiveCard({
>
<Globe className={`w-3 h-3 sm:w-4 sm:h-4 ${!archive.external_url && !archive.makerworld_url ? 'opacity-20' : ''}`} />
</Button>
<Button
variant="secondary"
size="sm"
className="min-w-0 p-1 sm:p-1.5"
onClick={() => setShowPrinterMedia(true)}
disabled={!archive.timelapse_path && (
!hasPermission('printers:files') || !archive.printer_id || !archive.started_at
)}
title={!archive.timelapse_path && !hasPermission('printers:files')
? t('printers.permission.noFiles')
: t('archives.media.download')}
>
<Film className="w-3 h-3 sm:w-4 sm:h-4" />
</Button>
<Button
variant="secondary"
size="sm"
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -26,7 +26,7 @@
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-DoAzVOXb.js"></script>
<script type="module" crossorigin src="/assets/index-e8_8xCtK.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DjndScv6.css">
</head>
<body>