Просмотр исходного кода

Camera: a card's overlay stays on the Printers page (issue #2737)

- Since #2737 every embedded camera was drawn on every page, also one
  opened from a printer card or the Cam Wall. Each open camera now
  keeps where it was opened: from the sidebar's Cameras button it is
  shown on every page, from a card or the Cam Wall on the Printers
  page only, hidden but open elsewhere and back on returning.
- Opening one from both puts it on every page; a card does not take
  it back. A camera popped back in from its window is on every page.
  Stored with the camera, so it survives a reload; entries from
  before have no scope and stay on every page.
maziggy 12 часов назад
Родитель
Сommit
cdb18152a6

+ 1 - 0
CHANGELOG.md

@@ -217,6 +217,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **The frontend build no longer warns about `path` and `crypto` being externalized for the STEP previewer (#2976)** — `occt-import-js`, the Emscripten build behind STEP previews, requires both modules, but only inside its `ENVIRONMENT_IS_NODE` branches; in the browser it loads its `.wasm` from the URL the preview worker passes and draws randomness from `crypto.getRandomValues`. Vite still externalized both and printed two warnings on every build. `vite.config.ts` now drops exactly those two warnings for that one package through `build.rolldownOptions.onLog`, so an externalization anywhere else, or of any other module, still shows.
 
 ### Fixed
+- **A camera opened from a printer card stays on the Printers page (#2737)** — Since cameras could be opened from any page, every camera overlay was shown on every page, also one opened from a printer card or the Cam Wall, which followed you around the app. Only a camera opened with the sidebar's **Cameras** button is shown everywhere now. One opened from a card or the Cam Wall is hidden on the other pages, still open, and back on returning to Printers; opening it from the **Cameras** button too puts it on every page. Each camera remembers which it is across a reload.
 - **Spoolman mode no longer shows slots from the built-in inventory (#3430, reported by @sean3318)** — Since switching inventory modes keeps both modes' slot assignments, the Inventory page in Spoolman mode also read the built-in inventory's and gave them priority. Its spool numbers are its own, so a built-in assignment kept from before the switch put a printer slot on whichever Spoolman spool had the same number, and nothing in Spoolman mode could clear it: a spool taken out of the AMS kept its slot. Spoolman mode now only uses Spoolman's slot assignments: on the Inventory page, on the printer card and in SpoolBuddy (a slot's fill level came from the built-in spool too), also while Spoolman is unreachable, when the printer card offered to assign and unassign built-in spools. The built-in assignments are kept for when you switch back.
 - **A spool whose tag is read in a slot leaves its other slots (#3427, reported by @Spegeli)** — When an AMS read a spool's tag, Bambuddy assigned the spool to that slot and left any other slot it was assigned to alone. A spool assigned by hand to an empty slot of one printer and then loaded into another was assigned to both: the first printer's slot was configured from it once a spool went in there, and its prints were charged to a spool sitting in the other printer. A tag read now takes the spool out of its other slots, as moving it by hand does; those slots keep what the printer has set, as after unassigning. A slot whose printer reports the same tag keeps its assignment. Internal inventory and Spoolman alike.
 - **AMS Filament Backup pools spools the printer itself treats as backups (#1824, reported by @lightmaster)** — With Filament Backup on, a print short on its assigned spool is only held when all spools of that filament together are short. Which spools count as the same filament was decided from the spools in the inventory, by their preset as stored: a spool created from its RFID tag stores `GFA10`, one set up from the preset picker `GFSA10_02` or a user preset of its own, so two rolls of the same PLA Tough+ never pooled, and Start reported "Insufficient filament for the assigned spool" for a 194 g print with 142 g in one slot and 1000 g in the other, while the printer listed both for Auto Refill. The pool now follows what the printer reports for each tray, its filament preset and colour, which is what it switches on; the remaining grams still come from the inventory. Internal inventory and Spoolman alike, for the queue's Start, the dispatcher and the print dialog's warning.

+ 101 - 8
frontend/src/__tests__/contexts/EmbeddedCameraContext.test.tsx

@@ -1,15 +1,16 @@
 /**
  * Camera overlays from any page (#2737). They used to live in the Printers
- * page, so they disappeared on every other page.
+ * page, so they disappeared on every other page. One opened from a printer
+ * card belongs to the Printers page again; one from the sidebar to every page.
  */
 import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
 import { fireEvent, render as rtlRender, screen, waitFor } from '@testing-library/react';
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { BrowserRouter } from 'react-router-dom';
+import { BrowserRouter, useNavigate } from 'react-router-dom';
 import { AuthProvider } from '../../contexts/AuthContext';
 import { ThemeProvider } from '../../contexts/ThemeContext';
 import { ToastProvider } from '../../contexts/ToastContext';
-import { EmbeddedCameraProvider } from '../../contexts/EmbeddedCameraContext';
+import { EmbeddedCameraProvider, useEmbeddedCameras } from '../../contexts/EmbeddedCameraContext';
 import { http, HttpResponse } from 'msw';
 import { server } from '../mocks/server';
 import { render } from '../utils';
@@ -89,7 +90,7 @@ describe('EmbeddedCameraProvider', () => {
     render(<SomeOtherPage />);
 
     await waitFor(() =>
-      expect(JSON.parse(localStorage.getItem('openEmbeddedCameras')!)).toEqual([{ id: 2, name: 'Office P1S' }]),
+      expect(JSON.parse(localStorage.getItem('openEmbeddedCameras')!)).toEqual([{ id: 2, name: 'Office P1S', scope: 'app' }]),
     );
     await waitFor(() => expect(overlayCount()).toBe(1));
   });
@@ -249,15 +250,15 @@ describe('EmbeddedCameraProvider', () => {
     fireEvent.click(await screen.findByRole('button', { name: /Office P1S/ }));
     await waitFor(() =>
       expect(JSON.parse(localStorage.getItem('openEmbeddedCameras')!)).toEqual([
-        { id: 1, name: 'Workshop X1C' },
-        { id: 2, name: 'Office P1S' },
+        { id: 1, name: 'Workshop X1C', scope: 'app' },
+        { id: 2, name: 'Office P1S', scope: 'app' },
       ]),
     );
 
     fireEvent.click(await screen.findByRole('button', { name: 'Cameras' }));
     fireEvent.click(await screen.findByRole('button', { name: /Office P1S/ }));
     await waitFor(() =>
-      expect(JSON.parse(localStorage.getItem('openEmbeddedCameras')!)).toEqual([{ id: 1, name: 'Workshop X1C' }]),
+      expect(JSON.parse(localStorage.getItem('openEmbeddedCameras')!)).toEqual([{ id: 1, name: 'Workshop X1C', scope: 'app' }]),
     );
   });
 
@@ -337,7 +338,7 @@ describe('CameraLauncher', () => {
     await waitFor(() => expect(overlayCount()).toBe(1));
     // A quick look: the list gets out of the way.
     expect(screen.queryByRole('region', { name: 'Cameras' })).toBeNull();
-    expect(JSON.parse(localStorage.getItem('openEmbeddedCameras')!)).toEqual([{ id: 2, name: 'Office P1S' }]);
+    expect(JSON.parse(localStorage.getItem('openEmbeddedCameras')!)).toEqual([{ id: 2, name: 'Office P1S', scope: 'app' }]);
   });
 
   it('marks an open camera and closes it from the list', async () => {
@@ -439,3 +440,95 @@ describe('CameraLauncher', () => {
     expect(await screen.findByText('No printers')).toBeInTheDocument();
   });
 });
+
+
+describe('Where an overlay is shown, by where it was opened', () => {
+  beforeEach(() => {
+    backStorage();
+    server.use(
+      http.get('/api/v1/printers/', () => HttpResponse.json(PRINTERS)),
+      http.get('/api/v1/printers/:id', ({ params }) =>
+        HttpResponse.json(PRINTERS.find(p => p.id === Number(params.id)) ?? null),
+      ),
+      http.post('/api/v1/printers/:id/camera/stop', () => HttpResponse.json({ success: true })),
+      http.get('/api/v1/printers/:id/camera/status', () => HttpResponse.json({ active: true, stalled: false })),
+    );
+    window.history.pushState({}, '', '/');
+  });
+
+  afterEach(() => {
+    vi.restoreAllMocks();
+    window.history.pushState({}, '', '/');
+  });
+
+  /** Opens printer 2 from a card ('printers') or the sidebar ('app'), and moves between pages. */
+  function Harness() {
+    const { openEmbeddedCamera } = useEmbeddedCameras();
+    const navigate = useNavigate();
+    return (
+      <>
+        <button onClick={() => openEmbeddedCamera(2, 'Office P1S', 'printers')}>from card</button>
+        <button onClick={() => openEmbeddedCamera(2, 'Office P1S', 'app')}>from sidebar</button>
+        <button onClick={() => navigate('/archives')}>to archives</button>
+        <button onClick={() => navigate('/')}>to printers</button>
+      </>
+    );
+  }
+
+  it('a camera opened from a printer card stays on the Printers page, open, and is back on returning', async () => {
+    render(<Harness />);
+    fireEvent.click(screen.getByText('from card'));
+    await waitFor(() => expect(overlayCount()).toBe(1));
+
+    fireEvent.click(screen.getByText('to archives'));
+    await waitFor(() => expect(overlayCount()).toBe(0));
+    expect(JSON.parse(localStorage.getItem('openEmbeddedCameras')!)).toEqual([
+      { id: 2, name: 'Office P1S', scope: 'printers' },
+    ]);
+
+    fireEvent.click(screen.getByText('to printers'));
+    await waitFor(() => expect(overlayCount()).toBe(1));
+  });
+
+  it('a camera opened from the sidebar is shown on every page', async () => {
+    render(<Harness />);
+    fireEvent.click(screen.getByText('from sidebar'));
+    await waitFor(() => expect(overlayCount()).toBe(1));
+    fireEvent.click(screen.getByText('to archives'));
+    await new Promise(resolve => setTimeout(resolve, 50));
+    expect(overlayCount()).toBe(1);
+  });
+
+  it('the sidebar moves a card camera to every page, and a card does not take it back', async () => {
+    render(<Harness />);
+    fireEvent.click(screen.getByText('from card'));
+    fireEvent.click(screen.getByText('from sidebar'));
+    fireEvent.click(screen.getByText('from card'));
+    await waitFor(() => expect(overlayCount()).toBe(1));
+    fireEvent.click(screen.getByText('to archives'));
+    await new Promise(resolve => setTimeout(resolve, 50));
+    expect(overlayCount()).toBe(1);
+    expect(JSON.parse(localStorage.getItem('openEmbeddedCameras')!)).toEqual([
+      { id: 2, name: 'Office P1S', scope: 'app' },
+    ]);
+  });
+
+  it('keeps the scope across a reload: a stored card camera is not shown on another page', async () => {
+    localStorage.setItem('openEmbeddedCameras', JSON.stringify([{ id: 2, name: 'Office P1S', scope: 'printers' }]));
+    window.history.pushState({}, '', '/archives');
+    render(<SomeOtherPage />);
+    await screen.findByText('Archives');
+    await new Promise(resolve => setTimeout(resolve, 100));
+    expect(overlayCount()).toBe(0);
+  });
+
+  it('a camera popped back in from its window is shown on every page', async () => {
+    window.history.pushState({}, '', '/archives');
+    render(<SomeOtherPage />);
+    await screen.findByText('Archives');
+    await waitFor(async () =>
+      expect(await requestCameraPopIn(1, 'Workshop X1C', null, { answerMs: 50 })).toBe(true),
+    );
+    await waitFor(() => expect(overlayCount()).toBe(1));
+  });
+});

+ 10 - 0
frontend/src/__tests__/pages/PrintersPageCameraSplitButton.test.tsx

@@ -184,6 +184,16 @@ describe('PrintersPage — camera split button', () => {
     expect(openSpy).not.toHaveBeenCalled();
   });
 
+  it('an overlay opened from the card belongs to the Printers page', async () => {
+    renderPage('window');
+    await openModeMenu();
+    fireEvent.click(await screen.findByText(/Embedded Overlay/));
+    expect(await screen.findByTitle('Refresh stream')).toBeInTheDocument();
+    expect(JSON.parse(storage.get('openEmbeddedCameras')!)).toEqual([
+      expect.objectContaining({ scope: 'printers' }),
+    ]);
+  });
+
   it('remembers the picked mode for the next visit', async () => {
     renderPage('window');
     await openModeMenu();

+ 1 - 1
frontend/src/components/CameraLauncher.tsx

@@ -169,7 +169,7 @@ export function CameraLauncher({
                       if (isOpen) {
                         closeEmbeddedCamera(printer.id);
                       } else {
-                        openEmbeddedCamera(printer.id, printer.name);
+                        openEmbeddedCamera(printer.id, printer.name, 'app');
                         setOpen(false);
                       }
                     }}

+ 38 - 12
frontend/src/contexts/EmbeddedCameraContext.tsx

@@ -1,5 +1,6 @@
 import { createContext, lazy, Suspense, useCallback, useContext, useEffect, useMemo, useState, type ComponentType, type ReactNode } from 'react';
 import { useQuery, useQueryClient } from '@tanstack/react-query';
+import { useLocation } from 'react-router-dom';
 import { api } from '../api/client';
 import { useAuth } from './AuthContext';
 import { listenForCameraPopIn, registerCameraTab } from '../utils/camera';
@@ -7,11 +8,14 @@ import { recoverFromMissingChunk } from '../utils/lazyPage';
 import type { EmbeddedCameraViewerProps } from '../components/EmbeddedCameraViewer';
 
 /**
- * The floating camera overlays, open on every page of the app (#2737).
+ * The floating camera overlays (#2737).
  *
- * They lived in the Printers page, so they vanished on any other page and
- * only came back on returning to it. Persisted under the same key as then,
- * so overlays left open before the update are still open after it.
+ * One opened from the sidebar's camera button is shown on every page. One
+ * opened from a printer card or the camera wall belongs to the Printers page:
+ * it is hidden on the other pages, still open, and back on returning there.
+ * Persisted under the same key as before #2737, so overlays left open before
+ * an update are still open after it; an entry without a scope is one from
+ * before the scope existed, and stays on every page as it was.
  */
 const STORAGE_KEY = 'openEmbeddedCameras';
 
@@ -31,14 +35,20 @@ const EmbeddedCameraViewer = lazy(async (): Promise<{ default: ComponentType<Emb
   }
 });
 
+/** Where an overlay is shown: every page, or the Printers page only. */
+export type CameraScope = 'app' | 'printers';
+
 export interface OpenCamera {
   id: number;
   name: string;
+  scope: CameraScope;
 }
 
 interface EmbeddedCameraContextType {
   openCameras: OpenCamera[];
-  openEmbeddedCamera: (id: number, name: string) => void;
+  /** Opening one already open for every page keeps it there; opening a
+   *  Printers-page one for every page moves it to every page. */
+  openEmbeddedCamera: (id: number, name: string, scope: CameraScope) => void;
   closeEmbeddedCamera: (id: number) => void;
 }
 
@@ -52,7 +62,7 @@ function readStored(): OpenCamera[] {
       if (Array.isArray(cameras)) {
         return cameras
           .filter(c => c && Number.isInteger(c.id) && c.id > 0)
-          .map(c => ({ id: c.id, name: String(c.name ?? '') }));
+          .map(c => ({ id: c.id, name: String(c.name ?? ''), scope: c.scope === 'printers' ? 'printers' : 'app' }));
       }
     }
   } catch {
@@ -89,9 +99,19 @@ export function EmbeddedCameraProvider({ children }: { children: ReactNode }) {
     () => new Map(readStored().map(c => [c.id, c])),
   );
 
-  const openEmbeddedCamera = useCallback((id: number, name: string) => {
-    setCameras(prev => (prev.has(id) ? prev : new Map(prev).set(id, { id, name })));
-    updateStored(list => (list.some(c => c.id === id) ? list : [...list, { id, name }]));
+  const openEmbeddedCamera = useCallback((id: number, name: string, scope: CameraScope) => {
+    // The wider of the two scopes wins, so a card can't pull a camera the
+    // sidebar put on every page back onto one.
+    const merge = (current: OpenCamera | undefined): OpenCamera =>
+      current ? { ...current, scope: current.scope === 'app' || scope === 'app' ? 'app' : 'printers' } : { id, name, scope };
+    setCameras(prev => {
+      const next = merge(prev.get(id));
+      const current = prev.get(id);
+      return current && current.scope === next.scope ? prev : new Map(prev).set(id, next);
+    });
+    updateStored(list =>
+      list.some(c => c.id === id) ? list.map(c => (c.id === id ? merge(c) : c)) : [...list, { id, name, scope }],
+    );
   }, []);
 
   const closeEmbeddedCamera = useCallback((id: number) => {
@@ -137,10 +157,16 @@ export function EmbeddedCameraProvider({ children }: { children: ReactNode }) {
       const list = await queryClient.fetchQuery({ queryKey: ['printers'], queryFn: api.getPrinters, staleTime: 0 });
       return list.some(p => p.id === printerId);
     };
-    return listenForCameraPopIn(registerCameraTab(), openEmbeddedCamera, canShow);
+    // Brought back into this tab, so onto whichever page it is on.
+    return listenForCameraPopIn(registerCameraTab(), (id, name) => openEmbeddedCamera(id, name, 'app'), canShow);
   }, [canView, openEmbeddedCamera, queryClient]);
 
   const openCameras = useMemo(() => Array.from(cameras.values()), [cameras]);
+  const onPrintersPage = useLocation().pathname === '/';
+  const shownCameras = useMemo(
+    () => openCameras.filter(c => c.scope === 'app' || onPrintersPage),
+    [openCameras, onPrintersPage],
+  );
   const value = useMemo(
     () => ({ openCameras, openEmbeddedCamera, closeEmbeddedCamera }),
     [openCameras, openEmbeddedCamera, closeEmbeddedCamera],
@@ -150,9 +176,9 @@ export function EmbeddedCameraProvider({ children }: { children: ReactNode }) {
     <EmbeddedCameraContext.Provider value={value}>
       {children}
       {/* Several can be open at once; each offsets itself by its index. */}
-      {canView && listChecked && openCameras.length > 0 && (
+      {canView && listChecked && shownCameras.length > 0 && (
         <Suspense fallback={null}>
-          {openCameras.map((camera, index) => (
+          {shownCameras.map((camera, index) => (
             <EmbeddedCameraViewer
               key={camera.id}
               printerId={camera.id}

+ 5 - 3
frontend/src/pages/PrintersPage.tsx

@@ -8955,6 +8955,8 @@ export function PrintersPage() {
   // The overlays themselves are hosted by the layout, so they stay open on
   // every page (#2737).
   const { openEmbeddedCamera } = useEmbeddedCameras();
+  // Opened from a card, a camera belongs to this page (shown nowhere else).
+  const openCardCamera = useCallback((id: number, name: string) => openEmbeddedCamera(id, name, 'printers'), [openEmbeddedCamera]);
 
   const { data: printers, isLoading } = useQuery({
     queryKey: ['printers'],
@@ -9934,7 +9936,7 @@ export function PrintersPage() {
             // A wall tile has no room for a split button, so it follows the
             // mode the card buttons last chose.
             if (cameraViewMode === 'embedded') {
-              openEmbeddedCamera(id, name);
+              openEmbeddedCamera(id, name, 'printers');
             } else {
               openCameraWindow(id);
             }
@@ -10030,7 +10032,7 @@ export function PrintersPage() {
                       onUnassignSpoolmanSpool={(id) => unassignSpoolmanMutation.mutate(id)}
                       timeFormat={settings?.time_format || 'system'}
                       cameraViewMode={cameraViewMode}
-                      onOpenEmbeddedCamera={openEmbeddedCamera}
+                      onOpenEmbeddedCamera={openCardCamera}
                       onSelectCameraViewMode={selectCameraViewMode}
                       checkPrinterFirmware={firmwareChecksOn}
                       dryingPresets={effectiveDryingPresets}
@@ -10082,7 +10084,7 @@ export function PrintersPage() {
               } : undefined}
               timeFormat={settings?.time_format || 'system'}
               cameraViewMode={cameraViewMode}
-              onOpenEmbeddedCamera={openEmbeddedCamera}
+              onOpenEmbeddedCamera={openCardCamera}
               onSelectCameraViewMode={selectCameraViewMode}
               checkPrinterFirmware={firmwareChecksOn}
               dryingPresets={effectiveDryingPresets}

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/ArchivesPage-A6_6z2xf.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/CamWallPage-Di472lOK.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/CameraPage-aP_-dXPB.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/FileManagerPage-B_d291Lu.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/InventoryPage-B00LDf2L.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/Layout-BLtz11zN.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/ModelSourcesPage-Ca16i08L.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/PrintersPage-4t5tmqMm.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/PrintersPage-DXCkNsFC.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/SettingsPage-C5HyMe98.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/SliceModal-8EI9m163.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/SpoolBuddyInventoryPage-SS6KYOIA.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
static/assets/SpoolBuddySettingsPage-CNoEtBGv.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/index-CDjEVEyJ.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/locationSensorDefaults-DpAPmG2n.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 1
static/assets/plateCheck-2zZZ2y0o.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 0
static/assets/plateCheck-DyJK-Mnb.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/spoolCost-BuoKjJCo.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/usePageFileDrop-BCIO4FoL.js


+ 4 - 4
static/index.html

@@ -26,18 +26,18 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-DNGt_wr5.js"></script>
+    <script type="module" crossorigin src="/assets/index-CDjEVEyJ.js"></script>
     <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-aKtaBQYM.js">
     <link rel="modulepreload" crossorigin href="/assets/client-Bw3urp1w.js">
     <link rel="modulepreload" crossorigin href="/assets/react-dom-Bihwyis7.js">
     <link rel="modulepreload" crossorigin href="/assets/preload-helper-Czpn1I53.js">
+    <link rel="modulepreload" crossorigin href="/assets/chunk-62JRHF6Z-BW23DMlx.js">
     <link rel="modulepreload" crossorigin href="/assets/QueryClientProvider-C3eWKLmF.js">
     <link rel="modulepreload" crossorigin href="/assets/useQuery-BTfjV-1Z.js">
     <link rel="modulepreload" crossorigin href="/assets/info-U9JJZHLV.js">
     <link rel="modulepreload" crossorigin href="/assets/AuthContext-DQrtirY_.js">
     <link rel="modulepreload" crossorigin href="/assets/camera-MsEzBYBi.js">
-    <link rel="modulepreload" crossorigin href="/assets/plateCheck-2zZZ2y0o.js">
-    <link rel="modulepreload" crossorigin href="/assets/chunk-62JRHF6Z-BW23DMlx.js">
+    <link rel="modulepreload" crossorigin href="/assets/plateCheck-DyJK-Mnb.js">
     <link rel="modulepreload" crossorigin href="/assets/useMutation-CGk77Rd2.js">
     <link rel="modulepreload" crossorigin href="/assets/useQueries-Xuq6LLzc.js">
     <link rel="modulepreload" crossorigin href="/assets/archive-BcjXl6s7.js">
@@ -122,7 +122,7 @@
     <link rel="modulepreload" crossorigin href="/assets/useColorCatalogVersion-D6uXNSEZ.js">
     <link rel="modulepreload" crossorigin href="/assets/circle-minus-C2rRKF7J.js">
     <link rel="modulepreload" crossorigin href="/assets/ConnectionDiagnostic-BaFc8Rjv.js">
-    <link rel="modulepreload" crossorigin href="/assets/Layout-zU06-Qgt.js">
+    <link rel="modulepreload" crossorigin href="/assets/Layout-BLtz11zN.js">
     <link rel="stylesheet" crossorigin href="/assets/index-Vboviekw.css">
   </head>
   <body>

Некоторые файлы не были показаны из-за большого количества измененных файлов