diff --git a/admin.go b/admin.go index 1441278..14220e5 100644 --- a/admin.go +++ b/admin.go @@ -24,9 +24,13 @@ const ( adminUsageHistoryPath = "/admin/api/usage/history" ) -// adminCSP is the Content-Security-Policy header served with GET /admin -// and the three /admin/api/* JSON routes (spec §4, v0.2; extended to the -// JSON routes by a folded review item, 2026-08-20 review). It was +// adminCSP is the Content-Security-Policy header served with GET /admin, +// every GET /admin/assets/{hashedname} response (serveAdminAsset), and +// the three /admin/api/* JSON routes — every /admin* response this +// gateway ever sends (spec §4, v0.2; extended to the JSON routes by a +// folded review item, 2026-08-20 review; extended to the asset route by +// a later review sweep — see serveAdminAsset's own doc comment for why). +// It was // widened with `script-src 'unsafe-inline'`/`style-src 'unsafe-inline'` // for the original vanilla-JS single-file page; the baked Vue build (Vue // admin-panel task) needs neither: `vite build` emits index.html with diff --git a/admin_test.go b/admin_test.go index 4377db8..02af1d7 100644 --- a/admin_test.go +++ b/admin_test.go @@ -696,10 +696,33 @@ func TestAdminAssets_GeneratedFilePresent(t *testing.T) { } // adminInlineScriptRe matches every `) + if len(got) != 1 { + t.Fatalf("hasInlineScript = %v, want exactly 1 offender (data-src is not src=)", got) + } +} + func truncateForTest(b []byte, n int) string { if len(b) <= n { return string(b) diff --git a/webui/README.md b/webui/README.md index 72da937..bdeaf3b 100644 --- a/webui/README.md +++ b/webui/README.md @@ -12,33 +12,47 @@ built, baked-in output, never `webui/` itself. ## Warning: `shadcn-vue add`/`init` regressions -`components.json`'s `iconLibrary` and `font` fields are locked to -`"lucide"` and `"inter"` — **not this project's real choices** (this -project uses FontAwesome only, per `vue-web-development` house skill, and -no external font at all). The shadcn-vue CLI's schema does not accept -`"none"`/`"fontawesome"`/`"system"` for either field (checked directly: -`node_modules/shadcn-vue/dist/preset-*.js`'s `PRESET_ICON_LIBRARIES` and -`PRESET_FONTS` arrays — five fixed choices each, no escape hatch), and -JSON supports no comment syntax to warn inline (the repo's own -pre-commit JSON validator rejects JSONC comments — see `tsconfig.*.json` -history). This warning is the only place the trap could be recorded. +`components.json` has no `iconLibrary` or `font` key. **Do not add them +back.** Their omission is the fix, not an oversight — read on before you +"helpfully" restore either one. -Both `npx shadcn-vue init` and `npx shadcn-vue add ` read -these fields and **silently rewrite `src/assets/main.css`** — hit twice -in this project's own history: `init` first, then a follow-up `add` -call, both re-added a `@import url('https://fonts.googleapis.com/...')` -line and switched the dark-mode `@custom-variant` back to a `.dark`-class -toggle. If you ever run either command again: +Earlier revisions of this file set `"iconLibrary": "lucide"` and +`"font": "inter"`, both wrong for this project (FontAwesome only, per +the `vue-web-development` house skill; no external font at all). Both +`npx shadcn-vue init` and `npx shadcn-vue add ` read those two +fields and, when set, rewrite `src/assets/main.css` — hit twice in this +project's own history, `init` then a later `add`, both re-adding a +`@import url('https://fonts.googleapis.com/...')` line and switching the +dark-mode `@custom-variant` back to a `.dark`-class toggle. -1. **Diff `src/assets/main.css` before committing.** Remove any - re-added `@import url('https://fonts.googleapis...')` line (external - font, blocked by the admin CSP's `style-src 'self'` anyway) and - restore the `@custom-variant dark` removal (see the file's own header - comment for why: `prefers-color-scheme`, not a class toggle). -2. **Check the new component for a `lucide-vue-next`/`@lucide/vue` - import** and swap it for the FontAwesome equivalent (see - `src/components/ui/select/Select*.vue` for the pattern already - applied there). +The actual shadcn-vue config schema +(`node_modules/shadcn-vue/dist/schema/index.js`'s `rawConfigSchema`) +declares both fields as `z.string().optional()` — not an enum, contrary +to an earlier, incorrect claim in this project's own review notes that +the CLI schema forced one of five fixed values. The CLI's `init --font`/ +`--icon-library` flags do offer five fixed choices each, but that is a +flag-parsing constraint, not a `components.json` schema constraint. +Deleting both keys leaves them `undefined` after parse, a valid state: +verified empirically, running `npx shadcn-vue add badge -o -y` against a +`components.json` with both keys removed left `src/assets/main.css` +byte-for-byte unchanged. + +That verification also surfaced a narrower, remaining trap: the same run +still added `@lucide/vue` back to `package.json`'s dependencies (Badge +itself does not use it — some components' registry entries list an icon +package regardless of `iconLibrary`). So after any future `shadcn-vue +add`/`init` call: + +1. **Check `git diff webui/package.json`** for a re-added + `lucide-vue-next`/`@lucide/vue` dependency, and remove it + (`npm uninstall`) if nothing in `src/` imports from it. +2. **Check the new component's own file** for a + `lucide-vue-next`/`@lucide/vue` import and swap it for the + FontAwesome equivalent (see `src/components/ui/select/Select*.vue` + for the pattern already applied there). +3. **Diff `src/assets/main.css` anyway** before committing, in case a + future shadcn-vue release starts reading a different config key for + the same font-injection step. ## Local development diff --git a/webui/components.json b/webui/components.json index 4282fc1..8203674 100644 --- a/webui/components.json +++ b/webui/components.json @@ -1,7 +1,6 @@ { "$schema": "https://shadcn-vue.com/schema.json", "style": "reka-nova", - "font": "inter", "typescript": true, "tailwind": { "config": "", @@ -10,7 +9,6 @@ "cssVariables": true, "prefix": "" }, - "iconLibrary": "lucide", "rtl": false, "pointer": false, "aliases": { diff --git a/webui/generate.mjs b/webui/generate.mjs index ba3d444..37d9091 100644 --- a/webui/generate.mjs +++ b/webui/generate.mjs @@ -80,7 +80,13 @@ function assertNoInlineAssets(html, sourceLabel) { const scriptTagRe = /]*)>/gi let match while ((match = scriptTagRe.exec(html)) !== null) { - if (!/\bsrc\s*=/i.test(match[1])) { + // Anchored to attribute-start (start-of-string or preceding + // whitespace), not a bare \b word boundary: \b alone matches inside + // "data-src=" too (the "-" before "src" is itself a non-word + // character, so \b sits right before "src"), which would wrongly + // treat a data-src/nosrc/whatever-src attribute as the real src= that + // makes a