Browse Source

fix(#1096): warn on Spoolman HTTP/HTTPS mismatch instead of silent blank iframe

  Users behind an HTTPS reverse proxy pointing the Spoolman URL at plain
  HTTP saw the Filament tab render as a blank page with only a console-
  side Mixed Content warning. Browsers block HTTP iframes inside HTTPS
  parents by design (independent of CSP; #1054's frame-src http: fix
  only helps when the parent is also HTTP). The fix for the user's
  setup is to put Spoolman behind the same reverse proxy with HTTPS.

  Bambuddy can't override the browser's mixed-content block, but it can
  stop rendering an iframe that will silently fail. When
  window.location.protocol is https: and the Spoolman URL starts with
  http://, render a warning card explaining the root cause and offering
  an "Open in new tab" fallback (standalone tabs aren't subject to
  mixed-content rules).

  Localised across all 8 UI languages.
maziggy 1 month ago
parent
commit
219af65c68

+ 1 - 0
CHANGELOG.md

@@ -15,6 +15,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **Settings page: permission-gated instead of admin-only** — the Settings sidebar entry has always been visible to any user holding `settings:read`, but the route guard required admin role, so a non-admin with `settings:read` would see the entry, click it, and get silently redirected back to the dashboard. The route guard now matches the sidebar: any user with `settings:read` can open the page, and the individual tabs / cards continue to enforce their own per-feature permissions (`users:read`, `groups:update`, `oidc:*`, etc. — many of them admin-only, some not). Group editor routes moved to permission-based guards too (`groups:create` for `/groups/new`, `groups:update` for `/groups/:id/edit`), so permission delegation works end-to-end. Admins retain full access since admins implicitly hold every permission.
 - **Settings page: permission-gated instead of admin-only** — the Settings sidebar entry has always been visible to any user holding `settings:read`, but the route guard required admin role, so a non-admin with `settings:read` would see the entry, click it, and get silently redirected back to the dashboard. The route guard now matches the sidebar: any user with `settings:read` can open the page, and the individual tabs / cards continue to enforce their own per-feature permissions (`users:read`, `groups:update`, `oidc:*`, etc. — many of them admin-only, some not). Group editor routes moved to permission-based guards too (`groups:create` for `/groups/new`, `groups:update` for `/groups/:id/edit`), so permission delegation works end-to-end. Admins retain full access since admins implicitly hold every permission.
 
 
 ### Fixed
 ### Fixed
+- **Spoolman iframe silently blank on HTTPS Bambuddy with HTTP Spoolman** ([#1096](https://github.com/maziggy/bambuddy/issues/1096)) — Users behind an HTTPS reverse proxy (Traefik / Nginx / Caddy) pointing the Spoolman URL at plain HTTP saw the Filament tab render as a blank page with only a console-side `Mixed Content` warning. CSP was fine (the `#1054` fix already allowed `frame-src http:`), but browsers enforce mixed-content blocking independently of CSP — an HTTP iframe inside an HTTPS parent is always blocked. Bambuddy can't technically fix this (the browser is correct to refuse), so instead of the silent blank frame the Filament page now detects the protocol mismatch (`window.location.protocol === 'https:'` plus Spoolman URL starting with `http://`) and renders an inline warning card explaining the root cause, pointing users at the right fix (put Spoolman behind the same HTTPS reverse proxy and update the Spoolman URL in Settings), and offering an "Open Spoolman in a new tab" button as an immediate workaround — a standalone tab isn't subject to mixed-content rules. Localised across all 8 UI languages. Thanks to @jsapede for the report.
 - **Reprint-from-Archive left `created_by_id` as `NULL`** ([#730](https://github.com/maziggy/bambuddy/issues/730) follow-up) — 0.2.4b1 fixed user attribution for Direct Print / File Manager / Library prints, but the reprint path was still unattributed on the *archive* row. Reprint intentionally reuses the source archive (to avoid duplicate rows — see `register_expected_print`), so an archive auto-created from a printer-initiated print with no known user stayed `created_by_id=NULL` forever, even after multiple reprints by authenticated Bambuddy users. Print Log got the reprinter's username correctly (via `_print_user_info`), but the Statistics per-user filter — which reads `archive.created_by_id` — kept showing the archive as unassigned. Fix in `main.py`'s print-complete handler: when the archive has no `created_by_id` and a print-session user is set (which reprint always sets via `set_current_print_user`), back-fill the archive's attribution. Never overwrites an existing attribution — the original uploader keeps ownership; NULL archives are the only ones touched. Thanks to @3823u44238 for the detailed retest that caught this.
 - **Reprint-from-Archive left `created_by_id` as `NULL`** ([#730](https://github.com/maziggy/bambuddy/issues/730) follow-up) — 0.2.4b1 fixed user attribution for Direct Print / File Manager / Library prints, but the reprint path was still unattributed on the *archive* row. Reprint intentionally reuses the source archive (to avoid duplicate rows — see `register_expected_print`), so an archive auto-created from a printer-initiated print with no known user stayed `created_by_id=NULL` forever, even after multiple reprints by authenticated Bambuddy users. Print Log got the reprinter's username correctly (via `_print_user_info`), but the Statistics per-user filter — which reads `archive.created_by_id` — kept showing the archive as unassigned. Fix in `main.py`'s print-complete handler: when the archive has no `created_by_id` and a print-session user is set (which reprint always sets via `set_current_print_user`), back-fill the archive's attribution. Never overwrites an existing attribution — the original uploader keeps ownership; NULL archives are the only ones touched. Thanks to @3823u44238 for the detailed retest that caught this.
 - **Settings: failed-save toast looped forever when the user lacked `settings:update`** — the Settings page runs a debounced auto-save effect that fires `PATCH /settings` whenever `localSettings` diverges from the last server snapshot. When a delegated user with `settings:read` but not `settings:update` toggled a control, the effect fired `PATCH`, got `403`, and kept re-firing every ~500 ms producing an endless stream of identical "Failed to save" toasts. Gated at three points so the mutation is never attempted without permission: (1) the `updateSetting` callback — every onChange path — shows one `settings.toast.noPermissionUpdate` toast and short-circuits before diverging `localSettings`; (2) the debounced-save effect safety-nets the same check in case any call site bypassed `updateSetting`; (3) the language `<select>` was a fire-and-forget direct `api.updateSettings` call that always flashed a success toast regardless of outcome — it now goes through `updateMutation` with the same permission guard. New `settings.toast.noPermissionUpdate` key added across all 8 locales with full translations (not English-fallback).
 - **Settings: failed-save toast looped forever when the user lacked `settings:update`** — the Settings page runs a debounced auto-save effect that fires `PATCH /settings` whenever `localSettings` diverges from the last server snapshot. When a delegated user with `settings:read` but not `settings:update` toggled a control, the effect fired `PATCH`, got `403`, and kept re-firing every ~500 ms producing an endless stream of identical "Failed to save" toasts. Gated at three points so the mutation is never attempted without permission: (1) the `updateSetting` callback — every onChange path — shows one `settings.toast.noPermissionUpdate` toast and short-circuits before diverging `localSettings`; (2) the debounced-save effect safety-nets the same check in case any call site bypassed `updateSetting`; (3) the language `<select>` was a fire-and-forget direct `api.updateSettings` call that always flashed a success toast regardless of outcome — it now goes through `updateMutation` with the same permission guard. New `settings.toast.noPermissionUpdate` key added across all 8 locales with full translations (not English-fallback).
 - **Groups: edits to custom-group permissions appeared lost on reopen** ([#1083](https://github.com/maziggy/bambuddy/issues/1083)) — creating a custom group and reopening the editor showed the correct permissions, but after editing that group's permissions and saving, reopening the editor within ~1 minute displayed the pre-edit snapshot as if the save had failed. The backend `PATCH /api/v1/groups/{id}` was persisting correctly (now covered by four new integration tests in `test_groups_api.py`, including a direct DB read after update); the issue was purely in the frontend React Query cache — `GroupEditPage.onSuccess` invalidated `['groups']` (the list) but left the `['group', id]` detail cache stale, and with the app-wide 60 s `staleTime` the next mount served the cached pre-update body instead of refetching. `onSuccess` now primes the `['group', id]` detail cache with the `PATCH` response body so the next mount hits fresh data immediately without a round-trip. Create-path invalidates `['group']` for symmetry. Regression test in `GroupEditPage.test.tsx` verifies the detail cache contains the updated permissions after save.
 - **Groups: edits to custom-group permissions appeared lost on reopen** ([#1083](https://github.com/maziggy/bambuddy/issues/1083)) — creating a custom group and reopening the editor showed the correct permissions, but after editing that group's permissions and saving, reopening the editor within ~1 minute displayed the pre-edit snapshot as if the save had failed. The backend `PATCH /api/v1/groups/{id}` was persisting correctly (now covered by four new integration tests in `test_groups_api.py`, including a direct DB read after update); the issue was purely in the frontend React Query cache — `GroupEditPage.onSuccess` invalidated `['groups']` (the list) but left the `['group', id]` detail cache stale, and with the app-wide 60 s `staleTime` the next mount served the cached pre-update body instead of refetching. `onSuccess` now primes the `['group', id]` detail cache with the `PATCH` response body so the next mount hits fresh data immediately without a round-trip. Create-path invalidates `['group']` for symmetry. Regression test in `GroupEditPage.test.tsx` verifies the detail cache contains the updated permissions after save.

+ 5 - 0
frontend/src/i18n/locales/de.ts

@@ -3261,6 +3261,11 @@ export default {
   // Inventar
   // Inventar
   inventory: {
   inventory: {
     title: 'Spulen-Inventar',
     title: 'Spulen-Inventar',
+    spoolmanMixedContentTitle: 'Spoolman lässt sich nicht über HTTPS laden — Browser blockiert gemischte Inhalte',
+    spoolmanMixedContentBody: 'Bambuddy wird über HTTPS ausgeliefert (über deinen Reverse-Proxy), aber deine Spoolman-URL ist nach wie vor HTTP. Browser blockieren gemischte Inhalte aus Sicherheitsgründen, daher kann die eingebettete Spoolman-Oberfläche nicht geladen werden. Spoolman muss ebenfalls über HTTPS erreichbar sein.',
+    spoolmanMixedContentFixReverseProxy: 'Stelle Spoolman hinter denselben Reverse-Proxy wie Bambuddy (Traefik / Nginx / Caddy) mit HTTPS und aktualisiere die Spoolman-URL in den Einstellungen auf die neue HTTPS-Adresse.',
+    spoolmanMixedContentFixOpenNewTab: 'Als Workaround kannst du Spoolman in einem neuen Tab über HTTP öffnen — gemischte Inhalte werden nur innerhalb eingebetteter Frames blockiert, ein eigener Tab funktioniert weiterhin.',
+    spoolmanOpenInNewTab: 'Spoolman in neuem Tab öffnen',
     addSpool: 'Spule hinzufügen',
     addSpool: 'Spule hinzufügen',
     editSpool: 'Spule bearbeiten',
     editSpool: 'Spule bearbeiten',
     material: 'Material',
     material: 'Material',

+ 5 - 0
frontend/src/i18n/locales/en.ts

@@ -3264,6 +3264,11 @@ export default {
   // Inventory
   // Inventory
   inventory: {
   inventory: {
     title: 'Spool Inventory',
     title: 'Spool Inventory',
+    spoolmanMixedContentTitle: 'Spoolman can\'t load over HTTPS — mixed-content blocked by your browser',
+    spoolmanMixedContentBody: 'Bambuddy is served over HTTPS (via your reverse proxy), but your Spoolman URL is still plain HTTP. Browsers block mixed content for security, so the embedded Spoolman UI can\'t render. Spoolman needs to be reachable over HTTPS for this to work.',
+    spoolmanMixedContentFixReverseProxy: 'Put Spoolman behind the same reverse proxy as Bambuddy (Traefik / Nginx / Caddy) with HTTPS, then update the Spoolman URL in Settings to the new HTTPS address.',
+    spoolmanMixedContentFixOpenNewTab: 'As a workaround, open Spoolman in a new browser tab over HTTP — mixed-content rules only apply to embedded frames, so a standalone tab still works.',
+    spoolmanOpenInNewTab: 'Open Spoolman in a new tab',
     addSpool: 'Add Spool',
     addSpool: 'Add Spool',
     editSpool: 'Edit Spool',
     editSpool: 'Edit Spool',
     material: 'Material',
     material: 'Material',

+ 5 - 0
frontend/src/i18n/locales/fr.ts

@@ -3183,6 +3183,11 @@ export default {
   // Inventory
   // Inventory
   inventory: {
   inventory: {
     title: 'Inventaire de Bobines',
     title: 'Inventaire de Bobines',
+    spoolmanMixedContentTitle: 'Spoolman ne peut pas se charger en HTTPS — contenu mixte bloqué par votre navigateur',
+    spoolmanMixedContentBody: 'Bambuddy est servi en HTTPS (via votre reverse proxy), mais votre URL Spoolman est encore en HTTP. Les navigateurs bloquent le contenu mixte pour des raisons de sécurité, donc l\'interface Spoolman intégrée ne peut pas s\'afficher. Spoolman doit être accessible en HTTPS.',
+    spoolmanMixedContentFixReverseProxy: 'Placez Spoolman derrière le même reverse proxy que Bambuddy (Traefik / Nginx / Caddy) en HTTPS, puis mettez à jour l\'URL Spoolman dans les Paramètres avec la nouvelle adresse HTTPS.',
+    spoolmanMixedContentFixOpenNewTab: 'Alternative : ouvrez Spoolman dans un nouvel onglet en HTTP — les règles de contenu mixte ne s\'appliquent qu\'aux cadres intégrés, un onglet autonome fonctionne.',
+    spoolmanOpenInNewTab: 'Ouvrir Spoolman dans un nouvel onglet',
     addSpool: 'Ajouter Bobine',
     addSpool: 'Ajouter Bobine',
     editSpool: 'Modifier Bobine',
     editSpool: 'Modifier Bobine',
     material: 'Matériau',
     material: 'Matériau',

+ 5 - 0
frontend/src/i18n/locales/it.ts

@@ -3182,6 +3182,11 @@ export default {
   // Inventory
   // Inventory
   inventory: {
   inventory: {
     title: 'Inventario Bobine',
     title: 'Inventario Bobine',
+    spoolmanMixedContentTitle: 'Spoolman non può essere caricato tramite HTTPS — contenuto misto bloccato dal browser',
+    spoolmanMixedContentBody: 'Bambuddy viene servito tramite HTTPS (dietro il tuo reverse proxy), ma l\'URL di Spoolman è ancora HTTP. I browser bloccano il contenuto misto per motivi di sicurezza, quindi l\'interfaccia Spoolman incorporata non può essere visualizzata. Anche Spoolman deve essere raggiungibile via HTTPS.',
+    spoolmanMixedContentFixReverseProxy: 'Metti Spoolman dietro lo stesso reverse proxy di Bambuddy (Traefik / Nginx / Caddy) in HTTPS, poi aggiorna l\'URL di Spoolman nelle Impostazioni con il nuovo indirizzo HTTPS.',
+    spoolmanMixedContentFixOpenNewTab: 'Come alternativa, apri Spoolman in una nuova scheda via HTTP — le regole sul contenuto misto si applicano solo ai frame incorporati, una scheda autonoma funziona.',
+    spoolmanOpenInNewTab: 'Apri Spoolman in una nuova scheda',
     addSpool: 'Aggiungi Bobina',
     addSpool: 'Aggiungi Bobina',
     editSpool: 'Modifica Bobina',
     editSpool: 'Modifica Bobina',
     material: 'Materiale',
     material: 'Materiale',

+ 5 - 0
frontend/src/i18n/locales/ja.ts

@@ -3221,6 +3221,11 @@ export default {
   // Inventory
   // Inventory
   inventory: {
   inventory: {
     title: 'スプール在庫管理',
     title: 'スプール在庫管理',
+    spoolmanMixedContentTitle: 'Spoolman を HTTPS で読み込めません — ブラウザが混在コンテンツをブロックしています',
+    spoolmanMixedContentBody: 'Bambuddy はリバースプロキシ経由で HTTPS 配信されていますが、Spoolman の URL は HTTP のままです。ブラウザはセキュリティ上の理由で混在コンテンツをブロックするため、埋め込みの Spoolman UI を表示できません。Spoolman も HTTPS でアクセスできる必要があります。',
+    spoolmanMixedContentFixReverseProxy: 'Spoolman を Bambuddy と同じリバースプロキシ(Traefik / Nginx / Caddy)の後ろに HTTPS で配置し、設定で Spoolman URL を新しい HTTPS アドレスに更新してください。',
+    spoolmanMixedContentFixOpenNewTab: '回避策として Spoolman を新しいタブで HTTP として開くことができます — 混在コンテンツのルールは埋め込みフレームのみに適用され、独立したタブは問題なく動作します。',
+    spoolmanOpenInNewTab: 'Spoolman を新しいタブで開く',
     addSpool: 'スプールを追加',
     addSpool: 'スプールを追加',
     editSpool: 'スプールを編集',
     editSpool: 'スプールを編集',
     material: '素材',
     material: '素材',

+ 5 - 0
frontend/src/i18n/locales/pt-BR.ts

@@ -3196,6 +3196,11 @@ export default {
   // Inventory
   // Inventory
   inventory: {
   inventory: {
     title: 'Inventário de Carretéis',
     title: 'Inventário de Carretéis',
+    spoolmanMixedContentTitle: 'Spoolman não pode carregar em HTTPS — conteúdo misto bloqueado pelo navegador',
+    spoolmanMixedContentBody: 'O Bambuddy é servido via HTTPS (pelo seu reverse proxy), mas a URL do Spoolman ainda é HTTP. Os navegadores bloqueiam conteúdo misto por segurança, então a interface embutida do Spoolman não consegue carregar. O Spoolman também precisa estar acessível via HTTPS.',
+    spoolmanMixedContentFixReverseProxy: 'Coloque o Spoolman atrás do mesmo reverse proxy do Bambuddy (Traefik / Nginx / Caddy) com HTTPS e atualize a URL do Spoolman em Configurações com o novo endereço HTTPS.',
+    spoolmanMixedContentFixOpenNewTab: 'Como alternativa, abra o Spoolman em uma nova aba via HTTP — as regras de conteúdo misto só se aplicam a frames embutidos, uma aba independente funciona normalmente.',
+    spoolmanOpenInNewTab: 'Abrir Spoolman em nova aba',
     addSpool: 'Adicionar Carretel',
     addSpool: 'Adicionar Carretel',
     editSpool: 'Editar Carretel',
     editSpool: 'Editar Carretel',
     material: 'Material',
     material: 'Material',

+ 5 - 0
frontend/src/i18n/locales/zh-CN.ts

@@ -3248,6 +3248,11 @@ export default {
   // Inventory
   // Inventory
   inventory: {
   inventory: {
     title: '耗材库存',
     title: '耗材库存',
+    spoolmanMixedContentTitle: 'Spoolman 无法通过 HTTPS 加载 — 浏览器已阻止混合内容',
+    spoolmanMixedContentBody: 'Bambuddy 通过您的反向代理以 HTTPS 提供服务,但您的 Spoolman 地址仍为 HTTP。出于安全考虑,浏览器会阻止混合内容,因此嵌入式 Spoolman 界面无法加载。Spoolman 也必须通过 HTTPS 访问。',
+    spoolmanMixedContentFixReverseProxy: '请将 Spoolman 置于与 Bambuddy 相同的反向代理(Traefik / Nginx / Caddy)之后并启用 HTTPS,然后在设置中将 Spoolman URL 更新为新的 HTTPS 地址。',
+    spoolmanMixedContentFixOpenNewTab: '作为变通方案,可在新标签页中通过 HTTP 打开 Spoolman — 混合内容规则仅适用于嵌入式框架,独立标签页仍可正常使用。',
+    spoolmanOpenInNewTab: '在新标签页中打开 Spoolman',
     addSpool: '添加耗材',
     addSpool: '添加耗材',
     editSpool: '编辑耗材',
     editSpool: '编辑耗材',
     material: '材料',
     material: '材料',

+ 5 - 0
frontend/src/i18n/locales/zh-TW.ts

@@ -3248,6 +3248,11 @@ export default {
   // Inventory
   // Inventory
   inventory: {
   inventory: {
     title: '耗材庫存',
     title: '耗材庫存',
+    spoolmanMixedContentTitle: 'Spoolman 無法透過 HTTPS 載入 — 瀏覽器已封鎖混合內容',
+    spoolmanMixedContentBody: 'Bambuddy 透過您的反向代理以 HTTPS 提供服務,但您的 Spoolman 位址仍為 HTTP。基於安全考量,瀏覽器會封鎖混合內容,因此內嵌的 Spoolman 介面無法載入。Spoolman 也必須可透過 HTTPS 存取。',
+    spoolmanMixedContentFixReverseProxy: '請將 Spoolman 置於與 Bambuddy 相同的反向代理(Traefik / Nginx / Caddy)之後並啟用 HTTPS,然後在設定中將 Spoolman URL 更新為新的 HTTPS 位址。',
+    spoolmanMixedContentFixOpenNewTab: '作為替代方案,可在新分頁以 HTTP 開啟 Spoolman — 混合內容規則僅適用於內嵌框架,獨立分頁仍可正常運作。',
+    spoolmanOpenInNewTab: '在新分頁開啟 Spoolman',
     addSpool: '新增耗材',
     addSpool: '新增耗材',
     editSpool: '編輯耗材',
     editSpool: '編輯耗材',
     material: '材料',
     material: '材料',

+ 42 - 1
frontend/src/pages/InventoryPage.tsx

@@ -409,6 +409,7 @@ function saveSortState(state: SortState) {
 
 
 // Wrapper: when Spoolman is enabled, embed its UI; otherwise show internal inventory
 // Wrapper: when Spoolman is enabled, embed its UI; otherwise show internal inventory
 export default function InventoryPageRouter() {
 export default function InventoryPageRouter() {
+  const { t } = useTranslation();
   const { data: spoolmanSettings } = useQuery({
   const { data: spoolmanSettings } = useQuery({
     queryKey: ['spoolman-settings'],
     queryKey: ['spoolman-settings'],
     queryFn: api.getSpoolmanSettings,
     queryFn: api.getSpoolmanSettings,
@@ -416,9 +417,49 @@ export default function InventoryPageRouter() {
   });
   });
 
 
   if (spoolmanSettings?.spoolman_enabled === 'true' && spoolmanSettings?.spoolman_url) {
   if (spoolmanSettings?.spoolman_enabled === 'true' && spoolmanSettings?.spoolman_url) {
+    const spoolmanUrl = spoolmanSettings.spoolman_url.replace(/\/+$/, '');
+    // Browsers block HTTP iframes inside HTTPS parents (mixed-content rule,
+    // independent of CSP). Spoolman must be reachable over the same protocol
+    // as Bambuddy. Surfacing a targeted error here beats the silent blank
+    // iframe users otherwise see. See issue #1096.
+    const bambuddyIsHttps = window.location.protocol === 'https:';
+    const spoolmanIsHttp = spoolmanUrl.toLowerCase().startsWith('http://');
+    if (bambuddyIsHttps && spoolmanIsHttp) {
+      return (
+        <div className="p-6 max-w-3xl mx-auto">
+          <div className="rounded-lg border border-amber-300 dark:border-amber-700 bg-amber-50 dark:bg-amber-900/20 p-5">
+            <div className="flex items-start gap-3">
+              <AlertTriangle className="w-5 h-5 text-amber-600 dark:text-amber-400 mt-0.5 shrink-0" />
+              <div className="flex-1 min-w-0 space-y-2 text-sm">
+                <p className="font-semibold text-amber-900 dark:text-amber-100">
+                  {t('inventory.spoolmanMixedContentTitle')}
+                </p>
+                <p className="text-amber-800 dark:text-amber-200">
+                  {t('inventory.spoolmanMixedContentBody')}
+                </p>
+                <ul className="list-disc pl-5 space-y-1 text-amber-800 dark:text-amber-200">
+                  <li>{t('inventory.spoolmanMixedContentFixReverseProxy')}</li>
+                  <li>{t('inventory.spoolmanMixedContentFixOpenNewTab')}</li>
+                </ul>
+                <div className="flex flex-wrap gap-2 pt-2">
+                  <a
+                    href={`${spoolmanUrl}/spool`}
+                    target="_blank"
+                    rel="noopener noreferrer"
+                    className="inline-flex items-center gap-2 px-3 py-1.5 text-sm rounded bg-amber-600 hover:bg-amber-700 text-white"
+                  >
+                    {t('inventory.spoolmanOpenInNewTab')}
+                  </a>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      );
+    }
     return (
     return (
       <iframe
       <iframe
-        src={`${spoolmanSettings.spoolman_url.replace(/\/+$/, '')}/spool`}
+        src={`${spoolmanUrl}/spool`}
         className="h-full w-full border-0"
         className="h-full w-full border-0"
         title="Spoolman"
         title="Spoolman"
         sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
         sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-Cl5t9soF.js


+ 1 - 1
static/index.html

@@ -26,7 +26,7 @@
 
 
     <!-- Splash screens for iOS -->
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-DJb_CJSk.js"></script>
+    <script type="module" crossorigin src="/assets/index-Cl5t9soF.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-DequwckK.css">
     <link rel="stylesheet" crossorigin href="/assets/index-DequwckK.css">
   </head>
   </head>
   <body>
   <body>

Some files were not shown because too many files changed in this diff