Names the project **Tokunia** (Tokun·IA, from 徳用 tokuyō — "good value for the money") and gives it a public face on two hosts: tokunia.dev.gabvdl.xyz the landing page demo.tokunia.dev.gabvdl.xyz the PWA on its in-browser mock backend The demo is this frontend built with VITE_MOCK=1, so a stranger gets every screen with no API exposed and nothing to install. Branding is env-driven (VITE_APP_TITLE / _SHORT_NAME / _DESCRIPTION in vite.config.ts); unset, the homelab build keeps its existing title and manifest byte for byte. landing/ is a dependency-free static site — the page is a ledger printed on ink, and its signature is a thermal receipt itemising one real run from the archive (opus-4-8, 304 turns, $25.79, 30.7M tokens) that then bills the same run again in carbon, energy and water using the app's own footprint.ts factors. Archive stats on the page are the live instance's real numbers. The "Run it" section states the one honest caveat: that build still resolves @gabvdl/ui from a private registry, so it only completes on the author's network today (GOAL.md's open item). Better to say so than to ship a one-liner that fails for everyone else. landing/deploy.sh publishes both hosts and excludes `demo./` from the landing rsync — the hosts are nested in zipgo's trailing-dot tree and zipgo mirrors by default, so an unfiltered deploy would delete the demo underneath it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
91 lines
3.9 KiB
TypeScript
91 lines
3.9 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,
|
|
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.
|
|
navigateFallbackDenylist: [/^\/api\//, /^\/avatars\//],
|
|
// 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 },
|
|
},
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
],
|
|
});
|