feat(frontend): UI revamp — IA restructure + action-center reskin (closes #340)#343
feat(frontend): UI revamp — IA restructure + action-center reskin (closes #340)#343cristim wants to merge 29 commits into
Conversation
…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.
…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.
…w 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.
…ection (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.
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.
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.
…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.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughRestructures the frontend IA to Home, Opportunities, Plans, Purchases, Inventory & Coverage, and Admin; replaces top tabs with an app-shell + left sidebar; adds :root design tokens and tokenized component styles; implements Inventory sub-tabs; refactors dashboard KPIs to DOM-rendered tiles with sparkline helpers; and updates routing and tests. ChangesUI Revamp: IA Restructuring + Visual Reskin UI Revamp: IA Restructuring + Visual Reskin
Estimated code review effort 🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
@coderabbitai review This PR ships a UI revamp (issue #340) restructuring the IA from 6 top tabs to a sidebar-driven action-center pattern + design-token reskin. Frontend-only, 7 atomic commits, 1602/1602 tests pass, bundle within +0.8% of baseline. Specifically interested in feedback on:
|
🧩 Analysis chainLet me pull the relevant source files to give you targeted feedback on all four areas. 🏁 Script executed: Length of output: 141 🏁 Script executed: Length of output: 44883
1.
|
| Area | Status | Action |
|---|---|---|
LEGACY_PATH_REDIRECTS coverage |
✅ / |
Verify /purchase-plans slug; add if needed |
Legacy redirect uses pushState not replaceState |
🐛 | Use replaceState in applyTabFromPath when a legacy segment is hit |
| Sparklines — design choice | ✅ | Correct, keep |
| 3 KPI tiles have orphaned empty SVG placeholders | Hide until data available | |
Math.min/max spread in sparklinePoints |
Use reduce |
|
data-inv-subtab vs extracting renderSubNav |
✅ | Separation is correct |
switchSettingsSubTab global .sub-tab-btn query clobbers inventory active state |
🐛 | Scope to #admin-tab .sub-tab-btn |
renderDashboardSummary XSS posture |
✅ | Clean |
renderUpcomingPurchases unescaped provider, execution_id, etc. |
🐛 | Apply escapeHtml to all interpolated API fields, or migrate to DOM API |
✅ 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.
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
frontend/src/navigation.ts (1)
164-168:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winScope admin sub-tab activation to the admin container.
The global
.sub-tab-btnquery can clear active state on unrelated sub-navs. Scope this to#admin-tab .sub-tab-btn(or equivalent admin root) to avoid cross-tab clobbering.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/navigation.ts` around lines 164 - 168, The current querySelectorAll('.sub-tab-btn') is global and will toggle active state across all sub-navs; restrict it to the admin container by querying only admin-scoped buttons (e.g., use '#admin-tab .sub-tab-btn' or the specific admin root element) so the loop that reads btn.dataset['settingsTab'] === subTab and updates classList.toggle('active', ...) and aria-selected only affects admin sub-tabs; locate the block that uses document.querySelectorAll<HTMLButtonElement>('.sub-tab-btn') and change the selector to the admin-scoped selector or query the admin root and call querySelectorAll on it instead.frontend/src/dashboard.ts (1)
362-383:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUnescaped attribute interpolation in
innerHTMLstill allows injection.
provideris interpolated into a class name andexecution_idintodata-idin aninnerHTMLtemplate. Please migrate this block to DOM construction (createElement,classList,dataset,textContent) to close attribute-injection risk.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/dashboard.ts` around lines 362 - 383, The current container.innerHTML = purchases.map(...) template injects unescaped values into attributes (class and data-id); replace this whole innerHTML construction in the block using DOM APIs: iterate purchases, call createElement for the card and its children (matching the structure built currently), use classList.add() for classes (validate/whitelist p.provider before adding to avoid injection), set element.dataset.executionId = String(p.execution_id) and dataset.action for buttons, and assign textual content with textContent (or use escapeHtml for any HTML you intend to allow) for plan_name, service, dateParts and formatCurrency output; keep getDateParts, escapeHtml and formatCurrency usage for values but stop using innerHTML and build the nodes programmatically so attributes are never interpolated into raw HTML.
🧹 Nitpick comments (3)
frontend/src/dashboard.ts (1)
211-214: ⚡ Quick winOnly render spark placeholders for tiles that actually receive data.
All KPI cards currently get an SVG placeholder, but only
ytdis populated. This leaves dead UI placeholders. Consider rendering spark SVG conditionally (or hiding by default until populated).Also applies to: 575-577
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/dashboard.ts` around lines 211 - 214, Only create or show the SVG spark placeholder for KPI tiles that actually have spark data: instead of unconditionally creating the element via spark = document.createElementNS(...) and adding class 'kpi-tile-spark' and dataset['sparkKey'] = t.kpi, gate that code on the presence of the tile's spark data (e.g., check a sparkData map or t.kpi === 'ytd' or similar) and only append the spark to the DOM when populated; alternatively, if you prefer to always create the element, add a default hidden state (e.g., add a 'hidden' class or set aria-hidden and CSS display:none) and ensure whatever populates the spark removes the hidden state when data is available, using the same identifiers spark, 'kpi-tile-spark', dataset['sparkKey'], and t.kpi to locate and toggle visibility.frontend/src/__tests__/navigation.test.ts (1)
44-55: ⚡ Quick winAdd canonical
/admin/*path coverage for sub-tab parsing.These updates move nav semantics to
admin, butgetSettingsSubTabFromPathcoverage is still centered on/settings/*. Please add/admin,/admin/accounts, and/admin/purchasingcases (and keep one/settings/*legacy case) so the new canonical routing is locked by tests.Proposed test additions
describe('getSettingsSubTabFromPath', () => { + test('returns general for root admin path', () => { + delete (window as unknown as Record<string, unknown>).location; + (window as unknown as Record<string, unknown>).location = { pathname: '/admin' } as Location; + expect(getSettingsSubTabFromPath()).toBe('general'); + }); + + test('returns accounts for /admin/accounts', () => { + delete (window as unknown as Record<string, unknown>).location; + (window as unknown as Record<string, unknown>).location = { pathname: '/admin/accounts' } as Location; + expect(getSettingsSubTabFromPath()).toBe('accounts'); + }); + + test('returns purchasing for /admin/purchasing', () => { + delete (window as unknown as Record<string, unknown>).location; + (window as unknown as Record<string, unknown>).location = { pathname: '/admin/purchasing' } as Location; + expect(getSettingsSubTabFromPath()).toBe('purchasing'); + }); + test('returns general for root settings path', () => { delete (window as unknown as Record<string, unknown>).location; (window as unknown as Record<string, unknown>).location = { pathname: '/settings' } as Location; expect(getSettingsSubTabFromPath()).toBe('general'); });Also applies to: 162-190
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/__tests__/navigation.test.ts` around lines 44 - 55, Update the navigation tests to include canonical /admin routes for sub-tab parsing: add test cases calling getSettingsSubTabFromPath with "/admin", "/admin/accounts", and "/admin/purchasing" (expecting the appropriate sub-tab results) and retain one legacy "/settings" case (e.g., "/settings", "/settings/accounts" or similar) to ensure backward compatibility; locate these additions in frontend/src/__tests__/navigation.test.ts near the existing getSettingsSubTabFromPath test block so both old "/settings/*" and new "/admin/*" paths are covered.frontend/src/inventory.ts (1)
68-77: ⚡ Quick winMake listener wiring conditional on finding sub-nav buttons
At Line 69–77,
listenersWiredis settrueeven when no#inventory-tab .sub-tab-btnnodes exist. If initialization ever happens before that DOM is present, listeners never get attached later.Suggested patch
function wireSubNavListeners(): void { if (listenersWired) return; - document.querySelectorAll<HTMLButtonElement>('#inventory-tab .sub-tab-btn').forEach((btn) => { + const buttons = document.querySelectorAll<HTMLButtonElement>('#inventory-tab .sub-tab-btn'); + if (buttons.length === 0) return; + buttons.forEach((btn) => { btn.addEventListener('click', () => { const name = btn.dataset['invSubtab'] ?? DEFAULT_SUB_SECTION; switchInventorySubSection(name); }); }); listenersWired = true; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/inventory.ts` around lines 68 - 77, The function wireSubNavListeners sets listenersWired = true even when document.querySelectorAll('#inventory-tab .sub-tab-btn') returns no elements, preventing future wiring; modify wireSubNavListeners so it only sets listenersWired to true after it actually attaches at least one listener (i.e., check the NodeList length or count attached handlers) and leave listenersWired false when no buttons were found so the function can be re-run later to attach handlers for elements targeted by switchInventorySubSection.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/__tests__/html.test.ts`:
- Around line 93-116: Add a new unit test mirroring the existing tab assertions
to explicitly check for the inventory tab: create a test (e.g., test('has
inventory tab button', ...)) that queries
document.querySelector('[data-tab="inventory"]') and asserts it is truthy; place
it alongside the other tab tests so the suite validates the inventory tab button
presence and prevents silent nav regressions.
In `@frontend/src/index.html`:
- Around line 41-64: The sidebar tab buttons (elements with class "tab-btn" such
as home-tab-btn, opportunities-tab-btn, plans-tab-btn, purchases-tab-btn,
inventory-tab-btn, admin-tab-btn) lose their visible label when .sidebar-label
is hidden; add an explicit aria-label attribute on each button with the same
human-readable text as its .sidebar-label (e.g., "Home", "Opportunities",
"Plans", "Purchases", "Inventory & Coverage", "Admin") so assistive tech still
has a name while keeping existing role="tab" and aria-selected attributes.
In `@frontend/src/navigation.ts`:
- Around line 222-230: applyTabFromPath currently maps legacy segments via
LEGACY_PATH_REDIRECTS but doesn't canonicalize the browser URL; update
applyTabFromPath to call history.replaceState to rewrite the URL to the
canonical path when a legacy mapping is used. Specifically, after resolving
segment via LEGACY_PATH_REDIRECTS (the branch that returns
LEGACY_PATH_REDIRECTS[segment]), compute the canonical path (e.g., '/' + the
resolved tab), call history.replaceState(null, '', canonicalPath) before
returning the tab, and leave other branches unchanged.
In `@frontend/src/styles/components.css`:
- Around line 32-39: The `.kpi-tile h3` rules (margin-bottom, color, font-size,
font-weight, letter-spacing) are being overridden by the later `.card h3` rule
because they have equal specificity; fix this by either moving the `.kpi-tile
h3` block to appear after the `.card h3` block or increase selector specificity
so it wins (e.g., change selector to a more specific form like `.card.kpi-tile
h3` or `.kpi-tile.card h3`), and update the selector references in
components.css accordingly so the KPI typography always takes precedence.
---
Outside diff comments:
In `@frontend/src/dashboard.ts`:
- Around line 362-383: The current container.innerHTML = purchases.map(...)
template injects unescaped values into attributes (class and data-id); replace
this whole innerHTML construction in the block using DOM APIs: iterate
purchases, call createElement for the card and its children (matching the
structure built currently), use classList.add() for classes (validate/whitelist
p.provider before adding to avoid injection), set element.dataset.executionId =
String(p.execution_id) and dataset.action for buttons, and assign textual
content with textContent (or use escapeHtml for any HTML you intend to allow)
for plan_name, service, dateParts and formatCurrency output; keep getDateParts,
escapeHtml and formatCurrency usage for values but stop using innerHTML and
build the nodes programmatically so attributes are never interpolated into raw
HTML.
In `@frontend/src/navigation.ts`:
- Around line 164-168: The current querySelectorAll('.sub-tab-btn') is global
and will toggle active state across all sub-navs; restrict it to the admin
container by querying only admin-scoped buttons (e.g., use '#admin-tab
.sub-tab-btn' or the specific admin root element) so the loop that reads
btn.dataset['settingsTab'] === subTab and updates classList.toggle('active',
...) and aria-selected only affects admin sub-tabs; locate the block that uses
document.querySelectorAll<HTMLButtonElement>('.sub-tab-btn') and change the
selector to the admin-scoped selector or query the admin root and call
querySelectorAll on it instead.
---
Nitpick comments:
In `@frontend/src/__tests__/navigation.test.ts`:
- Around line 44-55: Update the navigation tests to include canonical /admin
routes for sub-tab parsing: add test cases calling getSettingsSubTabFromPath
with "/admin", "/admin/accounts", and "/admin/purchasing" (expecting the
appropriate sub-tab results) and retain one legacy "/settings" case (e.g.,
"/settings", "/settings/accounts" or similar) to ensure backward compatibility;
locate these additions in frontend/src/__tests__/navigation.test.ts near the
existing getSettingsSubTabFromPath test block so both old "/settings/*" and new
"/admin/*" paths are covered.
In `@frontend/src/dashboard.ts`:
- Around line 211-214: Only create or show the SVG spark placeholder for KPI
tiles that actually have spark data: instead of unconditionally creating the
element via spark = document.createElementNS(...) and adding class
'kpi-tile-spark' and dataset['sparkKey'] = t.kpi, gate that code on the presence
of the tile's spark data (e.g., check a sparkData map or t.kpi === 'ytd' or
similar) and only append the spark to the DOM when populated; alternatively, if
you prefer to always create the element, add a default hidden state (e.g., add a
'hidden' class or set aria-hidden and CSS display:none) and ensure whatever
populates the spark removes the hidden state when data is available, using the
same identifiers spark, 'kpi-tile-spark', dataset['sparkKey'], and t.kpi to
locate and toggle visibility.
In `@frontend/src/inventory.ts`:
- Around line 68-77: The function wireSubNavListeners sets listenersWired = true
even when document.querySelectorAll('#inventory-tab .sub-tab-btn') returns no
elements, preventing future wiring; modify wireSubNavListeners so it only sets
listenersWired to true after it actually attaches at least one listener (i.e.,
check the NodeList length or count attached handlers) and leave listenersWired
false when no buttons were found so the function can be re-run later to attach
handlers for elements targeted by switchInventorySubSection.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 639f8247-a074-4d72-8d15-901a9d8dab72
📒 Files selected for processing (20)
frontend/src/__tests__/dashboard.test.tsfrontend/src/__tests__/html.test.tsfrontend/src/__tests__/inventory.test.tsfrontend/src/__tests__/navigation.test.tsfrontend/src/__tests__/recommendations.test.tsfrontend/src/__tests__/settings-subnav.test.tsfrontend/src/app.tsfrontend/src/dashboard.tsfrontend/src/index.htmlfrontend/src/index.tsfrontend/src/inventory.tsfrontend/src/navigation.tsfrontend/src/purchases-deeplink.tsfrontend/src/recommendations.tsfrontend/src/settings-subnav.tsfrontend/src/styles/base.cssfrontend/src/styles/components.cssfrontend/src/styles/layout.cssfrontend/src/styles/plans.cssfrontend/src/styles/settings.css
…okens (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.
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.
…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.
…(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.
…nment + 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.
…#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.
…#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.
… 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.
…sue #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.
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.
…ssue #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.
…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.
…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).
…y (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.
…tion (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.
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.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
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.
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/src/navigation.ts (1)
163-167:⚠️ Potential issue | 🟠 Major | ⚡ Quick winScope admin sub-tab activation to the admin panel.
Line 163 toggles every
.sub-tab-btnin the document. With Inventory & Coverage now having its own sub-nav, switching an Admin sub-tab also clears active state outside Admin. Query the admin container instead.Suggested fix
- document.querySelectorAll<HTMLButtonElement>('.sub-tab-btn').forEach(btn => { + document.querySelectorAll<HTMLButtonElement>('#admin-tab .sub-tab-btn').forEach(btn => { const isActive = btn.dataset['settingsTab'] === subTab; btn.classList.toggle('active', isActive); btn.setAttribute('aria-selected', isActive ? 'true' : 'false'); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/navigation.ts` around lines 163 - 167, The code currently toggles every '.sub-tab-btn' across the document, causing Admin sub-tab changes to clear active state in other panels; restrict the query to the Admin panel container (e.g., select the admin panel element and call querySelectorAll('.sub-tab-btn') on it) so only sub-tab buttons within the Admin container are iterated; keep using the same logic with btn.dataset['settingsTab'], btn.classList.toggle('active', isActive) and btn.setAttribute('aria-selected', ...) but replace document.querySelectorAll with the scoped container.querySelectorAll to limit updates to the Admin panel.
♻️ Duplicate comments (1)
frontend/src/navigation.ts (1)
223-225:⚠️ Potential issue | 🟠 MajorStill missing legacy URL canonicalization.
Line 224 resolves the legacy segment but leaves the stale pathname in place, so
/dashboard,/settings/..., etc. can remain in the address bar/back stack even though Lines 34-35 say they arereplaceState()'d. Rewrite to the canonical path before returning.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/navigation.ts` around lines 223 - 225, The legacy segment check resolves the target but doesn’t update the browser URL, leaving stale paths in the address bar/back stack; change the branch that handles LEGACY_PATH_REDIRECTS to compute the canonical segment (e.g., const canonical = LEGACY_PATH_REDIRECTS[segment]) then call the history replace API to update the pathname (preserving query/hash) — e.g., use window.history.replaceState(...) or your router's replace method — and finally return the canonical; likewise ensure the final return uses the rewritten canonical value (use LEGACY_PATH_REDIRECTS, segment, TABS as location hints).
🧹 Nitpick comments (1)
frontend/src/__tests__/settings-subnav.test.ts (1)
30-45: ⚡ Quick winAdd a regression test for the RI Exchange exemption.
This suite only exercises the generic
.settings-buttonspath. The special-case guard inreflectDirtyState()is what prevents a second sticky save bar inside Purchasing, so it should stay covered here.Suggested test
+ it('skips nested RI Exchange settings rows', () => { + const wrapper = document.createElement('div'); + wrapper.id = 'ri-exchange-automation-settings'; + const nested = document.createElement('div'); + nested.className = 'settings-buttons'; + wrapper.appendChild(nested); + document.body.appendChild(wrapper); + + reflectDirtyState(true); + expect(nested.classList.contains('settings-savebar')).toBe(false); + expect(nested.classList.contains('dirty')).toBe(false); + });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/__tests__/settings-subnav.test.ts` around lines 30 - 45, Add a regression test that verifies the RI Exchange/Purchasing exemption in reflectDirtyState: create a .settings-buttons element placed inside a container that matches the guard used by reflectDirtyState (e.g., class "purchasing" or data attribute "data-module='purchasing'" — use whichever the implementation checks for), call reflectDirtyState(true), and assert that the inner .settings-buttons does NOT receive the "settings-savebar" (or second sticky save bar) class while other .settings-buttons still do; then call reflectDirtyState(false) and assert cleanup of "dirty" as in the existing test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/index.html`:
- Around line 244-247: The Inventory sub-tabs (container class
"inventory-subnav" and the buttons with data-inv-subtab values
"active-commitments", "coverage", "ri-exchange" and aria-controls values like
"inventory-active-commitments") are missing the paired tabpanel elements and
proper button IDs/labeling; fix by giving each tab button a unique id, ensuring
aria-controls matches the id of a corresponding element with role="tabpanel",
adding aria-labelledby on each panel that references the tab button id, and
updating panel visibility (hidden or aria-hidden) and buttons' aria-selected
states so only the active tab (data-inv-subtab="ri-exchange") has
aria-selected="true" and its panel is shown; apply the same wiring pattern to
the other inventory/admin sub-tab group referenced around lines 302-307.
In `@frontend/src/styles/components.css`:
- Around line 498-505: The CSS is using invalid concatenations like
var(--cudly-surface)3cd and var(--cudly-surface)3e0 which browsers ignore; for
each affected selector (e.g., .settings-savebar.dirty::before and the
paused/medium-confidence/running badge rules mentioned in the comment) replace
the concatenated form with a valid color expression — either use the custom
property directly (background: var(--cudly-surface);) or compute a proper
translucent variant (e.g., background: rgba(...) or use
color-mix()/color-modifiers or a separate CSS variable that includes alpha) so
the background becomes valid CSS and displays as intended.
In `@frontend/src/styles/settings.css`:
- Line 253: Replace the invalid CSS tokens like "var(--cudly-surface)bf5",
"var(--cudly-surface)3cd", and "var(--cudly-surface)3e0" with valid color
values: for the two occurrences in the settings styles that are warning
backgrounds (previously at the shown diff line and another nearby), use `#fff3e0`
or var(--cudly-warn-bg); for the remaining warning/yellow uses (including the
settings.css line 421 and the four instances in components.css) replace them
with var(--cudly-warn-bg) to keep consistency; search for the exact malformed
strings and update each background-color declaration so the CSS parser accepts
the values.
---
Outside diff comments:
In `@frontend/src/navigation.ts`:
- Around line 163-167: The code currently toggles every '.sub-tab-btn' across
the document, causing Admin sub-tab changes to clear active state in other
panels; restrict the query to the Admin panel container (e.g., select the admin
panel element and call querySelectorAll('.sub-tab-btn') on it) so only sub-tab
buttons within the Admin container are iterated; keep using the same logic with
btn.dataset['settingsTab'], btn.classList.toggle('active', isActive) and
btn.setAttribute('aria-selected', ...) but replace document.querySelectorAll
with the scoped container.querySelectorAll to limit updates to the Admin panel.
---
Duplicate comments:
In `@frontend/src/navigation.ts`:
- Around line 223-225: The legacy segment check resolves the target but doesn’t
update the browser URL, leaving stale paths in the address bar/back stack;
change the branch that handles LEGACY_PATH_REDIRECTS to compute the canonical
segment (e.g., const canonical = LEGACY_PATH_REDIRECTS[segment]) then call the
history replace API to update the pathname (preserving query/hash) — e.g., use
window.history.replaceState(...) or your router's replace method — and finally
return the canonical; likewise ensure the final return uses the rewritten
canonical value (use LEGACY_PATH_REDIRECTS, segment, TABS as location hints).
---
Nitpick comments:
In `@frontend/src/__tests__/settings-subnav.test.ts`:
- Around line 30-45: Add a regression test that verifies the RI
Exchange/Purchasing exemption in reflectDirtyState: create a .settings-buttons
element placed inside a container that matches the guard used by
reflectDirtyState (e.g., class "purchasing" or data attribute
"data-module='purchasing'" — use whichever the implementation checks for), call
reflectDirtyState(true), and assert that the inner .settings-buttons does NOT
receive the "settings-savebar" (or second sticky save bar) class while other
.settings-buttons still do; then call reflectDirtyState(false) and assert
cleanup of "dirty" as in the existing test.
🪄 Autofix (Beta)
✅ Autofix completed
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0f7f8e84-538f-42d7-a21d-e578dda1375b
📒 Files selected for processing (16)
frontend/src/__tests__/css.test.tsfrontend/src/__tests__/html.test.tsfrontend/src/__tests__/settings-subnav.test.tsfrontend/src/index.htmlfrontend/src/navigation.tsfrontend/src/settings-subnav.tsfrontend/src/styles/base.cssfrontend/src/styles/charts.cssfrontend/src/styles/components.cssfrontend/src/styles/forms.cssfrontend/src/styles/layout.cssfrontend/src/styles/modals.cssfrontend/src/styles/plans.cssfrontend/src/styles/settings.cssfrontend/src/styles/tables.cssfrontend/src/styles/tabs.css
✅ Files skipped from review due to trivial changes (1)
- frontend/src/styles/charts.css
🚧 Files skipped from review as they are similar to previous changes (3)
- frontend/src/tests/html.test.ts
- frontend/src/styles/plans.css
- frontend/src/styles/layout.css
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Fixes Applied SuccessfullyFixed 3 file(s) based on 3 unresolved review comments. Files modified:
Commit: The changes have been pushed to the Time taken: |
|
The two
Both CR auto-acknowledged these in the per-comment thread. |
Fixed 3 file(s) based on 3 unresolved review comments. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
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.
Merges CodeRabbit's autofix commit (3095246) that fixed 7 invalid CSS values (var(--cudly-surface)bf5/3cd/3e0) and added ARIA tabpanel wiring to inventory and admin sub-tabs. My additions on top: - index.html: add role="tabpanel" + aria-labelledby="admin-subtab-users" to apikeys-section, which CR autofix missed (it's a second panel controlled by the same "users" sub-tab button) - settings-subnav.test.ts: add regression test guarding the RI Exchange exemption in reflectDirtyState — verifies that .settings-buttons nested inside #ri-exchange-automation-settings are not promoted to the sticky save-bar when dirty state changes All 1598 tests pass; build clean.
|
@coderabbitai review Round 3 — addressed all three actionable findings from your last review:
Two prior findings (admin sub-tab querySelector scoping to Tests: 1598/1598 pass. Build: clean. |
|
Triggering Round 3 review now. ✅ Actions performedReview triggered.
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
frontend/src/__tests__/navigation.test.ts (2)
277-315: ⚡ Quick winAdd
/admin/userspath coverage for settings sub-tab resolution.
getSettingsSubTabFromPathis now covered for/admin,/admin/accounts, and/admin/purchasing, but not theusersdeep link.Suggested patch
test('returns purchasing for /admin/purchasing', () => { delete (window as unknown as Record<string, unknown>).location; (window as unknown as Record<string, unknown>).location = { pathname: '/admin/purchasing' } as Location; expect(getSettingsSubTabFromPath()).toBe('purchasing'); }); + + test('returns users for /admin/users', () => { + delete (window as unknown as Record<string, unknown>).location; + (window as unknown as Record<string, unknown>).location = { pathname: '/admin/users' } as Location; + expect(getSettingsSubTabFromPath()).toBe('users'); + });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/__tests__/navigation.test.ts` around lines 277 - 315, Add a unit test in frontend/src/__tests__/navigation.test.ts that mirrors the existing admin path tests: call getSettingsSubTabFromPath with window.location.pathname set to '/admin/users' (use the same delete and reassign pattern used in the other tests) and assert the result is 'users'; place it alongside the other describe('getSettingsSubTabFromPath') cases so both canonical (/admin/users) and, if desired, the legacy '/settings/users' variant can be tested similarly.
43-54: ⚡ Quick winAdd
inventoryswitchTab coverage to match the six-section IA.The fixture and switch tests currently skip the
inventorytop-level section, so a regression there can pass unnoticed.Suggested patch
<div class="tabs"> <button class="tab-btn active" data-tab="home">Dashboard</button> <button class="tab-btn" data-tab="opportunities">Recommendations</button> <button class="tab-btn" data-tab="plans">Plans</button> <button class="tab-btn" data-tab="purchases">History</button> + <button class="tab-btn" data-tab="inventory">Inventory & Coverage</button> <button class="tab-btn" data-tab="admin">Settings</button> </div> <div id="home-tab" class="tab-content active"></div> <div id="opportunities-tab" class="tab-content"></div> <div id="plans-tab" class="tab-content"></div> <div id="purchases-tab" class="tab-content"></div> + <div id="inventory-tab" class="tab-content"></div> <div id="admin-tab" class="tab-content"> @@ test('switches to history tab and initializes date range', () => { switchTab('purchases'); @@ expect(initHistoryDateRange).toHaveBeenCalled(); }); + + test('switches to inventory tab', () => { + switchTab('inventory'); + + const inventoryBtn = document.querySelector('[data-tab="inventory"]'); + const inventoryContent = document.getElementById('inventory-tab'); + expect(inventoryBtn?.classList.contains('active')).toBe(true); + expect(inventoryContent?.classList.contains('active')).toBe(true); + });Also applies to: 73-193
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/__tests__/navigation.test.ts` around lines 43 - 54, The test fixture and switchTab assertions are missing the "inventory" top-level section; update the HTML fixture in navigation.test.ts to add a button with class "tab-btn" and data-tab="inventory" (e.g., data-tab="inventory") and a corresponding content div with id="inventory-tab" and class "tab-content", and then add assertions in the switchTab tests that call the switchTab handler (or simulate clicking the tab button) to verify the "active" class toggles correctly for the inventory button and `#inventory-tab` element just like the other sections (use existing patterns around data-tab="home", "opportunities", etc., to locate where to add the new case).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/dashboard.ts`:
- Around line 247-253: attachSparkline currently removes children but leaves the
previous sparkline visible when values are empty or on error because the early
return doesn't hide the svg; update attachSparkline to explicitly clear children
and add svg.classList.add('hidden') (or remove the visible class) before
returning when values.length < 2, and likewise ensure any error/empty-response
handlers that previously only returned on failure also call the same hide/clear
behavior (refer to function attachSparkline and the kpi-tile-spark element/class
and mirror this hide+clear logic in the error paths around the other update code
mentioned at lines 621-625).
---
Nitpick comments:
In `@frontend/src/__tests__/navigation.test.ts`:
- Around line 277-315: Add a unit test in
frontend/src/__tests__/navigation.test.ts that mirrors the existing admin path
tests: call getSettingsSubTabFromPath with window.location.pathname set to
'/admin/users' (use the same delete and reassign pattern used in the other
tests) and assert the result is 'users'; place it alongside the other
describe('getSettingsSubTabFromPath') cases so both canonical (/admin/users)
and, if desired, the legacy '/settings/users' variant can be tested similarly.
- Around line 43-54: The test fixture and switchTab assertions are missing the
"inventory" top-level section; update the HTML fixture in navigation.test.ts to
add a button with class "tab-btn" and data-tab="inventory" (e.g.,
data-tab="inventory") and a corresponding content div with id="inventory-tab"
and class "tab-content", and then add assertions in the switchTab tests that
call the switchTab handler (or simulate clicking the tab button) to verify the
"active" class toggles correctly for the inventory button and `#inventory-tab`
element just like the other sections (use existing patterns around
data-tab="home", "opportunities", etc., to locate where to add the new case).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9794562c-8e9f-4f34-bfee-5a3dbabcb427
📒 Files selected for processing (9)
frontend/src/__tests__/html.test.tsfrontend/src/__tests__/navigation.test.tsfrontend/src/__tests__/settings-subnav.test.tsfrontend/src/dashboard.tsfrontend/src/index.htmlfrontend/src/inventory.tsfrontend/src/navigation.tsfrontend/src/styles/components.cssfrontend/src/styles/settings.css
🚧 Files skipped from review as they are similar to previous changes (5)
- frontend/src/tests/settings-subnav.test.ts
- frontend/src/inventory.ts
- frontend/src/index.html
- frontend/src/navigation.ts
- frontend/src/styles/components.css
…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
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
♻️ Duplicate comments (1)
frontend/src/dashboard.ts (1)
618-623:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAlso clear the YTD sparkline in the error path.
The no-data branch now resets the KPI sparkline, but a failed analytics refresh still leaves the previous polyline visible while the widget switches to the unavailable state. Please mirror
attachSparkline('ytd', [])in thecatchblock around Line 676 so the KPI stays consistent.Suggested patch
} catch (err) { // Analytics is Postgres-backed now, but the endpoint still guards with // a 503 when the analytics client isn't wired. Don't break the rest of // the dashboard — hide the widget and fall back to a neutral message. console.warn('Savings trend chart unavailable:', err); if (savingsTrendChart) { savingsTrendChart.destroy(); savingsTrendChart = null; } + attachSparkline('ytd', []); canvas.classList.add('hidden'); if (empty) { empty.textContent = 'Savings history is not available yet.'; empty.classList.remove('hidden'); }Also applies to: 628-632
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/dashboard.ts` around lines 618 - 623, The error path currently leaves the previous YTD KPI sparkline visible; in the catch block that handles the analytics refresh failure (where you hide the canvas, show the unavailable/empty UI and destroy savingsTrendChart), call attachSparkline('ytd', []) as well to clear the KPI polyline; mirror the same attachSparkline('ytd', []) addition wherever the no-data branch resets the sparkline (same logic around the alternate branch that hides canvas and shows empty) so both the no-data and error paths consistently clear the YTD sparkline.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@frontend/src/dashboard.ts`:
- Around line 618-623: The error path currently leaves the previous YTD KPI
sparkline visible; in the catch block that handles the analytics refresh failure
(where you hide the canvas, show the unavailable/empty UI and destroy
savingsTrendChart), call attachSparkline('ytd', []) as well to clear the KPI
polyline; mirror the same attachSparkline('ytd', []) addition wherever the
no-data branch resets the sparkline (same logic around the alternate branch that
hides canvas and shows empty) so both the no-data and error paths consistently
clear the YTD sparkline.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6eca1756-f67e-4e6c-969a-462ac17dc1be
📒 Files selected for processing (2)
frontend/src/__tests__/navigation.test.tsfrontend/src/dashboard.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- frontend/src/tests/navigation.test.ts
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.
|
@coderabbitai review |
Summary
Restructure the CUDly frontend information architecture and reskin to the action-center pattern from
~/Desktop/CUDly UI Revamp/mocks. Frontend-only — no backend, API, IaC, or Go-side changes.Closes #340.
Before / After
Before: 6 top-tabbed nav (Dashboard · Recommendations · Purchase Plans · Purchase History · RI Exchange · Settings) above the content.
After: action-center shell — top bar (logo + user info) + left sidebar with 6 sections:
Commit-by-commit
style: design-token CSS variables:roottokens for color, type, space, radius, elevation, layoutfeat: action-center shell — top bar + sidebar navrefactor: rename tab IDs + URL redirectLEGACY_PATH_REDIRECTSso/dashboard,/recommendations,/history,/settings/*,/ri-exchangestill resolvefeat: fold RI Exchange into Inventory & Coveragefeat/admin: relabel Admin sub-tabsfeat/home: KPI sparklines + tightened card gridstyle: visual polish across Opportunities + Plans.context-drawerCSS slot reserved for future right-side drawerVerification
/dashboard→/home,/ri-exchange→/inventory,/settings/general→/admin/general).Deferred (tracked as sub-tasks in #340)
These mock widgets need backend support before the frontend can wire them:
.context-drawer); cross-payment-option comparisonEmpty-state placeholders ship today wherever a section would otherwise render nothing meaningful.
Constraints upheld
internal/,iac/,terraform/,Optimizer/.createElement/textContent/appendChildfor all new code paths; noinnerHTMLwith interpolated data.aria-*/role/aria-labelledbypreserved on every renamed panel; sidebar nav is keyboard-navigable.LEGACY_PATH_REDIRECTSmap handles every old top-level path.Closes
Summary by CodeRabbit
New Features
Style
Tests