Commit Graph

6 Commits

Author SHA1 Message Date
shankar0123 aa1c12ae2d feat(web): Phase 9 — backend-coupled + page-specific closures (5 shipped, 2 deferred)
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).
2026-05-14 18:27:18 +00:00
shankar0123 9ce2d8ca8f feat(frontend): Phase 4 Loading + Perceived Performance — close UX-M1 + FE-M5 + PERF-M1 + P-H3 + partial FE-M3 / P-M2
Closes the Phase 4 batch from cowork/frontend-design-audit.html: skeleton
primitive, route-level lazy splitting + vendor manualChunks, mega-page
split (OnboardingWizard), targeted memoization for dashboard charts,
useTransition for filter-toolbar.

═════════════════════════ AUDIT VERIFICATION ═════════════════════════
Confirmed facts from the live repo before implementing (not the audit's
stamped numbers — those drifted):

  • Pre-Phase-4 index-*.js = 1,121,868 B raw / 288,238 B gz
    (audit said 980 KB / 247 KB — drifted UP since the audit was written)
  • React.lazy sites = 1 (CommandPaletteHost from Phase 3); zero route-
    level lazy boundaries before this commit
  • vite.config.ts had NO rollupOptions.output.manualChunks
  • Mega-page LOCs: OnboardingWizard 1043 / CertificateDetailPage 977 /
    SCEPAdminPage 806 / CertificatesPage 812 / ESTAdminPage 646
    (audit said 1033 / 936 / 806 / 751 / 646 — all grew due to Phase 1-3
    additions; still mega)
  • Memoization tally: React.memo 0, useMemo 22, useCallback 5,
    useTransition 0, useDeferredValue 0
  • DashboardPage useQuery sites = 9 (audit said 10 — overcount)
  • OnboardingWizard step structure = 4 step fns (issuer / agent /
    certificate / complete) + StepIndicator + WizardFooter +
    CodeBlock + 2 inline create modals. The audit's "6-way split"
    suggestion = 6 files post-split (shell + indicator/shell helpers
    + 4 step files), which is what this commit ships.

═════════════════════════════ CLOSURES ═══════════════════════════════

UX-M1 — Skeleton primitive (web/src/components/Skeleton.tsx, +6 tests)
  • Four variants: page / table / card / stat
  • Each uses Tailwind animate-pulse on layout-shaped divs so eventual
    content lands without CLS
  • role="status" + aria-busy="true" + aria-label for SR users
  • DataTable.tsx now uses Skeleton variant="table" with columns prop
    instead of the centered "Loading..." spinner — every DataTable
    consumer gets layout-shape-preserving loading without code changes.
    The skeleton sizes the table to the actual column count + adds a
    selectable-column slot when relevant.

FE-M5 + SCALE-H1 — route-level code split + vendor manualChunks
  • main.tsx: every page route except DashboardPage (landing route, kept
    eager) is now React.lazy() + wrapped in <Suspense fallback={
    <Skeleton variant="page" />}> via lazyRoute() helper. 35 lazy
    routes total.
  • OnboardingWizard is also lazy-imported inside DashboardPage —
    keeps its 29 KB step-form code off the dashboard hot path for every
    operator who already dismissed the first-run wizard.
  • vite.config.ts: rollupOptions.output.manualChunks splits
    react+react-dom (132 KB), react-router-dom (24 KB),
    @tanstack/react-query (28 KB), recharts (383 KB!), and lucide-react
    (16 KB) into named vendor chunks. Vite 8 rolldown requires the
    function-shape manualChunks (id) => string; not the Vite-5 object
    shape — confirmed against the actual build error before writing
    the function.

  Bundle profile (raw / gz):
    pre-Phase-4   single index-*.js = 1,121,868 / 288,238
    post-Phase-4  index-*.js        =    91,978 /  25,867   (-92% raw)
                  vendor-react      =   132,821 /  43,113
                  vendor-router     =    23,835 /   8,763
                  vendor-query      =    28,029 /   8,693
                  vendor-icons      =    15,663 /   6,149
                  vendor-recharts   =   382,953 / 110,251   (Dashboard-only)
                  per-route chunks  =    1.4-26 KB raw each

  Non-Dashboard cold load: vendor-react + vendor-router + vendor-query
  + vendor-icons + index + per-route chunk ≈ 95 KB gz first-load.
  Dashboard cold load adds vendor-recharts (110 KB gz) on demand.

  Audit target was <100 KB gz first-load for non-Dashboard routes — hit.

