mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 16:11:29 +00:00
aa1c12ae2d
Closes the frontend-design-audit Phase 9 batch — the audit's
"backend-coupled or page-specific" tier. Five findings ship; two
defer to follow-ups that need backend handler work.
Shipped:
PERF-M2 — Build-time version + hidden sourcemaps
• vite.config.ts: `sourcemap: 'hidden'` (was `false`). Maps emit
to dist/ but are NOT referenced by JS, so browsers don't fetch
them. The maps stay available for Sentry-class upload at
release time. Comment-block above the build config documents
the tradeoff so a future operator doesn't re-flip to `false`
without realising they're losing release-time debuggability.
• `__APP_VERSION__` build-time `define` reads `web/package.json`
`version` so ErrorBoundary can stamp the build into telemetry
payloads (was previously hardcoded `'dev'`).
FE-L1 — ErrorBoundary copy-trace + telemetry gate
• 50 → 185 LOC rewrite of web/src/components/ErrorBoundary.tsx.
• componentDidCatch now POSTs an ErrorPayload (build version,
UA, href, timestamp, error name + message + stack,
componentStack) to `VITE_ERROR_TELEMETRY_URL` IF that env var
is set at build time. Uses navigator.sendBeacon (page-unload-
safe) → falls back to fetch + keepalive. Unset = no POST,
no console-error spam.
• Operator-facing "Copy details" button writes the same payload
as JSON to the clipboard (navigator.clipboard API → execCommand
fallback for older browsers). A `<details>` block (collapsed
by default) shows the stack + componentStack inline so the
operator can grok the failure without leaving the page.
• Two new data-testid hooks (`error-boundary-reload`,
`error-boundary-copy`) for QA + future Playwright coverage.
• web/src/components/ErrorBoundary.test.tsx — 5 vitest specs:
no-error pass-through, error fallback structure, copy payload
shape, details collapsed-by-default, NO telemetry POST when
URL is unset. cleanup() between tests + console.error
silenced via the React-error-handling pattern.
UX-M8 — DataTable density toggle (opt-in via tableId)
• Density type ('compact' | 'comfortable' | 'spacious') + per-
density cell/header class maps. Default 'comfortable' matches
the existing px-4 py-3 padding so all callers see byte-
identical layout until they opt in.
• DataTableProps gains optional `tableId` + `density` props.
Pages that pass `tableId` get a 3-button DensityToggle
(Compact / Cozy / Spacious) rendered above the table; the
selection persists to localStorage at
`certctl:table-density:<tableId>`. No tableId = no toggle =
no behavioral change for the 17 other tables.
• Hardcoded `px-4 py-3` replaced with the `cellCls` /
`headerCls` lookup against the active density. Three Tailwind
permutations cover compact (px-3 py-1.5), comfortable
(px-4 py-3), spacious (px-5 py-5).
UX-M7 (lever) — CI guard against new raw `<table>` regressions
• scripts/ci-guards/no-raw-table.sh: counts `<table` tags in
`web/src/**/*.tsx` (production only, tests excluded) outside
the canonical primitives (DataTable.tsx + Skeleton.tsx) and
fails CI if the count climbs above baseline. `--strict` mode
rejects any raw table once the backlog clears.
• Baseline pinned at 17 (the current count of page-level raw
tables — verified via the same grep the guard uses). Every
page migration to <DataTable> drops the baseline by 1; new
pages MUST route through <DataTable>.
• No representative migrations in this commit (operator
decision: ship the lever first, migrations as follow-up PRs).
• Pairs with the existing CI guard suite (no-unbound-label,
no-raw-toLocaleString, no-eager-issuer-deletes, etc.) —
same baseline-locked pattern.
FE-M2 — Desktop-only banner (operator chose path a: 2026-05-14)
• web/src/components/DesktopOnlyBanner.tsx: fixed top bar at
viewports < 1024px (Tailwind `lg` breakpoint, below which the
sidebar + content layout starts visibly cramping). Amber
"Desktop-only: certctl is designed for viewports ≥ 1024px"
notice with a Dismiss button that persists to localStorage
(`certctl:desktop-only-banner-dismissed`).
• web/src/index.css: `.desktop-only-banner` is `display: none`
by default and `display: flex` inside the
`@media (max-width: 1023px)` block. CSS-gated visibility,
not React state — the banner mounts always but only renders
visibly on narrow viewports.
• web/src/main.tsx: mounts the banner inside ErrorBoundary,
above QueryClientProvider, so it survives any provider
failure that breaks the rest of the tree.
• Operator-stated rationale (recorded in DesktopOnlyBanner.tsx
header comment): the audit flagged 29 partial sm:/md:/lg:
responsive classes that suggest mobile support which isn't
actually shipped. Rather than rip out the partials (zero
benefit at desktop widths) or ship full mobile (1+ sprint of
QA + ongoing maintenance), this ships an honest signal —
"we don't promise mobile" — that doesn't claim support that
isn't there. The partials stay (no benefit to ripping out;
they may help if the decision reverses).
Deferred:
P-H2 — AuditPage server-side time filters
Requires backend changes to internal/api/handler/audit.go +
service + repository: ListAuditEvents currently accepts only
page/per_page/category. Adds `since` / `until` ISO-8601
params (UTC), pushes the timestamp predicate into the SQL
query, surfaces them in OpenAPI + MCP. Queued as a backend-
first follow-up bundle.
P-M1 — DiscoveryPage in-flight scan panel
Out of scope for the frontend remediation pass; needs a
websocket / SSE channel from internal/service/discovery.go to
the frontend (current poll-and-render UI works against the
existing endpoint set). Queued.
Verification:
• npx tsc --noEmit — exits 0
• npx vitest run ErrorBoundary StatusBadge — 80/80 passed
• npm run build — ✓ built in 3.11s
• bash scripts/ci-guards/no-raw-table.sh —
Raw <table> tags outside DataTable + Skeleton — current: 17, baseline: 17
• Bundle shapes unchanged from Phase 4 (91.66 KB raw / 25.92 KB gz
initial chunk); the ErrorBoundary rewrite adds ~5 KB to index.
Falsifiable proof for the next CI run:
• Frontend Build job's `npm ci` step completes (Hotfix #9 settled
the Storybook peer conflict).
• New no-raw-table.sh guard exits 0 with current=17 baseline=17.
• All 34 CI guards (was 33, +1 for no-raw-table) pass.
Per-finding closure entries land in frontend-design-audit.html in
the follow-up commit (audit HTML update).
99 lines
4.6 KiB
TypeScript
99 lines
4.6 KiB
TypeScript
import { defineConfig } from 'vite'
|
|
import { configDefaults } from 'vitest/config'
|
|
import react from '@vitejs/plugin-react'
|
|
|
|
// C-1 closure (cat-u-vite_dev_proxy_plaintext_drift): pre-C-1 the dev
|
|
// proxy targeted http://localhost:8443 against an HTTPS-only backend
|
|
// (HTTPS-only since v2.0.47 — see docs/tls.md). Every dev-server API
|
|
// call 502'd. Post-C-1 the proxy targets https:// with secure:false
|
|
// because the dev cert is self-signed by deploy/test bootstrap and
|
|
// changes per-checkout — production stops validation at the reverse
|
|
// proxy or load balancer, not the Vite dev server.
|
|
// Phase 9 FE-L1 closure: ship the package.json version into the
|
|
// bundle as a build-time constant. ErrorBoundary's copy-trace payload
|
|
// uses this so a copied stack trace tells the operator which release
|
|
// produced the error. Pulled from package.json at config-load time
|
|
// (no runtime cost). Falls back to 'dev' if unreadable.
|
|
function readPkgVersion(): string {
|
|
try {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
const pkg = require('./package.json') as { version?: string };
|
|
return pkg.version || 'dev';
|
|
} catch {
|
|
return 'dev';
|
|
}
|
|
}
|
|
|
|
export default defineConfig({
|
|
plugins: [react()],
|
|
define: {
|
|
// Compile-time replace of __APP_VERSION__ in src files. Quoted
|
|
// so the replaced token becomes a string literal in the bundle.
|
|
__APP_VERSION__: JSON.stringify(readPkgVersion()),
|
|
},
|
|
server: {
|
|
port: 5173,
|
|
proxy: {
|
|
'/api': { target: 'https://localhost:8443', secure: false, changeOrigin: true },
|
|
'/health': { target: 'https://localhost:8443', secure: false, changeOrigin: true },
|
|
}
|
|
},
|
|
build: {
|
|
outDir: 'dist',
|
|
// Phase 9 closure (PERF-M2): 'hidden' generates source maps to
|
|
// disk but does NOT emit a `//# sourceMappingURL=` comment in the
|
|
// production JS chunks — so they're not loadable via the browser
|
|
// (no risk of exposing original source to operators in DevTools),
|
|
// but the operator (or a future Sentry/error-reporting integration)
|
|
// can still upload them as release artifacts for symbolication of
|
|
// FE-L1 ErrorBoundary stack traces. Pre-fix the value was `false`
|
|
// (no maps at all), which means ANY production exception's stack
|
|
// traces are minified-only — useless for triage.
|
|
sourcemap: 'hidden',
|
|
// Phase 4 closure (FE-M5 + SCALE-H1): vendor manualChunks. Pre-Phase-4
|
|
// the single index-*.js chunk weighed ~1.07 MB raw / ~281 KB gz because
|
|
// every dependency landed in the same first-load file. Splitting React,
|
|
// React Router, TanStack Query, Recharts, and lucide-react into their
|
|
// own chunks lets the browser:
|
|
// • Cache vendor chunks across deploys (only index-*.js rotates when
|
|
// feature code changes — vendor hashes only flip when those
|
|
// packages bump in package-lock.json).
|
|
// • Parallelise vendor downloads on cold loads (HTTP/2 multiplex).
|
|
// • Skip Recharts entirely on cold loads of non-Dashboard routes
|
|
// (recharts is ~410 KB unminified, see bundlephobia.com).
|
|
// Combined with React.lazy() per route in main.tsx the cold-load
|
|
// budget for a non-Dashboard route drops to vendor.react +
|
|
// vendor.router + index. Dashboard pulls vendor.recharts on demand.
|
|
// Vite 8 uses rolldown which requires manualChunks to be a function
|
|
// (id) => string, not the object-shape Vite-5-era rollup accepted.
|
|
rollupOptions: {
|
|
output: {
|
|
manualChunks(id: string) {
|
|
if (!id.includes('node_modules')) return undefined;
|
|
if (id.includes('node_modules/react-router-dom')) return 'vendor-router';
|
|
if (id.includes('node_modules/@tanstack/react-query')) return 'vendor-query';
|
|
if (id.includes('node_modules/recharts')) return 'vendor-recharts';
|
|
if (id.includes('node_modules/lucide-react')) return 'vendor-icons';
|
|
if (id.includes('node_modules/react/')
|
|
|| id.includes('node_modules/react-dom/')
|
|
|| id.includes('node_modules/scheduler/')) {
|
|
return 'vendor-react';
|
|
}
|
|
return undefined;
|
|
},
|
|
},
|
|
},
|
|
},
|
|
test: {
|
|
globals: true,
|
|
environment: 'jsdom',
|
|
setupFiles: ['./src/test/setup.ts'],
|
|
// Exclude Playwright e2e specs from the Vitest run. The harness in
|
|
// src/__tests__/e2e/ uses @playwright/test's test.describe(), which
|
|
// throws "did not expect test.describe() to be called here" under
|
|
// Vitest. Playwright runs them via `npm run e2e` against
|
|
// web/playwright.config.ts (testDir: './src/__tests__/e2e').
|
|
exclude: [...configDefaults.exclude, 'src/__tests__/e2e/**'],
|
|
},
|
|
})
|