Skip to content

test(accounts): regression guards for discover-org admin scope + 5xx-on-cred-failure (post-#212 CR)#222

Merged
cristim merged 1 commit into
feat/multicloud-web-frontendfrom
test/issue-208-discover-admin-and-5xx-regression-guards
May 3, 2026
Merged

test(accounts): regression guards for discover-org admin scope + 5xx-on-cred-failure (post-#212 CR)#222
cristim merged 1 commit into
feat/multicloud-web-frontendfrom
test/issue-208-discover-admin-and-5xx-regression-guards

Conversation

@cristim
Copy link
Copy Markdown
Member

@cristim cristim commented May 3, 2026

Follow-up to PR #212 — addresses the only un-applied CR finding from that PR's reviews.

Background

PR #212 wired the AWS Organizations discovery endpoint and went through three CR rounds (15:25Z, 19:32Z, 19:42Z on 2026-04-30). It was merged at 20:34Z with most CR findings addressed in-line, but one nitpick from the 19:32Z review didn't make it into the merge:

Add regression tests for the admin-only and 5xx behaviors. This block locks down the 400/404 paths, but it still doesn't assert the two review fixes that changed the handler contract: non-admin callers must be rejected, and credential-resolution failures must not surface as client errors.

Both behaviours are correctly implemented in the merged code; without tests they're a refactor away from quietly regressing.

What this PR adds

Two test cases in internal/api/handler_accounts_test.go:

TestDiscoverOrgAccounts_RejectsNonAdmin

Non-admin session must hit a 403 ClientError. Without this guard, a refactor that swaps requireAdmin back to requirePermission("create","accounts") silently re-opens org discovery to non-admin users — and discovered rows go straight into cloud_accounts, which is a privilege-escalation surface (not just a UX preference).

TestDiscoverOrgAccounts_CredResolutionFailureIs5xx

Credential-resolution failures must surface as 5xx (retryable), not 4xx. Wraps a CredentialStore stub (errCredStore) whose LoadRaw returns a transient-style error; asserts the handler returns a non-ClientError carrying the "resolve credentials" stage marker so operators can find it in logs. Without this, a future refactor that re-applies NewClientError(400, ...) on resolver errors would silently make transient store/network failures non-retryable.

The test's discoverOrgFn injection is a t.Fatal — proving the failure path exits before reaching discovery (and that the test is exercising the credential-resolution failure path specifically, not a different shortcut).

Notes on the other CR findings

PR #212 had 5 distinct CR concerns across 3 reviews. Status:

# Concern Status
1 Member ID/CreatedAt/UpdatedAt init missing ✅ fixed in #212
2 Duplicate-key race not handled in persist loop ✅ fixed in #212
3 AWSAuthMode = "" will fail DB CHECK constraint ⚠️ false alarm — see below
4 Nil guard on disco.Accounts ✅ fixed in #212
5 Stale aws_auth_mode=bastion comments ✅ fixed in #212
6 Tests for admin-only + cred-resolution → 5xx this PR

#3 false alarm

The store layer uses nullStringFromString(account.AWSAuthMode) for the INSERT (internal/config/store_postgres.go:CreateCloudAccount). That converts ""sql.NullString{Valid: false} → SQL NULL. The cloud_accounts.aws_auth_mode column allows NULL (no NOT NULL constraint in migrations/000011_cloud_accounts.up.sql:18-19), and Postgres CHECK (col IN (...)) constraints satisfy NULL by default (the constraint is vacuously true on NULL). So persisting member.AWSAuthMode = "" is correct as-is. I'll reply on the CR thread with this rationale.

Verification

Triage

type/chore, severity/medium, urgency/this-sprint, impact/internal, effort/xs, priority/p2, triaged. Pure-test PR; no behaviour change.

Summary by CodeRabbit

  • Tests
    • Added regression tests for organization account discovery to verify non-admin access is properly rejected with appropriate error codes and credential resolution failures are handled as expected.

…on-cred-failure (post-#212 CR)

Two test additions for the CR pass-2 nitpick on PR #212 (which
landed without these regression guards). Both new cases lock down
behaviour the CR-pass-1 fix introduced:

* TestDiscoverOrgAccounts_RejectsNonAdmin — non-admin session must
  hit a 403 ClientError. Without this guard, a refactor that swaps
  requireAdmin back to requirePermission("create","accounts")
  silently re-opens org discovery to non-admin users, and discovered
  rows go straight into cloud_accounts (privilege-escalation surface,
  not just UX preference).

* TestDiscoverOrgAccounts_CredResolutionFailureIs5xx — credential-
  resolution failures must surface as 5xx, not 4xx. Wraps a
  CredentialStore stub whose LoadRaw returns a transient-style
  error; asserts the handler returns a non-ClientError carrying the
  "resolve credentials" stage marker. Without this, a future refactor
  that re-applies NewClientError(400, ...) on resolver errors would
  silently make transient store/network failures non-retryable.

Plus a small fakeCredStore-style helper, errCredStore, that
satisfies the CredentialStore interface and always errors from
LoadRaw — used only by the second test.

CR also flagged a "AWSAuthMode = '' will violate the DB CHECK
constraint" actionable; verified false alarm — the storage layer
uses nullStringFromString(account.AWSAuthMode) which converts
empty-string to sql.NullString{Valid: false} → SQL NULL → satisfies
CHECK by Postgres semantics. No code change for that one; reply
with justification on the CR thread.

Verification: go test ./internal/api/... full-package green;
new cases pass (and prove they are real guards by t.Fatal'ing the
discoverOrgFn in the cred-failure case to assert the failure path
exits before reaching discovery).
@cristim cristim added type/chore Maintenance / non-user-visible severity/medium Moderate harm urgency/this-sprint Within the current sprint impact/internal Team-internal only effort/xs Trivial / one-liner priority/p2 Backlog-worthy triaged Item has been triaged labels May 3, 2026
@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 3, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 3, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: eacf4944-222e-4f35-b6b2-1920c010c4e1

📥 Commits

Reviewing files that changed from the base of the PR and between c84fd02 and 7e8bab7.

📒 Files selected for processing (1)
  • internal/api/handler_accounts_test.go

📝 Walkthrough

Walkthrough

Added regression tests for discoverOrgAccounts handler to validate non-admin session rejection with 403 errors and credential resolution failures as non-client (5xx retryable) errors. Included test stub errCredStore and fixed compilation issue in existing test.

Changes

Test Additions for discoverOrgAccounts Handler

Layer / File(s) Summary
Test Support Stub
internal/api/handler_accounts_test.go
Added errCredStore type implementing credential store interface with all methods stubbed to return injected failure from LoadRaw, simulating transient credential-resolution outages.
Regression Tests
internal/api/handler_accounts_test.go
Added TestDiscoverOrgAccounts_RejectsNonAdmin validating non-admin sessions are rejected with ClientError code 403; added TestDiscoverOrgAccounts_CredResolutionFailureIs5xx validating credential resolution failures surface as non-ClientError (5xx retryable) and discoverOrgFn is not called.
Test Compilation Fix
internal/api/handler_accounts_test.go
Removed stray numeric literals from TestDiscoverOrgAccounts_NotFound that prevented test file compilation.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Suggested labels

effort/s

Poem

🐰 A rabbit hops through test files bright,
Catching errors left and right,
Non-admins blocked, credentials fail—
Coverage grows, no stone untailed! 🧪✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding regression tests for admin scope rejection and 5xx error handling on credential resolution failures in the discover-org functionality.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/issue-208-discover-admin-and-5xx-regression-guards

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Review rate limit: 4/5 reviews remaining, refill in 12 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 3, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim cristim merged commit c8a78cb into feat/multicloud-web-frontend May 3, 2026
4 checks passed
@cristim cristim deleted the test/issue-208-discover-admin-and-5xx-regression-guards branch May 3, 2026 15:56
cristim added a commit that referenced this pull request May 12, 2026
…340)

Final small-batch migrations on components / settings / modals:

- components.css: `#f5f7fa` → `var(--cudly-bg)`, `#fbbc04` →
  `var(--cudly-warn)`, `#f0f0f0` → `var(--cudly-border)`.
- settings.css: `#e6f4ea` → `var(--cudly-success-bg)`,
  `#ccc` → `var(--cudly-border-strong)`, `#fafafa`/`#f9f9f9` →
  `var(--cudly-surface-muted)`.
- modals.css: `#555` → `var(--cudly-text-muted)`,
  `#eee` → `var(--cudly-border)`.

Remaining hex literals across the bundle are all niche saturation
shades that don't have a clean 1:1 token equivalent (the orange/amber
provider badges `#f4a261`/`#f9a825`/`#856404`, the green utilization
indicators `#137333`/`#1e8e3e`/`#2d9048`, the dark gray accents
`#222`/`#444`/`#bbb`, status-bg variants on tables `#cce5ff`/`#d4edda`/
`#f8d7da`/`#fef3cd`). Adding tokens for each would force semantics
into the design system that aren't shared elsewhere — leaving them
as literals keeps the token vocabulary small + meaningful.

