fix(webui): version display, CSP guards, config traps, bucket-aligned stacking

Review fix round (4 Important + 2 folded):

1. Version display: OverviewView.vue was missing the muted version line
   the old vanilla-JS page rendered (types/api.ts already declared
   overview.version) - added.

2. CSP invariant now guarded twice: webui/generate.mjs's new
   assertNoInlineAssets fails the build if dist/index.html contains an
   inline <script> (no src= attribute) or any <style> tag - the exact
   condition the tightened admin CSP (script-src/style-src 'self', no
   'unsafe-inline') depends on. admin_test.go's new
   TestAdminIndexHTML_NoInlineAssets re-asserts the identical invariant
   server-side, against the decoded adminIndexHTML admin.go actually
   serves, so a regression is caught even if admin_assets_gen.go were
   regenerated with a modified copy of generate.mjs.

3. webui/components.json's iconLibrary:"lucide"/font:"inter" fields
   cannot be fixed in place - checked shadcn-vue's own schema
   (PRESET_ICON_LIBRARIES/PRESET_FONTS in the installed package): no
   "none"/fontawesome/system choice exists for either, and JSON (unlike
   the tsconfig.*.json files touched in the previous commit) has no
   comment syntax to warn inline. Documented instead: webui/README.md
   gained a loud warning section naming the exact regression (already
   hit twice this branch - a re-added Google Fonts import, a re-added
   lucide icon import) and the two-step fix to apply after any future
   shadcn-vue add/init call.

4. Real-CSP verification: the browser pass earlier in this branch ran
   under vite preview, which sends no CSP header at all. New
   integration/integration_test.go: TestAdminAssetCSP fetches GET
   /admin against the real Traefik+Yaegi pipeline, extracts the asset
   URL from its <script src="..."> tag, fetches that asset, and asserts
   both responses carry the exact adminCSP string byte-for-byte.
   admin.go's serveAdminAsset now also sets Content-Security-Policy
   (previously JSON/page routes only) so this holds for a direct
   navigation to an asset URL, not just as a same-origin sub-resource of
   GET /admin; admin_test.go's TestAdminAsset_Serving updated to match.

5. (folded) UsageChart.vue stacked the "tokens" tab's two series
   (tokin/tokout - two separate HTTP requests, not one atomic call) by
   positional index. A window rollover landing between the two requests
   could leave one series one bucket ahead, mis-stacking that refresh.
   Now aligns both series by their bucket key (a sorted union of every
   active series' buckets - the fixed-width, zero-padded bucket strings
   sort chronologically by construction), with a missing bucket in one
   series reading as 0 rather than shifting the whole series.

6. (folded) AuthGate.vue now triggers an immediate dashboard.refresh()
   on successful key submit, instead of waiting up to 5s for the next
   poll tick - the dashboard no longer sits blank right after auth.

Rebuilt via make admin-ui; admin_assets_gen.go regenerated.
This commit is contained in:
2026-08-21 09:43:24 +01:00
parent afdb7415db
commit b98416d1e9
10 changed files with 1047 additions and 823 deletions
+15 -9
View File
@@ -839,15 +839,21 @@ or in CI.
- **Response headers**: all three JSON routes set
`X-Content-Type-Options: nosniff` and `Cache-Control: no-store`; every
hashed asset sets `X-Content-Type-Options: nosniff` and its own
immutable `Cache-Control` (above). `GET /admin` and the three JSON
routes share one `Content-Security-Policy` header (`default-src 'none';
script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self'
data:; frame-ancestors 'none'; base-uri 'none'; form-action 'none'`) —
no `'unsafe-inline'`, since the built app has no inline script or style
of any kind, only external `/admin/assets/*` references; `img-src`
additionally allows `data:` for the page's inlined favicon. The
dashboard loads no external asset of any kind and works in an
air-gapped cluster.
immutable `Cache-Control` (above). Every one of the four routes — `GET
/admin`, every hashed asset, and the three JSON routes — shares one
`Content-Security-Policy` header (`default-src 'none'; script-src
'self'; style-src 'self'; connect-src 'self'; img-src 'self' data:;
frame-ancestors 'none'; base-uri 'none'; form-action 'none'`) — no
`'unsafe-inline'`, since the built app has no inline script or style of
any kind, only external `/admin/assets/*` references; `img-src`
additionally allows `data:` for the page's inlined favicon. A browser
only enforces CSP against the top-level document that names it, so an
asset response's own header governs nothing when the browser fetched it
as a `<script src>`/`<link>` sub-resource of `GET /admin` — it matters
only if that asset URL is ever navigated to directly, and every other
admin route already sends the identical header, so there is no reason
for asset responses to be the exception. The dashboard loads no
external asset of any kind and works in an air-gapped cluster.
## MCP and A2A