maziggy hai 2 semanas
pai
achega
68140747f2

+ 1 - 1
CHANGELOG.md

@@ -5,7 +5,7 @@ All notable changes to Bambuddy will be documented in this file.
 ## [1.2.6b1] - 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.
 

+ 19 - 8
frontend/src/__tests__/pages/ArchivesPage.test.tsx

@@ -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
frontend/src/pages/ArchivesPage.tsx

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

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
static/assets/index-e8_8xCtK.js


+ 1 - 1
static/index.html

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

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio