PR-Agent flagged two more real regressions vs the original health page:
1. Stale timestamp (HealthStatusRibbon): formatRelativeTime reads
Date.now() during render, but nothing forces a re-render after mount
so the 'last scan' label froze and never advanced to '1 minute ago',
'2 hours ago', etc. Restore the per-second tick (matching the
original health.tsx behavior) so the label behaves like a real
relative timestamp.
2. Hidden copy action (HealthPriorityCard): the fix-copy button was
gated by group-hover, making it invisible to keyboard users and
unreachable on mobile (no hover state). Make it always visible with
an aria-label so screen-reader and touch users can copy the fix
command.
PR-Agent flagged two real bugs at score 84:
1. Accessibility (HealthPriorityCard): the priority card header was a
clickable <div> with onClick, making expand/collapse mouse-only.
Promote it to a <button type="button"> with aria-expanded reflecting
state and a contextual aria-label, and a focus-visible ring. The
inner content is extracted into a shared fragment so the button and
the no-content fallback <div> render the same markup.
2. Time display (HealthStatusRibbon): formatRelativeTime lost the hours
branch from the original health.tsx, so a 2-hour-old scan showed as
'120 minutes ago' instead of '2 hours ago'. Restore the hours
rollover with health.hoursAgo i18n key.
After the v1.1 restructure, health was further redesigned per a separate
brief and went bespoke -- new components (HealthStatusRibbon,
HealthPriorityCard, HealthPriorityList, HealthAuditSection), no
PageShell/PageHeader/MonitorLayout, custom severity-driven hierarchy.
Bring the docs in line:
- design-system.md: add §1d Bespoke as a fourth identity-strip pattern
with health as canonical reference; remove health from PageHeader
and Monitor canonical-reference slots (now TBD)
- design-decisions.md: v1.2 revision row capturing the redesign and
the resulting doc cleanup
- screenshots-phase2/after-03-health.png: replace stale Monitor-archetype
screenshot with the actual final redesigned page (Privacy mode ON)
- design-system-phase2-preview.html: regenerate; describe the four
patterns, embed the new screenshot, update the file change list
Self-contained verification report at ui/docs/health-redesign/report.html
showing four states of the redesigned health page (initial, loading,
expanded issue, expansion fix). Screenshots at the same level so the
report's relative image refs resolve when opened directly.
Captures the design exploration delivered against the handoff brief
(plans/reports/handoff-260425-1417-health-page-redesign.md).
Iteration on top of the health redesign:
- layout.tsx: drop backdrop-blur transparency on the global header
(interfered with the new health page colored backdrop)
- health.tsx: switch from fixed full-viewport background to scoped
absolute positioning; replace unreliable external grain SVG (vercel
app) with an inline data URI that always loads
- health-priority-card.tsx: refine spacing, expansion behavior, and
visual hierarchy
The v1.1 design contract names three identity-strip patterns but the
styleguide previously only demoed PageHeader. Future contributors
visiting /_styleguide had no live preview of HeroBar or rail-anchored
identity, leaving the contract under-documented.
Add:
- 1a. HeroBar demo (canonical: home pattern) -- single-row dense hero
with logo + title + version badge + 4 inline stat tiles, dotted
background pattern, anonymized data
- 1b. Rail-anchored identity demo (canonical: cliproxy pattern) -- a
Config 3-pane layout where the left rail carries brand + Quick Setup
CTA + provider list + footer status, with NO top chrome
- 1c. PageHeader demo (existing, retitled with health pattern reference)
All demos use anonymized data (Provider A/B/C, Demo Dashboard, fake
metrics) so screenshots are safe to publish without Privacy mode.
home and cliproxy were reverted to their original designs; before/after
screenshots would show identical content. Keep only health migration
screenshots and refocus the HTML report on the design-system docs
restructure (three identity-strip patterns).
User feedback caught a Phase 2 mistake: forcing home and cliproxy into
a one-size-fits-all PageShell + PageHeader chrome regressed density on
both pages.
- home: original 1-row hero (logo + title + version + 4 inline stats)
was split into PageHeader + KpiRow, doubling vertical footprint
- cliproxy: original rail-anchored brand was duplicated by a top
PageHeader, stealing ~80px from the 3-pane body
Resolution: revert both pages to dev-branch state, then restructure
ui/docs/design-system.md to extract THREE identity-strip patterns from
the canonical references rather than imposing one:
1. HeroBar (1-row dense) -> home pattern
2. Rail-anchored (no top chrome) -> cliproxy pattern
3. PageHeader (title + description) -> health pattern
Health stays migrated -- the Monitor archetype + PageHeader is a real
improvement there because the gauge + KPI row + group cards don't fit
the HeroBar pattern.
Future page migrations adapt to whichever identity-strip pattern fits
the page's content shape, not the other way around. design-decisions.md
gets a v1.1 revision row capturing the rationale.
PR-Agent caught a regression: pre-migration the four home stats had
onClick handlers navigating to /providers, /cliproxy, /accounts, and
/health. The Phase 2 migration dropped those handlers, removing the
shortcuts for all users.
Add an optional onClick prop to KpiCard. When supplied, the tile
promotes from a static <div> to a <button> with hover lift, keyboard
focus ring, and an accessible aria-label derived from the label.
Wire all four home KPIs back to their original navigation targets.
Self-contained HTML report at ui/docs/design-system-phase2-preview.html
with embedded base64 before/after screenshots so reviewers can scrub
the cliproxy / home / health migrations side-by-side without leaving
GitHub. Captured via Playwright with Privacy mode on; data anonymized.
Wrap master-detail layout in PageShell + PageHeader + ConfigLayout.
PageHeader carries the brand strip (Zap icon + CLIProxy title +
account-management description) and refresh button. The existing
sidebar (Quick Setup CTA, sectioned providers list, variants list,
ProxyStatusWidget, footer connection summary) becomes ConfigLayout's
left rail; ProviderEditor and EmptyProviderState become the form pane.
Pure layout migration: every sub-flow still mounts correctly --
QuickSetupWizard dialog, AddAccountDialog (URL action=auth deep-link),
AccountSafetyWarningCard for gemini/agy, provider selection persisted
to localStorage and URL deep-link, all bulk pause/resume/solo/delete
mutations untouched. ProviderEditor (which contains the form + raw JSON
internally) remains as a single 'form' slot.
Hero gauge + status copy moved into a MonitorCard variant=terminal,
preserving the ccs doctor / live-ops aesthetic per locked decision #4.
HealthStatsBar replaced by a 4-column KpiRow with severity tones
(Checks / Passed / Warnings / Errors). Each HealthGroupSection becomes
its own MonitorCard while keeping the existing accordion behavior
intact -- so check expansion, sort-by-severity, and live monitoring
continue to work unchanged.
PageHeader carries the version badge, last-scan relative time,
auto-refresh hint, ccs doctor copy action, and refresh button.
Pure layout + visual hierarchy migration -- no new metrics, no new
features. LOC: 263 -> ~245.
Render exactly one layout at a time -- desktop 3-pane grid OR mobile
tabs -- via a useIsDesktop matchMedia hook gated at the lg breakpoint.
Previously the component rendered BOTH and toggled visibility via
Tailwind 'hidden lg:grid' / 'lg:hidden'. This duplicated FormSection
elements in the DOM, so document.getElementById() in SectionRail's
scroll-spy and click-to-jump would resolve to the (hidden) desktop copy
first on mobile -- making the rail attach to the wrong scroller and
never update the active section.
MonitorLayout:
- Wrap ScrollArea in an explicit flex-col container with min-h-0. The
previous version relied on the parent (PageShell) being flex-col for
flex-1 to compute a definite height; that's fragile. The wrapper now
establishes its own flex column so the ScrollArea reliably scrolls
whether or not the caller's wrapper is flex.
ConfigLayout (MobileTabs):
- Derive active tab during render with fallback to first available, same
pattern applied to JsonPane in the previous round. Parent toggling
`left` or `json` props could change the available tabs and leave
the controlled Tabs value pointing at a tab id that no longer existed,
rendering an empty pane on mobile until the user manually switched.
JsonPane:
- Reset active tab when the tabs prop changes. Previously activeTab was
initialized once and never re-synced; a parent swapping the tab set
(e.g. selecting a different entity) could leave us pointing at a stale
id, leaving the pane empty and Copy returning '{}'.
- Force textarea remount on data change in editable mode via key={text}.
Uncontrolled defaultValue retained the prior value when the underlying
config changed, so onBlur could save stale text.
SectionRail:
- Default scroll-spy root to the nearest scrollable ancestor instead of
the page viewport. FormPane wraps its body in a shadcn ScrollArea, so
IntersectionObserver with root:null was watching the wrong scroller and
the active section never updated as the form scrolled.
- observeRoot prop still wins when supplied for explicit overrides.
Self-contained HTML report at ui/docs/design-system-preview.html with
embedded base64 screenshots so the PR review and the report itself remain
viewable without external hosting. Companion to the live /_styleguide
route.
Screenshots captured with Privacy mode ON via Playwright; all data
anonymized (Provider A/B/C, fake metrics) -- no real account names,
emails, or OAuth identifiers.
The previous fix used react-hook-form's `dirtyFields.extraModels` to
gate forwarding of the empty value. RHF compares each value to its
`defaultValues` entry, so typing "x" and erasing back to "" reverts
the dirty flag to false — making it impossible to clear a saved
ANTHROPIC_EXTRA_MODELS through the dashboard.
Track a local boolean that latches true on the first onChange and
resets when the dialog closes. The latch survives a "type then
delete" round-trip, so the empty-string delete signal still reaches
the server, while a never-touched field still gets skipped to
preserve the existing saved value.
The edit dialog cannot pre-populate `extraModels` from the existing
profile (the Profile type carries no env data), so the field always
opens blank in edit mode. Forwarding that blank value unconditionally
clobbered any saved ANTHROPIC_EXTRA_MODELS, since the PUT route treats
an empty string as a delete signal.
Track react-hook-form's `dirtyFields.extraModels` and only include the
field in the update payload when the user actually touched it. Typing
then clearing still marks the field dirty, so the explicit-clear UX
keeps working.
Add ANTHROPIC_EXTRA_MODELS env var to allow each API profile to
configure additional models alongside the primary ANTHROPIC_MODEL.
These are synced to CLIProxy config.yaml during ccs cliproxy sync.
- profile-mapper.ts: parse ANTHROPIC_EXTRA_MODELS on sync
- profile-writer.ts: write extra models to settings.json
- shared.ts + create-command.ts: --extra-models CLI flag
- profile-routes.ts + route-helpers.ts: REST API support
- profile-dialog.tsx + api-client.ts: frontend input