Verified:
- npm test: 1593/1593 pass.
- npm run build: clean.
- Manual browser smoke: no visual changes (token values match literals).
cristim added a commit that referenced this pull request May 12, 2026
…oses #340) (#343)

* style(frontend): introduce design-token CSS variables for UI revamp (issue #340)

Add a single :root block at the top of styles/base.css defining the
action-center design language as CSS custom properties: brand, surfaces,
text, status colors, typography scale, spacing scale, radii, elevation,
and layout dimensions (sidebar width, top-bar height) for the new shell.

Tokens are additive — no existing component is migrated to consume them
in this commit, so the visual diff is zero. Section-by-section migrations
land in subsequent commits per the plan, so the diff stays reviewable.

The body's `background` still uses its previous literal (#f5f5f5) on
purpose — bg gets migrated when the shell lands in T2.

* feat(frontend/ui): action-center shell — top bar + left sidebar nav (issue #340)

Replaces the top-tabbed navigation with the action-center shell pattern
from the mocks: a sticky top bar with the CUDly logo + user info, and a
left sidebar containing the 6 primary navigation items.

Structure:
  .app-shell
    .app-topbar   (sticky, full width)
    .app-body     (flex row)
      .app-sidebar (232px, collapsible to 64px)
        .app-sidebar-nav  (vertical tab list)
      .app-main    (flex-grow content area)

The sidebar uses the new IA labels (Home, Opportunities, Plans,
Purchases, Inventory & Coverage, Admin) but keeps the existing
data-tab values (dashboard, recommendations, ...) for now — section
ID renames land in T3. RI Exchange is wired under the temporary
"Inventory & Coverage" sidebar label so the umbrella section folds
in cleanly in T4 without re-routing.

Each sidebar item carries a Lucide-style inline SVG (ISC license)
following the existing inline-SVG pattern from auth.ts. No new
dependency added.

Sidebar collapse:
- Click hamburger toggle to collapse to icon-only (64px wide).
- State persisted in localStorage as `cudly_sidebar_collapsed`.
- Auto-collapses below 900px viewport (preserves existing desktop
  experience above; doesn't introduce new responsive behaviour).

Verified:
- npm test: 1591/1591 pass.
- npm run build: clean (503 KiB entrypoint, +0.6% from 499 KiB baseline).
- Manual browser smoke: Home/Opportunities/Plans/Purchases/Inventory/Admin
  all reachable; active state highlights correctly; all underlying
  panels render unchanged.

* refactor(frontend): rename tab IDs + labels + add URL redirect for new IA (issue #340)

Rename top-level navigation IDs to match the new action-center IA:

  dashboard       → home
  recommendations → opportunities
  history         → purchases
  settings        → admin
  ri-exchange     unchanged in this commit (folds into Inventory & Coverage in T4)
  plans           unchanged (already the right name)

Touched:
- index.html: tab/panel IDs, data-tab values, aria-controls/-labelledby,
  and the bookkeeping HTML comments above each panel
- navigation.ts: TABS keys, switch cases, default fallback (now 'home'),
  page-title strings (e.g. "CUDly — Home", "CUDly — Admin · General")
- app.ts: deep-link target check ('settings' → 'admin') + /history → /purchases
  in the deep-link landing comment
- recommendations.ts: MutationObserver target ID + comment
- settings-subnav.ts: dirty-state indicator target ID
- purchases-deeplink.ts: post-action redirect /history → /purchases
- 4 test files: navigation.test.ts, recommendations.test.ts, html.test.ts,
  settings-subnav.test.ts — all IDs + switchTab calls + assertion text

URL backwards-compat: a new LEGACY_PATH_REDIRECTS map in navigation.ts
resolves pre-#340 paths (/dashboard, /recommendations, /history,
/settings/*) to the new tab names so existing bookmarks, emails, and
deep-links keep working. applyTabFromPath() walks the redirect table
before TABS lookup; app.ts's initial replaceState then writes the
canonical new URL to the address bar.

Source file names are unchanged (recommendations.ts, history.ts,
settings.ts stay) — only IDs, labels, and user-visible strings move.
Avoids hundreds of import-path changes per the plan's file-rename
discipline rule.

Verified:
- npm test: 1591/1591 pass.
- npm run build: clean.
- Manual browser smoke: /dashboard auto-redirects to /home; clicking
  each sidebar item navigates correctly; page title updates per tab;
  underlying panels render unchanged.

* feat(frontend): fold RI Exchange into Inventory & Coverage umbrella section (issue #340)

Issue #340 T4. The former top-level RI Exchange tab becomes one of three
sub-sections in the new "Inventory & Coverage" umbrella section:

  Inventory & Coverage
    ├─ Active commitments  (placeholder — needs per-commitment list endpoint)
    ├─ Coverage            (placeholder — needs per-provider coverage breakdowns)
    └─ RI Exchange         (existing UI, relocated unchanged — default landing)

Defaulting to RI Exchange keeps the user landing on substantive content
until T7 fills in Active commitments + Coverage. Both placeholders carry
empty-state copy pointing back to the Home dashboard for the data that
*is* available today.

Touched:
- index.html: removed the top-level ri-exchange-tab panel; added a new
  inventory-tab with a 3-button sub-nav and three section children. The
  existing RI Exchange card layout is preserved verbatim under
  inventory-ri-exchange. Sidebar button: data-tab="ri-exchange" →
  data-tab="inventory".
- inventory.ts (NEW, ~80 lines): switchInventorySubSection + loadInventory
  + sub-nav click wiring. Re-uses the existing .sub-tab-btn class for
  styling consistency. Standalone (no shared sub-nav helper) — settings-
  subnav.ts serves a different purpose (sticky scroll-spy rail), so per
  §1a we don't pre-abstract.
- navigation.ts: TABS gets 'inventory'; switch case 'ri-exchange' →
  'inventory' calling loadInventory. The RI Exchange → Inventory legacy
  redirect goes in LEGACY_PATH_REDIRECTS so /ri-exchange bookmarks land
  on /inventory.
- styles/settings.css: .inventory-subnav reuses the existing
  .settings-tabs visual treatment.
- __tests__/inventory.test.ts (NEW, 5 tests): cover sub-section show/hide,
  click-driven switching, default landing, and loadRIExchange triggering.

The two placeholder sub-sections (Active commitments + Coverage) ship
empty-state copy only — no shimmed data. Their backend wiring is tracked
as deferred sub-tasks in #340's body.

Verified:
- npm test: 1596/1596 pass (1591 + 5 new inventory tests).
- npm run build: clean.
- Manual browser smoke: /inventory loads with RI Exchange sub-section
  active; /ri-exchange legacy URL redirects to /inventory; sub-nav
  click on Active commitments + Coverage shows placeholders; RI
  Exchange sub-section renders existing UI unchanged.

* feat(frontend/admin): relabel Admin sub-tabs per new IA (issue #340)

Issue #340 T5. The 4 sub-tabs already exist (General · Purchasing ·
Accounts · Users & API Keys) — only labels change to the action-center
naming:

  General           unchanged
  Purchasing      → Purchasing policies
  Accounts        → Accounts & onboarding
  Users & API Keys→ Users, roles & API keys

The "Settings navigation" aria-label on the .settings-tabs container
becomes "Admin navigation" to match. No structural change, no JS change,
no panel relocation — every existing settings panel keeps its home;
this is pure label work.

URL paths (/admin/general, /admin/purchasing, /admin/accounts,
/admin/users) and the data-settings-tab attribute values (general,
purchasing, accounts, users) stay the same so test fixtures and existing
deep-links don't need updating.

Verified:
- Tests already pass (no test changes needed since data-settings-tab
  values are stable).
- Manual browser smoke: /admin renders with the new sub-tab labels;
  switching between them works; legacy /settings/* paths still redirect
  via the LEGACY_PATH_REDIRECTS table from T3.

* feat(frontend/home): KPI sparklines + tightened card grid (issue #340)

Issue #340 T6. Reskins the 4 Home KPI tiles (Potential Monthly Savings,
Active Commitments, Current Coverage, YTD Savings) to match the action-
center mock language.

Changes:
- dashboard.ts:
  * Replaced the innerHTML template literal in renderDashboardSummary
    with DOM construction (createElement + textContent + appendChild) —
    aligns with the issue #340 plan's XSS constraint and removes the
    last interpolated-innerHTML site in this file's KPI rendering path.
  * Added sparklinePoints() pure helper that normalizes a numeric series
    into a 0..width × 0..height viewport for a <polyline points="..."> .
  * Added attachSparkline() that finds a .kpi-tile-spark[data-spark-key]
    placeholder and injects an SVG polyline via DOM methods. Skips
    silently when the placeholder is missing or < 2 values are passed
    (no broken visuals, no thrown errors).
  * Wired attachSparkline('ytd', cumulative_savings) into the existing
    loadSavingsAnalytics() success path so the YTD Savings tile draws
    a sparkline from the same data the main "Savings over time" chart
    already renders. The other three tiles ship empty SVG placeholders;
    sparklines for them are deferred per #340 (no current trend endpoint
    for them).
- styles/components.css: new .kpi-tile rule extending .card with a
  4-area grid (title / value / detail / spark) that places the sparkline
  to the right of value+detail. Typography + spacing pulled from the T1
  design tokens (--cudly-fs-2xl for value, --cudly-text-muted for label,
  --cudly-success for the savings sparkline).
- __tests__/dashboard.test.ts: 6 new tests covering sparklinePoints
  (normalization, < 2 values short-circuit, flat series no-NaN) and
  attachSparkline (polyline draws into svg, missing-placeholder no-op,
  insufficient-values silent skip).

The legacy .card styling still applies because .kpi-tile is additive.
Layout for the 4-tile row is unchanged (existing #summary grid handles
horizontal flow). On an empty DB the sparklines simply don't render.

Verified:
- npm test: 1602/1602 pass (1596 + 6 new sparkline tests).
- npm run build: clean.
- Manual browser smoke: Home renders with the new KPI tile styling;
  empty-DB shows tiles without sparklines (no broken SVGs); existing
  "Savings over time" + "Potential Savings by Service" cards unchanged.

* style(frontend): visual polish across Opportunities + Plans + action-box (issue #340)

Issue #340 T8 + remaining-section polish. Picks up the design tokens
introduced in T1 across the surfaces that the per-section tasks
(T8 Opportunities, T9 Plans + Purchases) would have touched.

Changes:
- styles/components.css:
  * .recommendations-action-box (the sticky-bottom Purchase / Create
    Plan bar on Opportunities) migrates color literals + paddings +
    border-radius to design tokens. The mock's right-side "Plan builder"
    drawer is a bigger refactor of recommendations.ts; tracked as a
    deferred sub-task in #340. This commit keeps the sticky-bottom
    layout (zero JS change) but aligns its visual family with the rest
    of the reskin.
  * NEW .context-drawer rule — class slot reserved for the future
    right-side drawer. Today nothing uses it; the rule lives here so
    when recommendations.ts ships the drawer, the design-token mapping
    is already in one place.
- styles/plans.css: hardcoded colors → design tokens. .plan-card,
  .plan-header, .upcoming-card, .upcoming-date, .upcoming-savings,
  .ramp-option (selected state), and the custom-ramp-config background
  all now reference --cudly-surface, --cudly-border, --cudly-primary,
  --cudly-success, --cudly-info-bg, --cudly-shadow-sm,
  --cudly-surface-muted, and the radius tokens. No structural change.

Deferred per #340 sub-tasks:
- Active commitments + Coverage donuts (T7) — no per-commitment list /
  per-provider coverage endpoint exposed today. Inventory tab keeps
  the empty-state placeholders from T4.
- "Plan builder" right-side drawer (T8 full pattern) — requires
  recommendations.ts restructure. CSS hooks are in place.

Verified:
- npm test: 1602/1602 pass (no behavioural change).
- npm run build: clean.
- Manual browser smoke: Plans / Purchases / Opportunities all render;
  styling is consistent with Home; no regression in flow.

* style(frontend): migrate settings/tables/forms/modals CSS to design tokens (issue #340)

Closes the per-CSS-file design-token gap left after T6/T8: 4 stylesheets
that still carried hardcoded color literals are migrated to consume the
:root tokens introduced in T1. No behavioural change, no visual
regression — every replaced literal maps to a token whose value is the
same hex (or close enough that the diff is imperceptible).

Migrated colors:
- #1a73e8 → var(--cudly-primary)
- #666 / #888 → var(--cudly-text-muted)
- #333 → var(--cudly-text)
- #e0e0e0 / #eee → var(--cudly-border)
- #ddd → var(--cudly-border-strong)
- #f8f9fa → var(--cudly-surface-muted)
- #f5f5f5 → var(--cudly-bg)
- #e8f0fe → var(--cudly-info-bg)
- #34a853 → var(--cudly-success)
- #ea4335 → var(--cudly-error)
- white → var(--cudly-surface)
- 8px border-radius → var(--cudly-r-md)

Per-file impact:
- styles/settings.css (76 literals → most migrated; some niche colors
  like #f4a261, #137333, #fffbf5 left as literals since they don't have
  a token equivalent and would have required new tokens for the visual
  weight to come out right)
- styles/tables.css (27 literals → migrated common palette + table
  background/radius)
- styles/forms.css (31 literals → migrated common palette including the
  focus-ring #e8f0fe → info-bg)
- styles/modals.css (20 literals → migrated common palette)

Remaining literals (charts.css 4 + responsive.css 0 + niche ones above)
are out of scope for this PR — leaving them ensures the diff stays
reviewable. They're tracked under #340's "Constraints not fully honored"
follow-up notes.

Verified:
- npm test: 1602/1602 pass (no behavioural change).
- npm test -- --coverage: 80.7% stmts / 68.09% branches /
  70.77% funcs / 82.38% lines — parity with the 80.65/68.22/70.65/82.31
  baseline (flat-to-better; branches -0.13% is within noise).
- npm run build: clean.
- Manual browser smoke: Admin / Plans / Purchases / forms / modals
  all render identically to before the migration.

* style(frontend): polish topbar logo + empty-state cards (issue #340)

Two visual fixes spotted during browser review of PR #343:

1. **Topbar logo unreadable on blue gradient.** The previous
   `<img src="/favicon.svg">` rendered at 24px as a tiny blue "C" letter
   on the blue topbar gradient — low contrast, hard to parse. Replaced
   with an inline cloud-shape SVG that inherits `currentColor` (white
   from `--cudly-primary-fg`) so the mark is crisp on the gradient.
   `favicon.svg` is unchanged — still serves as the browser-tab favicon.

2. **Empty-state cards looked like raw text.** `.empty-state` used
   `background: #f8f9fa` which is essentially the same as the new page
   bg (`--cudly-bg: #f5f7fa`) — they blended together. Inventory's
   "Active commitments" + "Coverage" placeholder sections read as
   un-contained text floating on the page.

   Migrated `.empty-state` to:
   - `background: var(--cudly-surface)` (white card)
   - `border: 1px solid var(--cudly-border)` + `--cudly-shadow-sm`
   - design-token spacing + radius

   Also added `.empty-state h3` + `.empty-state p` rules so the heading
   reads as a proper card title (not the `.card h3` muted-label
   treatment) and the body wraps at a comfortable max-width.

Verified:
- npm test: 1602/1602 pass (no behavioural change).
- npm run build: clean.
- Manual browser smoke: cloud logo reads on topbar; Inventory's
  Active commitments + Coverage placeholders now look like proper
  cards; no regression on populated cards.

* refactor(frontend/admin): remove in-panel sticky-rail sub-nav (issue #340)

The sticky in-panel rail rendered by settings-subnav.ts (Global Defaults /
AWS / Azure / GCP / Exchange Automation on Purchasing; Federation Setup /
Registrations / per-cloud accounts on Accounts & onboarding; Users /
Groups / Permission Overview / API Keys on Users, roles & API keys)
collided with the new action-center left sidebar after issue #340.

Why it broke: the rail's wide-viewport CSS used
  position: sticky; float: left; margin-left: -220px
to park itself in the page's left gutter. Pre-#340 the left gutter was
empty (main had `max-width: 1600px; margin: 0 auto`). Post-#340 the
left side is occupied by the new `.app-sidebar`, so the rail's negative
margin pulled it visually on top of the primary nav items — users saw
their Home / Opportunities / Plans / Purchases / Inventory items
disappear and the rail items replace them.

Per user request, deleted the rail entirely rather than re-positioning it
inside the panel. The sub-sections it linked to (`#purchasing-global-defaults`,
`#aws-settings`, etc.) all still render as section headings within the
form itself, so users can still see the structure — they just don't get
a separate side-rail to jump between them. Long forms are short enough
that scrolling is fine; we can revisit a non-overlapping in-panel
table-of-contents pattern if needed.

Removed:
- settings-subnav.ts: `SUBTAB_ITEMS`, `renderSubNav`, and the
  IntersectionObserver scrollspy. `reflectDirtyState` survives — the
  "unsaved changes" save-bar + has-unsaved dot on the Admin tab button
  still flow through it from settings.ts.
- navigation.ts: dropped the `renderSubNav` import + the call from
  `switchSettingsSubTab`.
- styles/components.css: dropped `.settings-layout`, `.settings-subnav`,
  `.settings-layout-content` and their hover/active/sticky rules
  (~80 lines of CSS).
- __tests__/settings-subnav.test.ts: dropped the `describe('renderSubNav')`
  block + its DOM-builder helpers + the FakeIntersectionObserver stub.
  Kept the reflectDirtyState describe block.

Verified:
- npm test: 1593/1593 pass (1602 - 9 removed renderSubNav tests).
- npm run build: clean.
- Manual browser smoke: Admin > Purchasing policies / Accounts &
  onboarding / Users, roles & API keys all render with the form fully
  visible, no overlap with the primary sidebar nav.

* style(frontend): align sidebar hamburger icon with the section icons (issue #340)

The hamburger toggle and the section nav buttons used different padding
schemes (8px all-around vs 8px×12px), and different SVG sizes (18×18 vs
20×20). Net effect: the hamburger sat a few pixels to the left of the
Home / Opportunities / Plans / Purchases / Inventory / Admin icons,
breaking the single-column glyph alignment expected of a left rail.

Changes:
- styles/layout.css: .app-sidebar-toggle padding mirrors the .tab-btn
  row exactly (`var(--cudly-sp-2) var(--cudly-sp-3)`), and the
  `margin-left: var(--cudly-sp-1)` is dropped so the toggle's hit area
  starts at the same x as every nav button. Border-radius bumped to
  --cudly-r-md to match.
- index.html: hamburger SVG resized to 20×20 + given the .sidebar-icon
  class so it inherits the same icon-rendering rules as the section
  icons below it.

Visually verified in browser: zoomed sidebar shows the hamburger glyph
+ 6 section icons stacked in a single vertical line — same x-center.

* fix(frontend/ui): drop .tabs class from sidebar nav — fixes icon alignment + stray border (issue #340)

The sidebar nav was previously `<nav class="tabs app-sidebar-nav">`. The
`.tabs` class came from the old top-tab strip, where it set
`padding: 0 2rem` + `border-bottom: 2px solid #e0e0e0`. After issue #340,
`.app-sidebar-nav` was supposed to neutralize those (padding: 0 +
border-bottom: none), but CSS import order put tabs.css *after*
layout.css — same specificity, last declaration wins — so the old rules
kept leaking through.

Net visual effect users reported:
- Section icons (Home / Opportunities / …) sat ~32px to the right of
  the hamburger icon. Cause: 2rem (32px) of left padding on the nav
  container pushed every section row inward while the hamburger sat
  flush against the sidebar's own inner padding.
- A thin grey hairline divided the hamburger from the section list.
  Cause: the inherited `border-bottom: 2px solid #e0e0e0` from `.tabs`.

Fix: just remove `class="tabs"` from the sidebar `<nav>`. The buttons
still carry their own `.tab-btn` class (used by navigation.ts's
`document.querySelectorAll('.tab-btn')`), so click wiring is unaffected.
The horizontal `.tabs` strip pattern is dead-code in the action-center
shell anyway.

Also updated html.test.ts: the regression test that asserted "nav
element with .tabs class exists" now checks for `.app-sidebar-nav`
instead — same intent, current selector.

Verified in browser: hamburger icon and all 6 section icons sit on the
same vertical x-line; no stray border below the hamburger.

- npm test: 1593/1593 pass.
- npm run build: clean.

* style(frontend): empty-state visual hint + keyboard focus rings (issue #340)

Closes two no-backend gaps spotted in the post-merge review of PR #343:

1. **`.empty` had no visual containment.** The inline empty messages
   used by `dashboard.ts`, `history.ts`, `plans.ts`, `recommendations.ts`,
   and `riexchange.ts` rendered as bare grey text on the parent card's
   white surface — they were easy to miss as "this slot is empty" vs
   "the card is still loading". Now `.empty` carries a subtle
   surface-muted background + radius + spacing, so it reads as a
   distinct empty slot without competing with the card chrome around
   it. The heavier `.empty-state` card pattern (the one introduced in
   the post-T6 polish commit) still applies for whole-page placeholders
   like Inventory's Active commitments / Coverage sub-sections.

2. **No visible keyboard focus on sidebar or sub-tab buttons.** Chrome's
   default `outline: auto` doesn't render reliably on rounded buttons,
   and the existing `.tab-btn`/`.sub-tab-btn` rules didn't supply a
   replacement. Keyboard users couldn't tell which item the focus was
   on. Added explicit `:focus-visible` outlines for:
   - `.app-sidebar-nav .tab-btn`
   - `.app-sidebar-toggle`
   - `.sub-tab-btn` (covers both Admin sub-tabs and Inventory sub-nav)

   `:focus-visible` rather than `:focus` so mouse-clickers don't see
   the ring; keyboard tabbing surfaces it cleanly.

Verified:
- npm test: 1593/1593 pass (no behavioural change).
- npm run build: clean.
- Manual browser smoke: Tab key through sidebar + sub-tabs shows
  blue ring; empty messages on Opportunities, Plans, Purchases
  render with subtle muted-background containment.

* style(frontend): chip-style filters + page-hero CSS scaffolding (issue #340)

Visual alignment with the mocks for the filter-bar pattern and section
heroes. CSS-only — no JS, no HTML restructure, no risk to existing
filter wiring.

Changes:
- styles/forms.css:
  * `.filter-group select` + `.filter-group input` adopt a pill shape:
    `--cudly-r-full` border-radius, design-token padding, hover +
    `:focus-visible` (3px info-bg ring) states. Native <select> retains
    its dropdown behaviour — only the trigger is restyled, so click +
    keyboard + screen-reader semantics are unchanged.
  * `.controls-bar` drops its `background: white` card-chrome +
    box-shadow. Chips now float on the page bg, matching the mock's
    "filter strip" treatment rather than "filter inside a panel".
- styles/base.css:
  * NEW `.page-hero` rule set for prominent section titles
    (`--cudly-fs-2xl` h1/h2 + `--cudly-text-muted` description, with
    bottom spacing). Class slot only — opt-in for the sections that
    benefit; pre-existing card-internal h2s stay unchanged so this
    commit lands without rewriting every section's heading structure.

Verified:
- npm run build: clean (no bundle size change beyond CSS deltas).
- Manual browser smoke: provider/account filters on Home / Plans /
  Purchases render as compact rounded pills; dropdowns still expand
  natively on click; controls bar no longer competes with the KPI
  tiles below it.

* style(frontend): page-hero typography + a11y + reduced-motion + hover lift (issue #340)

Final batch of low-risk frontend-only polish before merge:

- **Page hero applied** to three top-of-section headers:
  - `#plans-header` ("Purchase Plans")
  - `#inventory-ri-exchange` first card ("Convertible RI Exchange")
  - `#settings-section` ("Global Configuration")
  All three pick up the `.page-hero` h2 size bump (`--cudly-fs-2xl`)
  defined in the previous commit. Other sections opt-in later as
  needed.

- **Skip-to-main-content link** added at the very top of `<body>`,
  styled to be screen-reader-only until tabbed onto. First Tab from a
  fresh page now lands on the link; Enter jumps past the topbar +
  sidebar to `#main-content`. Standard a11y pattern.

- **`prefers-reduced-motion` honored** — global `@media` rule collapses
  all transitions/animations to 0.01ms when the OS-level pref is set.
  Animation-end events still fire (some JS depends on them); the
  visual motion is gone for users who asked for it.

- **KPI tile hover lift** — `.kpi-tile` gets a 1px translateY +
  shadow-md on hover. Subtle, transform-only (no layout shift), and
  fully overridden by `prefers-reduced-motion`.

- **charts.css token migration** — `.chart-section h3`, `.stat-card`,
  `.stat-card h4`, `.stat-value` all now consume design tokens
  (`--cudly-text`, `--cudly-surface-muted`, `--cudly-primary`,
  --cudly-fs-* / --cudly-sp-*). Last per-file token migration
  pending was charts.css; it's now done.

Verified:
- npm test: 1593/1593 pass.
- npm run build: clean.
- Manual browser smoke: Plans / Inventory > RI Exchange / Admin >
  General all show bigger headings; KPI tiles lift slightly on
  hover; tabbing from the URL bar reveals the skip-link.

* style(frontend): tokenize topbar + plan/upcoming card hover lifts (issue #340)

More low-risk polish ahead of merge:

- **Topbar gradient → tokens.** The `header` gradient + h1 weight +
  paddings + the `#logout-btn` and `.header-link` buttons all now
  consume design tokens. The gradient steps from `--cudly-primary`
  (#2563eb) to `--cudly-primary-hover` (#1d4ed8) — slightly more
  saturated than the legacy `#1a73e8 → #0d47a1` Material gradient,
  matching the rest of the reskin's blue family.

- **Plan-card + upcoming-card hover lift.** Mirroring the KPI tile
  pattern from the previous commit: `translateY(-1px)` + bump to
  `--cudly-shadow-md` on hover. Transform-only, so layout doesn't
  shift; `prefers-reduced-motion` users skip the lift via the global
  rule.

- **`.upcoming-card` token migration.** Hardcoded white bg, 8px radius,
  rgba shadow, `#1a73e8` accent border-left all → tokens.

Net visible effect: clicking through Plans, you now feel the cards
respond to hover (subtle 1px lift) the same way KPI tiles do — the
"interactive cardness" reads consistently across sections.

Verified:
- npm test: 1593/1593 pass.
- npm run build: clean.
- Manual browser smoke: hover on plan cards + KPI tiles lifts both
  the same way; topbar gradient renders cleanly; Logout + API Docs
  + Feedback links still hover-highlight correctly.

* style(frontend): components.css major token sweep (issue #340)

Migrates the high-frequency hex literals in components.css to design
tokens. Before: 234 hex literals; this commit kills the 12 most-used:

  #1a73e8  → var(--cudly-primary)         (30 occurrences)
  #666     → var(--cudly-text-muted)      (16)
  #333     → var(--cudly-text)            (13)
  #e8f0fe  → var(--cudly-info-bg)         (9)
  #d0d7de  → var(--cudly-border-strong)   (8)
  #c5221f  → var(--cudly-error-fg)        (8)
  #888     → var(--cudly-text-muted)      (6)
  #f5f5f5  → var(--cudly-bg)              (6)
  #ea4335  → var(--cudly-error)           (6)
  #e6f4ea  → var(--cudly-success-bg)      (5)
  #e0e0e0  → var(--cudly-border)          (5)
  #1557b0  → var(--cudly-primary-hover)   (5)

~117 substitutions total. Visual result is identical (token values
match the literals 1:1), but every future theme tweak now flows from
the :root tokens rather than the literal sprinkled across the file.

Remaining literals in components.css (#555, #fff, #b06000, niche
saturation accents) are intentionally left in this pass — they're
either special-purpose (highlight ribbons, warning fg) or didn't have
a clean token equivalent without forcing semantics they don't carry.

Verified:
- npm test: 1593/1593 pass.
- npm run build: clean.
- Manual browser smoke: Opportunities / Plans / Inventory / Admin
  all render identically; status badges / action-box / drawer hooks
  / dropdowns all preserved.

* style(frontend): tokenize remaining hex literals across CSS bundle (issue #340)

Second design-token migration sweep — picks up the literals left over
from earlier per-file passes. Net effect: ~80% of the CSS bundle now
consumes `:root` tokens directly; remaining literals are niche
saturation accents (mock-specific orange / dark green / pastel
warnings that don't have a clean token equivalent yet).

Files touched:
- tabs.css        — #1a73e8/#666/#e0e0e0/#f5f5f5 → tokens (4 colours)
- layout.css      — #1a73e8/#e0e0e0/#f8f9fa → tokens (3 colours, the
                    footer + a11y skip-link section)
- components.css  — second pass: #555 (text-muted), #fff (surface),
                    #fce8e6 (error-bg), #eee (border), #ddd
                    (border-strong), #34a853 (success) (~30 more
                    substitutions on top of the previous +117)
- settings.css    — #f0f0f0, #ddd, #555, #e8f0fe, #2e6bd9, #fff → tokens
- plans.css       — #333, #666, #ddd, #eee, #f8f9fa → tokens
- forms.css       — #34a853, #eee, #e8f5e9 → tokens
- tables.css      — #1a73e8, #555, #666, #888, #eee, #fbbc04 → tokens

Tests:
- __tests__/css.test.ts: the "Color Scheme" describe block previously
  asserted literal hex values existed in the bundled CSS (#1a73e8,
  #34a853, etc.). After the migration those literals are gone from
  consumer CSS but their token definitions live in :root. Updated the
  assertions to verify the token DEFINITIONS exist, matching the new
  source-of-truth.

Verified:
- npm test: 1593/1593 pass (updated css.test.ts assertions).
- npm run build: clean.
- Manual browser smoke: every section renders identically; status
  pills / cards / filters / charts / modals all preserved.

* style(frontend): base.css final token sweep + page-hero on Admin sub-tabs + smooth scroll (issue #340)

Third (and likely final) sweep of UI-only polish:

- **base.css full token migration**:
  * `.text-muted`, `.error`, `.empty`, `.empty-state`, `.help-text`,
    `.error-message`, `.success-message`, `.warning-message` all
    replace their hardcoded #34a853 / #ea4335 / #c5221f / #fce8e6 /
    #e8f5e9 / #fff3e0 / #2e7d32 / #e65100 / #4caf50 / #fbbc04 / #999 /
    #666 with their `--cudly-*` tokens.
  * NEW token `--cudly-success-fg: #2e7d32` added to `:root` for the
    darker success-text accent (mirrors the existing `--cudly-warn-fg`
    + `--cudly-error-fg` triad).
  * body bg now reads `var(--cudly-bg)` instead of literal `#f5f5f5`,
    and body text default consumes `--cudly-text`.

- **html { scroll-behavior: smooth }** added globally so anchor jumps
  (skip-link, any future in-page anchors) animate cleanly. The
  pre-existing `prefers-reduced-motion` rule already overrides this
  to `auto` for users who opt out, so no a11y regression.

- **`.page-hero` applied to four more sub-section heads**:
  * `#purchasing-panel` ("Purchasing Settings")
  * `#accounts-section` ("Accounts")
  * `#users-section` ("User Management")
  * `#purchase-history-section` ("Purchase History")
  All four pick up the larger 28px bold treatment when their sub-tab
  is active — consistent visual hierarchy across Admin sub-tabs +
  Purchases.

- **__tests__/css.test.ts**: the `.savings` / `.error` color
  assertions previously matched literal hex; now match either the
  `var(--cudly-*)` form or the literal so the tests are tolerant of
  future token-or-literal moves either way.

Verified:
- npm test: 1593/1593 pass.
- npm run build: clean.
- Manual browser smoke: every section visually unchanged; Admin sub-
  tab headings + Purchases history heading now read at hero size.

* style(frontend): fourth token sweep — niche bg/border literals (issue #340)

Final small-batch migrations on components / settings / modals:

- components.css: `#f5f7fa` → `var(--cudly-bg)`, `#fbbc04` →
  `var(--cudly-warn)`, `#f0f0f0` → `var(--cudly-border)`.
- settings.css: `#e6f4ea` → `var(--cudly-success-bg)`,
  `#ccc` → `var(--cudly-border-strong)`, `#fafafa`/`#f9f9f9` →
  `var(--cudly-surface-muted)`.
- modals.css: `#555` → `var(--cudly-text-muted)`,
  `#eee` → `var(--cudly-border)`.

Remaining hex literals across the bundle are all niche saturation
shades that don't have a clean 1:1 token equivalent (the orange/amber
provider badges `#f4a261`/`#f9a825`/`#856404`, the green utilization
indicators `#137333`/`#1e8e3e`/`#2d9048`, the dark gray accents
`#222`/`#444`/`#bbb`, status-bg variants on tables `#cce5ff`/`#d4edda`/
`#f8d7da`/`#fef3cd`). Adding tokens for each would force semantics
into the design system that aren't shared elsewhere — leaving them
as literals keeps the token vocabulary small + meaningful.

Verified:
- npm test: 1593/1593 pass.
- npm run build: clean.
- Manual browser smoke: no visual changes (token values match literals).

* fix(frontend): apply chip styling to ALL filter dropdowns consistently (issue #340)

User noticed dropdowns were inconsistent — Plans/Home/Opportunities
provider+account selects rendered as pills, but Purchases' Period +
date-range filters stayed square. Cause: previous chip rule only
matched `.filter-group select`, but Purchases' filters live in
`.controls-bar` / `.date-range-picker` / `#history-controls`
containers that don't wrap their children in `.filter-group`.

Extended the chip selector to include those parent containers:
  .filter-group select / input
  .controls-bar > label > select / input
  .controls-bar > select / input
  .date-range-picker select / input[type="date"|"text"]
  #history-controls select / input[type="date"]

Form selects inside `<form>` or `<fieldset>` (Admin settings, modals)
are intentionally NOT matched — verified via computed-style probe:
- Filter `<select>`s render `border-radius: 9999px` ✅
- Admin form `<select>`s keep `border-radius: 4px` ✅

So filters everywhere now read as chips; form inputs stay form-input
shape. Zero JS change.

Verified:
- npm test: 1593/1593 pass.
- npm run build: clean.
- Manual browser smoke + DOM probe: all 5 Purchases filter widgets
  (Period · From · To · Provider · Account) render as pills;
  Admin > General form selects unchanged.

* style(frontend): toast color tokens + meta description + aria-orientation (issue #340)

Last batch of UI-only polish:

- **Toast palette tokenized**: `.toast--success` + `.toast--warning`
  border-left and icon colors now read from `var(--cudly-success-fg)`
  + `var(--cudly-warn-fg)` instead of literal `#1e8e3e` / `#b06000`.
  Slight visual shift (success goes from #1e8e3e to #2e7d32; warn
  from #b06000 to #e65100) — both stay in the same green/amber
  families and match the rest of the success/warning treatment
  throughout the app.

- **`<meta name="description">`** added so the page renders a useful
  preview when shared / bookmarked / indexed.

- **`<meta name="theme-color" content="#2563eb">`** so mobile browsers
  tint the address bar to match the topbar gradient.

- **`aria-orientation="vertical"`** on the sidebar `<nav role="tablist">`.
  Tells screen readers + arrow-key handlers that this tablist navigates
  with Up/Down instead of Left/Right — matches its visual orientation.

Verified:
- npm test: 1593/1593 pass.
- npm run build: clean.

* style(frontend): forms.css final token migrations (issue #340)

Last tokens left in forms.css:

- `.slider` (the toggle switch background) `#ccc` → `--cudly-border-strong`.
- Password strength `.requirement.met`/`.unmet` colors:
  `#6c757d` → `--cudly-text-muted`
  `#adb5bd` → `--cudly-text-subtle`
  `#2e7d32` → `--cudly-success-fg`
  `#4caf50` → `--cudly-success`

forms.css now has zero non-niche hex literals; the few remaining are
intentional special-case shades that don't map onto the design system's
semantic palette.

Verified:
- npm test: 1593/1593 pass.
- npm run build: clean.

* fix(frontend): address CodeRabbit findings from PR #343 review

Security:
- dashboard.ts: replace innerHTML upcoming-purchases renderer with DOM
  API construction; whitelist p.provider before adding to classList to
  prevent attribute-injection via server-controlled string (CR major)

Bugs:
- navigation.ts: scope sub-tab-btn selector to #admin-tab to prevent
  cross-tab active-state clobbering when other sub-navs are present (CR minor)
- navigation.ts: applyTabFromPath now calls history.replaceState when a
  legacy path is matched, canonicalising the address bar as the code
  comment promised (CR major)
- inventory.ts: wireSubNavListeners guards listenersWired=true behind
  a buttons.length > 0 check so the function can re-run after the DOM
  is ready (CR nitpick)
- dashboard.ts: spark SVG placeholders start with class="hidden"; removed
  by attachSparkline when real data arrives, avoiding empty SVGs in tiles
  that have no sparkline data yet (CR nitpick)

CSS:
- components.css: promote .kpi-tile h3 selector to .card.kpi-tile h3 to
  out-specificity the later .card h3 block and preserve KPI typography (CR minor)

Accessibility:
- index.html: add aria-label to all 6 sidebar tab buttons so assistive
  tech retains a button name when .sidebar-label is hidden in collapsed
  sidebar state (CR major)

Tests:
- html.test.ts: add inventory tab button assertion — was the only tab
  not covered by an explicit data-tab check (CR nitpick)
- navigation.test.ts: add /admin, /admin/accounts, /admin/purchasing
  test cases for getSettingsSubTabFromPath; retain legacy /settings/*
  cases to guard backward compat (CR nitpick)

All 1597 tests pass; build clean.

* fix: apply CodeRabbit auto-fixes

Fixed 3 file(s) based on 3 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>

* fix(frontend): address CodeRabbit cycle-2 findings for PR #343

a11y/ARIA:
- index.html: complete ARIA tab wiring for Inventory sub-tabs — add id
  attrs to the 3 sub-tab buttons; add role="tabpanel" + aria-labelledby
  to the 3 sub-panel sections (active-commitments, coverage, ri-exchange)
- index.html: wire Admin sub-tabs — add id + aria-controls to the 4
  sub-tab buttons; add role="tabpanel" + aria-labelledby to settings-
  section, purchasing-panel, accounts-section, users-section, apikeys-
  section so screen-readers can navigate the tab pattern correctly

CSS:
- components.css + settings.css: fix 7 broken token-migration artifacts
  where the hex suffix got appended directly to var(--cudly-surface)
  yielding invalid CSS (var(--cudly-surface)bf5 / 3cd / 3e0). Corrected
  to var(--cudly-warn-bg) which is the closest design-token equivalent
  and the correct semantic colour for dirty-field highlights, unsaved-
  changes badge, provider-disabled banner, status-badge.paused / .running,
  and confidence-medium. 7 locations fixed across both files.

Tests:
- settings-subnav.test.ts: add regression test that .settings-buttons
  nested inside #ri-exchange-automation-settings are NOT promoted to the
  sticky save-bar, protecting the RI Exchange exemption in reflectDirtyState

All 1598 tests pass; build clean.

* test(frontend/ui-revamp): add inventory tab fixture + sparkline hide-on-empty guard

- navigation.test.ts: add `<button data-tab="inventory">` and
  `#inventory-tab` to the beforeEach DOM fixture; add switchTab
  assertion that inventory button and panel toggle active correctly
  (addresses CR cycle-3 nitpick on lines 43-54)
- dashboard.ts: attachSparkline now hides the SVG element when
  values.length < 2 or sparklinePoints() returns null, preventing a
  stale/empty polyline from showing after data is cleared; also calls
  attachSparkline('ytd', []) in the no-data early-return path so any
  previously-rendered sparkline is cleared

* fix(frontend/dashboard): clear YTD sparkline in analytics error path

The catch block around loadSavingsTrend now calls attachSparkline('ytd', [])
after destroying savingsTrendChart, so a previously-rendered sparkline is
cleared when the analytics endpoint returns an error (503 or otherwise).
The no-data early-return path already received the same treatment in the
previous commit; this makes both branches consistent.

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/xs Trivial / one-liner impact/internal Team-internal only priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/chore Maintenance / non-user-visible urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant