mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-20 00:25:22 +02:00
fix(library): send camera stream token for 3D Preview plate thumbnails (#2661)
The File Manager 3D Preview dialog (ModelViewerModal) rendered plate thumbnails with the raw thumbnail_url. The plate-thumbnail endpoints are gated behind a camera stream token passed as ?token= (an <img> can't send an Authorization header), so with auth enabled the browser fetched without a token and got 401 "Valid camera stream token required" — broken image icons for every plate. The Slice dialog's picker (PlatePickerModal) and the Print modal's PlateSelector already append the token via withStreamToken(), which is why the same file's thumbnails showed there. Wrap the thumbnail src in withStreamToken(), matching the other two call sites. The token is synced app-wide and withStreamToken() is a no-op when auth is off, so non-auth setups are unchanged.
This commit is contained in:
@@ -5,6 +5,7 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
## [1.2.6b1] - Unreleased
|
||||
|
||||
### Fixed
|
||||
- **3D Preview plate thumbnails were broken (401) in File Manager when login was enabled (#2661, reporter @fbordonaro)** — Opening a multi-plate 3MF via **File Manager → 3D Preview** showed broken-image icons for every plate thumbnail, and the network tab showed `GET /api/v1/library/files/<id>/plate-thumbnail/<plate>` returning **401 "Valid camera stream token required."** The Slice dialog displayed the same file's thumbnails correctly, which is what made it look inconsistent. **Root cause.** The plate-thumbnail endpoints (both archive and library) are gated behind a **camera stream token** passed as a `?token=` query param, because an `<img>` tag can't send an `Authorization: Bearer` header. Every place that renders these thumbnails is supposed to append the token via the `withStreamToken()` helper — `PlatePickerModal` (the Slice dialog's multi-plate picker) and the Print modal's `PlateSelector` both do — but the **3D Preview dialog** (`ModelViewerModal`) rendered the raw `thumbnail_url` with no token, so with auth enabled the browser fetched without one and got a 401. **Fix.** `ModelViewerModal` now wraps the plate thumbnail `src` in `withStreamToken()`, matching the two existing call sites. The token is already synced app-wide (the same global the working pickers read), and `withStreamToken()` is a no-op when auth is off, so nothing changes for non-auth setups. Covered by a component test asserting the plate thumbnail `<img>` carries the `?token=` query param.
|
||||
- **Force color match dispatched a print onto the wrong PLA variant — Matte jobs went to Basic and Silk printers alike, and the wrong AMS slot on a printer holding two same-colour variants (#2650, reporter @MartinNYHC)** — With **Force color match** on, a job sliced for **White PLA Matte** was dispatched to every printer that had *any* white PLA loaded — the ones holding White PLA **Basic** and White PLA **Silk+** included — so a matte model came out glossy on the wrong machine. **Root cause.** Bambu's MQTT status reports every PLA sub-variant as `tray_type == "PLA"`; the Basic/Matte/Silk distinction is carried only in `tray_info_idx` (`GFA00` = Basic, `GFA01` = Matte, `GFA06` = Silk, …), which the 3MF's `slice_info.config` also records per filament. Three places dropped it: the Virtual-Printer queue built each force override as `{slot_id, type, color, force_color_match}` without the parsed `tray_info_idx`; the scheduler's eligibility check (`_get_missing_force_color_slots`) compared loaded trays on `(type, colour)` only — so `(PLA, #FFFFFF)` matched Basic, Matte and Silk indiscriminately and all three printers looked eligible; and the AMS slot mapper cleared `tray_info_idx` when applying the override, so even on the correct printer it could pick a different-variant tray of the same colour. **Fix.** The force override now carries the 3MF's `tray_info_idx`; a slot counts as satisfied only when a loaded tray matches type **and** colour **and** the variant (identical `tray_info_idx`, *or* either side lacks one); and the slot mapper now keeps the variant for force-colour overrides so it pins the matching tray. A blank idx on either side (custom/third-party spools report none, and older 3MFs carry none) falls back to the historical type+colour behaviour, so those setups are unaffected, and a manual filament *swap* (a preference override) still clears the idx so it matches the swapped-in spool rather than the old one. A job sliced for GFA01 now goes only to a printer with GFA01 loaded, and lands on that printer's GFA01 tray. The printer-card queue-compatibility hint (which printers show a pending job as runnable) now applies the same variant rule. Covered by scheduler tests (Matte requirement rejects Basic/Silk, accepts Matte, blank loaded idx falls back, requirement without an idx unchanged; the mapper pins the GFA01 tray over a same-colour GFA00 on both the 3MF and no-3MF paths; a preference swap still matches by colour), a Virtual-Printer test asserting the override carries `tray_info_idx`, and frontend tests for the variant-aware queue hint (rejects other variants, accepts the match, blank-idx and no-variant-data fall back).
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { render } from '../utils';
|
||||
import { ModelViewerModal } from '../../components/ModelViewerModal';
|
||||
import { setStreamToken } from '../../api/client';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { server } from '../mocks/server';
|
||||
|
||||
@@ -385,6 +386,33 @@ describe('ModelViewerModal', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// #2661: plate-thumbnail endpoints are gated behind a camera stream token
|
||||
// (an <img> can't send a Bearer header), so the src must carry ?token=.
|
||||
// Without it the 3D Preview thumbnails 401 while the Slice dialog (which
|
||||
// already appends the token) shows the same file's thumbnails fine.
|
||||
it('appends the camera stream token to plate thumbnail URLs', async () => {
|
||||
setStreamToken('tok-2661');
|
||||
try {
|
||||
render(
|
||||
<ModelViewerModal
|
||||
archiveId={1}
|
||||
title="Test Model"
|
||||
onClose={mockOnClose}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Plate 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const thumb = screen.getByAltText('Plate 1') as HTMLImageElement;
|
||||
expect(thumb.src).toContain('/api/v1/archives/1/plates/1/thumbnail');
|
||||
expect(thumb.src).toContain('token=tok-2661');
|
||||
} finally {
|
||||
setStreamToken(null);
|
||||
}
|
||||
});
|
||||
|
||||
it('allows plate selection via click', async () => {
|
||||
render(
|
||||
<ModelViewerModal
|
||||
|
||||
@@ -5,7 +5,7 @@ import { X, ExternalLink, Box, Code2, Cog, Loader2, Layers, Check, Maximize2, Mi
|
||||
import { ModelViewer } from './ModelViewer';
|
||||
import { GcodeViewer } from './GcodeViewer';
|
||||
import { Button } from './Button';
|
||||
import { api } from '../api/client';
|
||||
import { api, withStreamToken } from '../api/client';
|
||||
import { openInSlicer, type SlicerType } from '../utils/slicer';
|
||||
import type { ArchivePlatesResponse, LibraryFilePlatesResponse, PlateMetadata } from '../types/plates';
|
||||
|
||||
@@ -477,7 +477,7 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
|
||||
>
|
||||
{plate.has_thumbnail && plate.thumbnail_url ? (
|
||||
<img
|
||||
src={plate.thumbnail_url}
|
||||
src={withStreamToken(plate.thumbnail_url)}
|
||||
alt={`Plate ${plate.index}`}
|
||||
className={`${splitFullscreen ? 'w-8 h-8' : 'w-10 h-10'} rounded object-cover bg-bambu-dark-tertiary`}
|
||||
/>
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -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-Bx0t8q7D.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-DYQO8a-5.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-kl51qImb.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user