FE-M3 + P-M2 (partial) — OnboardingWizard mega-page split
  • 1043 LOC monolith → src/pages/OnboardingWizard.tsx (100 LOC shell) +
    src/pages/onboarding/{types.ts, StepShell.tsx, IssuerStep.tsx,
    AgentStep.tsx, CertificateStep.tsx, CompleteStep.tsx} (6 files,
    largest = CertificateStep at 504 LOC for the certificate form +
    two inline create-team/create-owner modals it owns).
  • Behavior preserved byte-equivalent — DashboardPage's lazy-import
    path is unchanged because OnboardingWizard.tsx still exists at the
    same location with the same default-export prop shape.
  • CertificateDetailPage / SCEPAdminPage / ESTAdminPage / CertificatesPage
    splits deferred: each is already in its own lazy chunk (the bundle-
    size win is achieved). Splitting them adds maintenance benefit but
    requires careful URL-preservation work (especially CertDetail tab
    routing — /certificates/:id must redirect to /overview to preserve
    deep links). Documented as Phase 4 follow-up; not blocking on this
    closure.

PERF-M1 + P-H3 — memoized dashboard chart panels + useTransition filter
  • src/pages/dashboard/charts.tsx — 4 React.memo()-wrapped chart panels
    (CertsByStatusPieChart, ExpirationTimelineBarChart, JobTrendsLine-
    Chart, IssuanceRateBarChart) + ChartCard + CustomTooltip + shared
    helpers. Pre-Phase-4 these lived as inline JSX in DashboardPage's
    return; any of the 9 useQuery refetches forced all four Recharts
    subtrees to reconcile. Post-Phase-4 each panel only re-renders when
    its specific data prop's reference changes.
  • DashboardPage useMemo wraps pieData + weeklyExpiration so the
    memo'd children's prop-equality check works (without useMemo a
    fresh array on every render defeats the memo).
  • Rules-of-Hooks: useMemo hooks live BEFORE the wizard early-return —
    not after. (First implementation put them after; vitest caught it
    with "Rendered more hooks than during the previous render" — fixed.)
  • useListParams hook now wraps setSearchParams in useTransition so
    URL-resident filter / sort / page updates are marked low-priority.
    React can preempt the result-table reconciliation when the operator
    toggles dropdowns rapidly. Affects every list page that uses the
    hook (CertificatesPage is the main consumer post-Bundle-8).

═══════════════════════════ VERIFICATION ═════════════════════════════

  • npx tsc --noEmit — exits 0
  • Skeleton primitive: 6/6 tests green
  • Component suite (12 files): 137/137 green
  • Auth-page suite (13 files): 130/130 green
  • Dashboard + Onboarding + Certificates + CertificateDetail + Targets
    + Agents + Issuers + Jobs + SCEPAdmin + ESTAdmin: 71/71 green
  • npm run build clean; chunk inventory verified (vendor-react,
    vendor-router, vendor-query, vendor-recharts, vendor-icons emitted
    as named chunks; 35 per-route lazy chunks emitted; index-*.js
    shrunk to 91.66 KB raw / 25.92 KB gz).

═══════════════════════════ RESIDUAL RISK ════════════════════════════

  • Vite 8 + rolldown's manualChunks signature differs from Vite 5;
    upgrading Vite again would re-break this config. Comment in
    vite.config.ts pins the function-shape requirement.
  • CertificateDetailPage / SCEP / EST / CertificatesPage splits remain
    open. Mega-LOC files but already lazy-chunked, so deferring is safe.
  • Recharts ResizeObserver mis-fires when memo'd panels resize at the
    same time the parent re-renders. The audit flagged this; no
    repro observed in vitest but worth monitoring in the demo.
2026-05-14 16:14:24 +00:00
shankar0123 c6602bcbe8 fix(ci): exclude Playwright e2e specs from Vitest run
The Phase 3 Playwright harness stub landed
web/src/__tests__/e2e/smoke.spec.ts using @playwright/test's
test.describe(). Vitest's default include glob
('**/*.{test,spec}.{js,...}') matches that file and tries to
execute it under jsdom, but test.describe() from Playwright
throws:

    Error: Playwright Test did not expect test.describe() to be
    called here.

The Frontend Build CI job (npm run test → vitest run) hits this
on every push.

