diff --git a/docs/superpowers/specs/2026-04-23-checkpoints-grid-row-design.md b/docs/superpowers/specs/2026-04-23-checkpoints-grid-row-design.md new file mode 100644 index 00000000..724f97fc --- /dev/null +++ b/docs/superpowers/specs/2026-04-23-checkpoints-grid-row-design.md @@ -0,0 +1,252 @@ +# Checkpoints in the Identity grid + locale time + remove History — design + +**Date:** 2026-04-23 +**Scope:** three targeted UX changes on the unified app deployment page, follow-up to `2026-04-23-deployment-page-polish-design.md`. +**Status:** Draft — pending user review. + +## 1. Motivation + +The previous polish shipped a collapsible `CheckpointsTable` as a standalone section below the Identity & Artifact block. That made the visual hierarchy noisy — Checkpoints became a third section between Identity and the config tabs, competing for attention. The proper home for "how many past deployments exist and what were they" is *inside* the Identity panel, as one more row in its config grid. + +Three changes: + +1. Move the checkpoints section into the Identity & Artifact config grid as an in-grid row. +2. Format the Deployed-column sub-line to the user's locale (replaces the raw ISO string). +3. Remove the redundant `HistoryDisclosure` from the Deployment tab — the checkpoints table covers the same information and the per-deployment log drill-down now lives in the drawer. + +## 2. Design + +### 2.1 Checkpoints row in the Identity config grid + +**Current structure** (`IdentitySection.tsx`): + +```tsx +
+ Identity & Artifact +
+ ... label + value cells (Application Name, Slug, Environment, External URL, Current Version, Application JAR) ... +
+ {children} {/* CheckpointsTable + CheckpointDetailDrawer currently render here */} +
+``` + +**New structure:** + +```tsx +
+ Identity & Artifact +
+ ... existing label + value cells ... + {checkpointsSlot} {/* NEW: rendered as direct grid children via React.Fragment */} +
+ {children} {/* still used — for the portal-rendered CheckpointDetailDrawer */} +
+``` + +**Slot contract.** `IdentitySection` gains a new prop: + +```ts +interface IdentitySectionProps { + // ... existing props ... + checkpointsSlot?: ReactNode; + children?: ReactNode; +} +``` + +`checkpointsSlot` is expected to be a React.Fragment whose children are grid-direct cells (spans / divs). React fragments are transparent to CSS grid, so the inner elements become direct children of `configGrid` and flow into grid cells like the existing rows. + +**`CheckpointsTable` rewrite.** Instead of wrapping itself in `
`, the component returns a Fragment of grid-ready children: + +```tsx +if (checkpoints.length === 0) { + return null; +} + +return ( + <> + Checkpoints +
+ +
+ {open && ( +
+ ...
+ {hidden > 0 && !expanded && ( + + )} +
+ )} + +); +``` + +**Why this layout.** +- The trigger button sits in the value column (180px label + 1fr value). When closed, the row reads `Checkpoints ▸ Expand (5)`. +- When opened, a third grid child appears: a div that spans both columns (`grid-column: 1 / -1`) containing the `` + optional "Show older" button. This gives the 7-column table the full grid width so columns don't crush. +- The trigger remains in the value cell of the label row above — collapse/expand stays attached to its label. + +**CSS changes** (`AppDeploymentPage.module.css`): + +*Add:* + +```css +.checkpointsTriggerCell { + display: flex; + align-items: center; +} + +.checkpointsTrigger { + display: inline-flex; + align-items: center; + gap: 6px; + background: none; + border: none; + padding: 0; + color: var(--text-primary); + cursor: pointer; + font: inherit; + text-align: left; +} + +.checkpointsTrigger:hover { + color: var(--amber); +} + +.checkpointsTableFullRow { + grid-column: 1 / -1; + margin-top: 4px; +} +``` + +*Remove (no longer referenced):* +- `.checkpointsSection` +- `.checkpointsHeader` + `.checkpointsHeader:hover` +- `.checkpointsCount` + +*Keep:* `.checkpointsChevron` (still used by the trigger for the arrow). `.checkpointsTable`, `.jarCell`, `.jarName`, `.jarStrike`, `.archivedHint`, `.isoSubline`, `.muted`, `.strategyPill`, `.outcomePill`, `.outcome-*`, `.chevron`, `.showOlderBtn`, `.checkpointArchived` — all still referenced by the table body. + +*Also remove* (cleanup — unrelated dead weight from the retired `Checkpoints.tsx` row-list view, safe to delete because no TSX references remain): +- `.checkpointsRow` +- `.disclosureToggle` +- `.checkpointList` +- `.checkpointRow` +- `.checkpointMeta` +- Standalone `.checkpointArchived { color: var(--warning); font-size: 12px; }` (the table-row variant `.checkpointsTable tr.checkpointArchived { opacity: 0.55; }` stays) +- `.historyRow` (see §2.3) + +### 2.2 Deployed-column locale sub-line + +In `CheckpointsTable.tsx`, the Deployed ` +``` + +Replace with: + +```tsx + +``` + +`new Date(iso).toLocaleString()` uses the browser's resolved locale via the Intl API. No locale plumbing, no new util. + +Primary "5h ago" display stays unchanged. + +### 2.3 Remove the History disclosure from the Deployment tab + +`HistoryDisclosure.tsx` renders a collapsible `DataTable` + nested `StartupLogPanel`. It duplicates information now surfaced via `CheckpointsTable` + `CheckpointDetailDrawer` (which has its own LogsPanel). + +**Changes:** + +- Delete `ui/src/pages/AppsTab/AppDeploymentPage/DeploymentTab/HistoryDisclosure.tsx`. +- `ui/src/pages/AppsTab/AppDeploymentPage/DeploymentTab/DeploymentTab.tsx` — remove the import and the `` render at the bottom of the tab. +- `ui/src/pages/AppsTab/AppDeploymentPage/AppDeploymentPage.module.css` — drop the `.historyRow` rule (covered in §2.1's CSS cleanup list). + +## 3. Page wiring + +`ui/src/pages/AppsTab/AppDeploymentPage/index.tsx` currently passes the table + drawer together as `children` to `IdentitySection`: + +```tsx + + {app && ( + <> + + {selectedDep && } + + )} + +``` + +After the change: + +```tsx + : undefined} +> + {app && selectedDep && } + +``` + +The drawer continues to pass through as `children` because `SideDrawer` uses `createPortal` — it can live at any DOM depth, but conceptually sits outside the Identity grid so it doesn't become a stray grid cell. + +## 4. Files touched + +| Path | Change | +|------|--------| +| `ui/src/pages/AppsTab/AppDeploymentPage/IdentitySection.tsx` | Add `checkpointsSlot?: ReactNode`; render inside `configGrid` after JAR row | +| `ui/src/pages/AppsTab/AppDeploymentPage/CheckpointsTable.tsx` | Return React.Fragment of grid-ready children; replace header wrapper with `checkpointsTrigger` button; locale sub-line in Deployed cell | +| `ui/src/pages/AppsTab/AppDeploymentPage/CheckpointsTable.test.tsx` | Update `expand()` helper to target `/expand|collapse/i`; add test asserting locale sub-line differs from raw ISO | +| `ui/src/pages/AppsTab/AppDeploymentPage/AppDeploymentPage.module.css` | Add `.checkpointsTriggerCell`, `.checkpointsTrigger`, `.checkpointsTableFullRow`; remove obsolete classes listed in §2.1 | +| `ui/src/pages/AppsTab/AppDeploymentPage/index.tsx` | Split `checkpointsSlot` out of `children`; drawer stays in `children` | +| `ui/src/pages/AppsTab/AppDeploymentPage/DeploymentTab/DeploymentTab.tsx` | Remove `HistoryDisclosure` import + render | +| `ui/src/pages/AppsTab/AppDeploymentPage/DeploymentTab/HistoryDisclosure.tsx` | **Delete** | + +## 5. Testing + +**Unit (vitest + RTL):** + +- Update `CheckpointsTable.test.tsx`: + - `expand()` helper targets `screen.getByRole('button', { name: /expand|collapse/i })`. + - The "defaults to collapsed" test asserts the trigger button exists and reads `Expand (1)`; rows hidden. + - The "clicking header expands" test clicks the button (now labeled `Expand`); after click, button label is `Collapse`; rows visible. + - One new test: render the table with `deployedAt: '2026-04-23T10:35:00Z'`, expand, grab the `.isoSubline` element, assert its text contains neither the raw ISO `T` nor `Z`, i.e. it was parsed into a localized form. (Avoids asserting the exact string — CI locales vary.) + +**Manual smoke:** + +- Page loads → `Checkpoints | ▸ Expand (N)` as a grid row under Application JAR. Collapsed by default. +- Click trigger → text swaps to `▾ Collapse (N)`; table appears below, spanning full grid width. +- Deployed column sub-line shows a local-format date/time (e.g. `4/23/2026, 12:35:00 PM` in `en-US`). +- Deployment tab no longer shows `▶ History (N)` below `Startup Logs`. +- `CheckpointDetailDrawer` still opens on row click (unaffected). +- Empty state: app with no checkpoints shows no Checkpoints row at all. + +## 6. Non-goals + +- No changes to `CheckpointDetailDrawer` layout or behavior. +- No changes to `timeAgo` (other components still use it). +- No new locale-formatting helpers; `toLocaleString()` inline at the one callsite. +- Not touching primary Deployed column display (keeps "5h ago"). +- No changes to the `CheckpointsTable` columns themselves. + +## 7. Open questions + +None — all resolved during brainstorming.
` currently renders: + +```tsx + + {d.deployedAt && timeAgo(d.deployedAt)} +
{d.deployedAt}
+
+ {d.deployedAt && timeAgo(d.deployedAt)} +
+ {d.deployedAt && new Date(d.deployedAt).toLocaleString()} +
+