test(accounts): regression guards for discover-org admin scope + 5xx-on-cred-failure (post-#212 CR)#222
Conversation
…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).
|
@coderabbitai review |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdded regression tests for ChangesTest Additions for discoverOrgAccounts Handler
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Review rate limit: 4/5 reviews remaining, refill in 12 minutes. Comment |
✅ Actions performedReview triggered.
|
…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).
…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>
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:
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_RejectsNonAdminNon-admin session must hit a 403 ClientError. Without this guard, a refactor that swaps
requireAdminback torequirePermission("create","accounts")silently re-opens org discovery to non-admin users — and discovered rows go straight intocloud_accounts, which is a privilege-escalation surface (not just a UX preference).TestDiscoverOrgAccounts_CredResolutionFailureIs5xxCredential-resolution failures must surface as 5xx (retryable), not 4xx. Wraps a
CredentialStorestub (errCredStore) whoseLoadRawreturns a transient-style error; asserts the handler returns a non-ClientErrorcarrying the"resolve credentials"stage marker so operators can find it in logs. Without this, a future refactor that re-appliesNewClientError(400, ...)on resolver errors would silently make transient store/network failures non-retryable.The test's
discoverOrgFninjection is at.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:
AWSAuthMode = ""will fail DB CHECK constraintdisco.Accountsaws_auth_mode=bastioncomments#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}→ SQLNULL. Thecloud_accounts.aws_auth_modecolumn allowsNULL(noNOT NULLconstraint inmigrations/000011_cloud_accounts.up.sql:18-19), and PostgresCHECK (col IN (...))constraints satisfyNULLby default (the constraint is vacuously true onNULL). So persistingmember.AWSAuthMode = ""is correct as-is. I'll reply on the CR thread with this rationale.Verification
go build ./...cleango test ./internal/api/...— full package green (incl. the two new cases + all PR feat(accounts): wire AWS Organizations discovery endpoint (closes #208) #212 cases)Triage
type/chore,severity/medium,urgency/this-sprint,impact/internal,effort/xs,priority/p2,triaged. Pure-test PR; no behaviour change.Summary by CodeRabbit