Files
budget2.0/src/index.ts
T
Lucas WintherandClaude Opus 5 53707b14ca feat(tooling): drive headless Chrome, and see the app for the first time
CLAUDE.md, AGENTS.md, ARCHITECTURE.md and TODO.md all said no browser exists
in this environment, so appearance had never been verified by anyone. Chrome
is on PATH. Its own --screenshot flag is what fails: it fires on the load
event, before this app's async IndexedDB read resolves, and captures a blank
page — which is presumably how the claim took hold.

scripts/screenshot.ts drives CDP instead: waits past the load event, seeds
IndexedDB via --eval-file and reboots the app with --reload, clicks with
--click, applies shadcn's class-based dark palette with --dark, and reports
console errors and uncaught exceptions. A page that renders blank from a
thrown error looks identical to an empty one, so the console output is half
the value.

Two defects found by simply looking:

- The month table's numeric columns had no horizontal padding, so figures ran
  into each other: "+7,175.00 DKK4,200.00 DKK". Now padded.
- Dev logged "The script has an unsupported MIME type ('text/html')" on every
  boot, because registerServiceWorker asks for sw.js and the "/*" catch-all
  answered with the HTML shell. Dev now serves an inert worker.

Confirmed working, first time seen: the carry meter (including red where a post
is overspent), the Task 5 rule-history editor, the Task 6 "change from here"
control, and baked FX rates showing as "manual - 2026-09-01" in Settings.

Recorded in the docs, with the limits stated: this container's fontconfig maps
every generic family to a monospace face, so screenshots need a FONTCONFIG_FILE
override or you will chase a font bug that does not exist; Chrome starts from
an empty profile each run; and no screenshot can judge how a design feels.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 14:46:59 +02:00

51 lines
2.1 KiB
TypeScript

import { serve } from "bun";
import index from "./index.html";
import { normalizeBase } from "./ui/basePath.ts";
import { buildManifest } from "./manifest.ts";
const basePath = normalizeBase(process.env.BUN_PUBLIC_BASE_PATH);
const server = serve({
routes: {
// index.html references both of these with a plain <link href>. Without
// explicit routes the "/*" catch-all below answers them with the HTML
// shell, so the manifest arrives as text/html (a parse error, and no
// install prompt) and the icon never loads. Generated rather than served
// from a committed file so the dev manifest cannot drift from the built
// one — a stale copy is exactly how it came to hardcode scope "/".
[`${basePath}manifest.webmanifest`]: () =>
new Response(buildManifest(basePath), {
headers: { "content-type": "application/manifest+json" },
}),
[`${basePath}icon.svg`]: () =>
new Response(Bun.file(new URL("./icon.svg", import.meta.url)), {
headers: { "content-type": "image/svg+xml" },
}),
[`${basePath}icon-192.png`]: () =>
new Response(Bun.file(new URL("./icon-192.png", import.meta.url)), {
headers: { "content-type": "image/png" },
}),
[`${basePath}icon-512.png`]: () =>
new Response(Bun.file(new URL("./icon-512.png", import.meta.url)), {
headers: { "content-type": "image/png" },
}),
// registerServiceWorker() asks for sw.js on every boot. Without this the
// catch-all answers with the HTML shell and the browser logs
// "The script has an unsupported MIME type ('text/html')" — caught and
// harmless, but noise that hides real errors. A worker that claims no
// clients and handles no fetches keeps registration honest in dev without
// caching anything, which would fight HMR.
[`${basePath}sw.js`]: () =>
new Response("/* dev: intentionally does nothing */\n", {
headers: { "content-type": "text/javascript", "cache-control": "no-cache" },
}),
"/*": index,
},
development: process.env.NODE_ENV !== "production" && {
hmr: true,
console: true,
},
});
console.log(`Dev server running at ${server.url}`);