The browser fetches `<link rel="manifest">` anonymously unless told otherwise, so Authelia answered it with the 302 to the portal and the cross-origin hop was then blocked by CORS. The manifest failed to load on every single page view — visible as an `ERR_FAILED` in the console — which is the file carrying the app name, icons and theme colours used when installing the PWA. `useCredentials: true` emits `crossorigin="use-credentials"`, so the request carries the session cookie and is served as a normal authenticated 200. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
102 lines
4.7 KiB
TypeScript
102 lines
4.7 KiB
TypeScript
import { defineConfig } from "vite";
|
|
import react from "@vitejs/plugin-react";
|
|
import { VitePWA } from "vite-plugin-pwa";
|
|
import path from "node:path";
|
|
|
|
// Product branding, overridable at build time so the same bundle can ship as
|
|
// the homelab's viewer or as the public Tokunia demo
|
|
// (`VITE_APP_TITLE=… VITE_APP_SHORT_NAME=… npm run build`).
|
|
const TITLE = process.env.VITE_APP_TITLE || "Claude Agent — context";
|
|
// The manifest name has always been the longer form; a branded build overrides
|
|
// both with the same string.
|
|
const APP_NAME = process.env.VITE_APP_TITLE || "Claude Agent — context viewer";
|
|
const SHORT_NAME = process.env.VITE_APP_SHORT_NAME || "ai-agent";
|
|
const DESCRIPTION =
|
|
process.env.VITE_APP_DESCRIPTION ||
|
|
"Viewer/editor for the homelab's CLAUDE.md files and skills.";
|
|
|
|
export default defineConfig({
|
|
// Build-time id used to bust the persisted React Query (IndexedDB) cache on
|
|
// every deploy, so a changed API response shape can't resurrect stale entries.
|
|
define: { __BUILD_ID__: JSON.stringify(String(Date.now())) },
|
|
resolve: {
|
|
alias: { "@": path.resolve(__dirname, "src") },
|
|
},
|
|
server: { host: true, port: 5180 },
|
|
plugins: [
|
|
react(),
|
|
// index.html ships the default <title>; a branded build rewrites it here so
|
|
// the served HTML and the manifest below always agree.
|
|
{
|
|
name: "app-title",
|
|
transformIndexHtml: (html) =>
|
|
html.replace(/<title>[^<]*<\/title>/, `<title>${TITLE}</title>`),
|
|
},
|
|
VitePWA({
|
|
// The generated worker calls skipWaiting()/clientsClaim(), so a new build
|
|
// always activates and takes over the open page's fetches. What it never
|
|
// does is reload that page — src/pwa.ts does, once it sees the handover.
|
|
registerType: "autoUpdate",
|
|
// We register the worker ourselves from src/pwa.ts so we can poll for
|
|
// updates; suppress the <script src="/registerSW.js"> the plugin injects.
|
|
injectRegister: false,
|
|
// Emit `crossorigin="use-credentials"` on <link rel="manifest">. The
|
|
// browser fetches the manifest anonymously by default, so behind Authelia
|
|
// it was answered with the 302 to the portal and then killed by CORS —
|
|
// the manifest never loaded at all, on any page view. Sending the session
|
|
// cookie with it makes the request a plain authenticated 200.
|
|
useCredentials: true,
|
|
includeAssets: ["icon.svg"],
|
|
manifest: {
|
|
name: APP_NAME,
|
|
short_name: SHORT_NAME,
|
|
description: DESCRIPTION,
|
|
theme_color: "#0a0a0a",
|
|
background_color: "#0a0a0a",
|
|
display: "standalone",
|
|
orientation: "portrait",
|
|
start_url: "/",
|
|
icons: [
|
|
{ src: "/icon.svg", sizes: "any", type: "image/svg+xml", purpose: "any maskable" },
|
|
],
|
|
},
|
|
workbox: {
|
|
// The plugin only implies these for `injectRegister: "auto"`, and we
|
|
// register the worker ourselves. Without them a new build installs but
|
|
// parks in "waiting" forever, and nothing ever serves the new assets.
|
|
skipWaiting: true,
|
|
clientsClaim: true,
|
|
navigateFallback: "/index.html",
|
|
// /avatars/* are self-contained static pages (sprite collections), not
|
|
// SPA routes — the worker must let their navigations hit the network.
|
|
// `?reauth=` is the re-authentication navigation src/api.ts makes when
|
|
// the Authelia session expires: it MUST reach Traefik to be answered
|
|
// with the 302 to the portal. Served from the precache instead, it
|
|
// reboots the logged-out shell and loops. Workbox tests these against
|
|
// `pathname + search`, so a query-string pattern is matched.
|
|
navigateFallbackDenylist: [/^\/api\//, /^\/avatars\//, /[?&]reauth=/],
|
|
// Drop precaches from previous builds so an upgrade can't serve a mix of
|
|
// old and new hashed chunks.
|
|
cleanupOutdatedCaches: true,
|
|
// Cache the API bundle so the shell still has data on a cold offline start.
|
|
// `/api/events` is exempt: it is an endless SSE stream, and NetworkFirst
|
|
// would try to read the whole body into the cache. That read never
|
|
// finishes, which keeps the old worker alive and wedges every future
|
|
// update in "waiting" forever. Let it go straight to the network.
|
|
runtimeCaching: [
|
|
{
|
|
urlPattern: ({ url }) =>
|
|
url.pathname.startsWith("/api/") && url.pathname !== "/api/events",
|
|
handler: "NetworkFirst",
|
|
options: {
|
|
cacheName: "ai-agent-api",
|
|
networkTimeoutSeconds: 5,
|
|
expiration: { maxEntries: 32, maxAgeSeconds: 60 * 60 * 24 * 30 },
|
|
},
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
],
|
|
});
|