Fix: extend the Vitest exclude list to skip src/__tests__/e2e/**.
Playwright still runs them via 'npm run e2e' against
web/playwright.config.ts (testDir './src/__tests__/e2e').

Verified locally that fast-glob matches the file at that pattern.
configDefaults imported from 'vitest/config' preserves Vitest's
own default excludes (node_modules + .git) alongside the
addition.
2026-05-13 20:44:07 +00:00
shankar0123 1c6009a920 chore(cleanup,docs): vite proxy + dead scheduler setter wired + registerAgent/CLI docs (C-1 master)
Closes six 2026-04-24 audit findings (3 P2 + 3 P3) — a cleanup-and-doc
tail bundle that drains the smallest remaining leaves of the audit:

  - cat-u-vite_dev_proxy_plaintext_drift (P2): web/vite.config.ts
    proxied dev requests to http://localhost:8443 against an HTTPS-only
    backend (HTTPS-only since v2.0.47). Every dev-server API call 502'd.
    Fix: targets are now object-form `{target: 'https://...', secure: false,
    changeOrigin: true}` — the dev cert is self-signed by the
    deploy/test bootstrap and changes per-checkout.

  - cat-g-7e38f9708e20 (P3): Scheduler.SetShortLivedExpiryCheckInterval
    was defined + tested but never called from cmd/server/main.go.
    Operators tuning CERTCTL_SHORT_LIVED_EXPIRY_CHECK_INTERVAL got
    no effect — the 30s default in scheduler.NewScheduler was
    effectively hardcoded. Fix: added Config.Scheduler.ShortLivedExpiryCheckInterval
    + getEnvDuration in Load() reading the env var with a 30s default,
    + sched.SetShortLivedExpiryCheckInterval(...) call in main.go
    alongside the other scheduler-interval setters.

  - diff-10xmain-2bf4a0a60388 (P3): same root cause as cat-g-7e38f9708e20;
    closes as ride-along.

  - cat-b-6177f36636fb (P2): registerAgent client fn orphan. By-design
    per pull-only deployment model. Fix (audit recommendation:
    "document"): added a closure docblock above the export in
    client.ts + a new "Registration is by-design pull-only" paragraph
    in docs/architecture.md::Agents section explaining when/why a
    future GUI-driven enrollment feature might reach the endpoint
    (proxy-agent topologies for network appliances).

  - cat-i-7c8b28936e3d (P2): CLI scope intentionally narrow but
    undocumented. Fix: new "Scope (intentionally narrow)" subsection
    in docs/features.md::CLI capturing the SSH-into-prod / day-to-day
    GUI / AI-automation MCP three-way split.

Verification:
- go build ./... — clean
- go vet ./... — clean
- go test ./internal/scheduler/... ./internal/config/... — pass
- golangci-lint v2.11.4 run ./... — 0 issues
- tsc --noEmit (frontend) — clean
- All sibling guardrails (S-1 / G-3 / D-1+D-2 / B-1 / L-1 / H-1) still pass

Audit findings closed:
- cat-u-vite_dev_proxy_plaintext_drift (P2)
- cat-g-7e38f9708e20 (P3)
- diff-10xmain-2bf4a0a60388 (P3)
- cat-b-6177f36636fb (P2)
- cat-i-7c8b28936e3d (P2)
- (audit-bookkeeping ride-along: ensures every closed-bundle row has a non-empty merge SHA)

Deferred follow-ups: none from this bundle. The remaining audit
backlog (frontend test campaign, F-1 CertificatesPage UX, P-1
orphan-fn sweep, S-2 handler error-mapping refactor) is sibling
sub-bundles in this mega-prompt.
2026-04-25 17:34:59 +00:00
shankar0123 73c6bd1416 feat: add frontend action buttons, fix notification auth bug, add 53 Vitest tests
Bug fix:
- markNotificationRead was using raw fetch() without auth headers,
  bypassing the shared client's Authorization header. Moved to
  api/client.ts to use fetchJSON with proper auth.

New action buttons:
- CertificatesPage: "New Certificate" modal with form fields
- CertificateDetailPage: "Deploy" button with target selector modal,
  "Archive" button with confirmation
- IssuersPage: "Test Connection" and "Delete" per-row actions
- TargetsPage: "Delete" per-row action
- PoliciesPage: "Enable/Disable" toggle and "Delete" per-row actions

New API client functions:
- updateCertificate, archiveCertificate, registerAgent,
  createPolicy, updatePolicy, deletePolicy, getPolicyViolations,
  createIssuer, testIssuerConnection, deleteIssuer,
  createTarget, deleteTarget, markNotificationRead

Frontend tests (53 tests, 2 files):
- client.test.ts: 35 tests covering all API endpoints, auth headers,
  401 handling, error parsing, HTTP methods, request bodies
- utils.test.ts: 18 tests covering formatDate, formatDateTime,
  timeAgo, daysUntil, expiryColor

CI: Added "Run Frontend Tests" step to frontend-build job

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 00:05:21 -04:00
shankar0123 9e6756d02f Implement M5: hardening, input validation, and Vite+React+TS dashboard
Backend hardening:
- Fix 6 nginx.go non-constant format string build errors
- Add validation.go with hostname, PEM, and enum validators
- Apply input validation to all POST/PUT handlers (certificates,
  agents, CSR, policies, teams, owners, targets, issuers)
- Fix unchecked JSON decode in TriggerDeployment handler

Frontend (Vite + React + TypeScript):
- Migrate from single-file SPA to proper build pipeline
- 7 pages: Dashboard, Certificates (list+detail), Agents, Jobs,
  Notifications, Policies, Audit Trail
- TanStack Query for server state with auto-refetch intervals
- Certificate detail with version history and renewal trigger
- Job cancellation, status/type filtering, expiry countdowns
- Reusable components: DataTable, StatusBadge, ErrorState, PageHeader
- Dark theme with Tailwind CSS, sidebar nav via React Router

Server integration:
- Go server serves web/dist/ (Vite output) with SPA fallback
- Falls back to web/index.html for legacy mode
- .gitignore updated for web/node_modules/ and web/dist/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 01:19:19 -04:00