# Checkpoints Grid Row Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Move the Checkpoints section into the Identity & Artifact config grid as an in-grid row, localize the Deployed-column sub-line, and remove the now-redundant History disclosure from the Deployment tab. **Architecture:** `CheckpointsTable` is rewritten to return a React.Fragment whose children become direct grid cells inside `IdentitySection`'s `.configGrid`. Label + trigger occupy one grid row; when opened, a second grid row spans both columns for the full-width table. `IdentitySection` gains a `checkpointsSlot` prop (distinct from `children`, which continues to host the portal-rendered drawer). The Deployed cell's ISO sub-line is replaced by `new Date(iso).toLocaleString()` inline. `HistoryDisclosure` is deleted — the checkpoints table + drawer now cover its job. **Tech Stack:** React 18 + TypeScript, CSS modules, Vitest + React Testing Library. **Spec:** `docs/superpowers/specs/2026-04-23-checkpoints-grid-row-design.md` --- ## Files touched (summary) | Path | Change | |------|--------| | `ui/src/pages/AppsTab/AppDeploymentPage/IdentitySection.tsx` | Add `checkpointsSlot?: ReactNode` prop; render inside `.configGrid` after the JAR row | | `ui/src/pages/AppsTab/AppDeploymentPage/CheckpointsTable.tsx` | Rewrite as React.Fragment (label span + trigger cell + optional full-width row); swap ISO sub-line for `toLocaleString()` | | `ui/src/pages/AppsTab/AppDeploymentPage/CheckpointsTable.test.tsx` | Update `expand()` helper (button name) + add locale sub-line assertion | | `ui/src/pages/AppsTab/AppDeploymentPage/AppDeploymentPage.module.css` | Add `.checkpointsTriggerCell`, `.checkpointsTrigger`, `.checkpointsTableFullRow`; remove obsolete classes | | `ui/src/pages/AppsTab/AppDeploymentPage/index.tsx` | Route `CheckpointsTable` via `checkpointsSlot`; 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** | --- ## Commands — run from `ui/` - **Test one file:** `npx vitest run ` - **All tests:** `npx vitest run` - **Typecheck:** `npm run typecheck` --- ## Task 1: `IdentitySection` accepts `checkpointsSlot` **Why:** Give the parent page a way to inject the checkpoints row *inside* the grid, separate from `children` (which still hosts the drawer portal). **Files:** - Modify: `ui/src/pages/AppsTab/AppDeploymentPage/IdentitySection.tsx` - [ ] **Step 1.1: Add the prop to the interface** Open `ui/src/pages/AppsTab/AppDeploymentPage/IdentitySection.tsx`. Find `IdentitySectionProps` (around line 18-29): ```tsx interface IdentitySectionProps { mode: 'net-new' | 'deployed'; environment: Environment; app: App | null; currentVersion: AppVersion | null; name: string; onNameChange: (next: string) => void; stagedJar: File | null; onStagedJarChange: (file: File | null) => void; deploying: boolean; children?: ReactNode; } ``` Add the `checkpointsSlot` field: ```tsx interface IdentitySectionProps { mode: 'net-new' | 'deployed'; environment: Environment; app: App | null; currentVersion: AppVersion | null; name: string; onNameChange: (next: string) => void; stagedJar: File | null; onStagedJarChange: (file: File | null) => void; deploying: boolean; checkpointsSlot?: ReactNode; children?: ReactNode; } ``` - [ ] **Step 1.2: Destructure `checkpointsSlot` + render inside the grid** Find the function signature: ```tsx export function IdentitySection({ mode, environment, app, currentVersion, name, onNameChange, stagedJar, onStagedJarChange, deploying, children, }: IdentitySectionProps) { ``` Add `checkpointsSlot` to the destructure list: ```tsx export function IdentitySection({ mode, environment, app, currentVersion, name, onNameChange, stagedJar, onStagedJarChange, deploying, checkpointsSlot, children, }: IdentitySectionProps) { ``` Then find the closing of the Application JAR row, which looks like: ```tsx Application JAR
onStagedJarChange(e.target.files?.[0] ?? null)} /> {stagedJar && ( staged: {stagedJar.name} ({formatBytes(stagedJar.size)}) )}
{children} ); } ``` Change the closing to render `checkpointsSlot` INSIDE the grid, just before `` that closes `.configGrid`: ```tsx Application JAR
onStagedJarChange(e.target.files?.[0] ?? null)} /> {stagedJar && ( staged: {stagedJar.name} ({formatBytes(stagedJar.size)}) )}
{checkpointsSlot} {children} ); } ``` `checkpointsSlot` is expected to be a React.Fragment whose inner children are grid-direct cells. - [ ] **Step 1.3: Typecheck** Run (from `ui/`): `npm run typecheck` Expected: PASS. (Call sites don't pass `checkpointsSlot` yet — that's fine because the prop is optional.) - [ ] **Step 1.4: Commit** ```bash git add ui/src/pages/AppsTab/AppDeploymentPage/IdentitySection.tsx git commit -m "feat(ui): IdentitySection accepts checkpointsSlot rendered inside configGrid" ``` --- ## Task 2: Rewrite `CheckpointsTable` as grid-fragment + locale sub-line **Why:** The table must emit grid-ready children (label + trigger cell + optional full-width row), drop its own wrapping `
`, and localize the Deployed sub-line. **Files:** - Modify: `ui/src/pages/AppsTab/AppDeploymentPage/CheckpointsTable.tsx` - [ ] **Step 2.1: Replace the full file contents** Replace the entire contents of `ui/src/pages/AppsTab/AppDeploymentPage/CheckpointsTable.tsx` with: ```tsx import { useState } from 'react'; import { Badge } from '@cameleer/design-system'; import type { Deployment, AppVersion } from '../../../api/queries/admin/apps'; import { timeAgo } from '../../../utils/format-utils'; import styles from './AppDeploymentPage.module.css'; const FALLBACK_CAP = 10; interface CheckpointsTableProps { deployments: Deployment[]; versions: AppVersion[]; currentDeploymentId: string | null; jarRetentionCount: number | null; onSelect: (deploymentId: string) => void; } export function CheckpointsTable({ deployments, versions, currentDeploymentId, jarRetentionCount, onSelect, }: CheckpointsTableProps) { const [expanded, setExpanded] = useState(false); const [open, setOpen] = useState(false); const versionMap = new Map(versions.map((v) => [v.id, v])); const checkpoints = deployments .filter((d) => d.deployedConfigSnapshot && d.id !== currentDeploymentId) .sort((a, b) => (b.deployedAt ?? '').localeCompare(a.deployedAt ?? '')); if (checkpoints.length === 0) { return null; } const cap = jarRetentionCount && jarRetentionCount > 0 ? jarRetentionCount : FALLBACK_CAP; const visible = expanded ? checkpoints : checkpoints.slice(0, cap); const hidden = checkpoints.length - visible.length; return ( <> Checkpoints
{open && (
{visible.map((d) => { const v = versionMap.get(d.appVersionId); const archived = !v; const strategyLabel = d.deploymentStrategy === 'BLUE_GREEN' ? 'blue/green' : 'rolling'; return ( onSelect(d.id)} > ); })}
Version JAR Deployed by Deployed Strategy Outcome
{v ? ( {v.jarFilename} ) : ( <> JAR pruned
archived — JAR pruned
)}
{d.createdBy ?? } {d.deployedAt && timeAgo(d.deployedAt)}
{d.deployedAt && new Date(d.deployedAt).toLocaleString()}
{strategyLabel} {d.status}
{hidden > 0 && !expanded && ( )}
)} ); } ``` Key points: - Returns `<>...` (React.Fragment). Its children become direct children of the parent `.configGrid`. - Trigger button text: `Expand (N)` when closed, `Collapse (N)` when open. - `.checkpointsTableFullRow` wraps the inner `.checkpointsTable` so the full-width row owns the grid span. - The Deployed sub-line uses `new Date(d.deployedAt).toLocaleString()` instead of the raw ISO. - The "Show older" button sits inside the full-width row (unchanged behavior). - [ ] **Step 2.2: Typecheck** Run: `npm run typecheck` Expected: PASS (no callers pass new props; the component's signature is unchanged). Note: tests will now FAIL until Task 3 updates them — this is expected. Do not run tests here. - [ ] **Step 2.3: Commit** ```bash git add ui/src/pages/AppsTab/AppDeploymentPage/CheckpointsTable.tsx git commit -m "feat(ui): CheckpointsTable emits grid fragment + locale sub-line" ``` --- ## Task 3: Update `CheckpointsTable.test.tsx` **Why:** The trigger button's accessible name changed from `/checkpoints \(1\)/i` to `/expand|collapse/i`. Also add a locale sub-line assertion. **Files:** - Modify: `ui/src/pages/AppsTab/AppDeploymentPage/CheckpointsTable.test.tsx` - [ ] **Step 3.1: Replace the full test file** Replace the entire contents of `ui/src/pages/AppsTab/AppDeploymentPage/CheckpointsTable.test.tsx` with: ```tsx import { describe, it, expect, vi } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/react'; import { ThemeProvider } from '@cameleer/design-system'; import type { ReactNode } from 'react'; import { CheckpointsTable } from './CheckpointsTable'; import type { Deployment, AppVersion } from '../../../api/queries/admin/apps'; function wrap(ui: ReactNode) { return render({ui}); } const v6: AppVersion = { id: 'v6id', appId: 'a', version: 6, jarPath: '/j', jarChecksum: 'c', jarFilename: 'my-app-1.2.3.jar', jarSizeBytes: 1, detectedRuntimeType: null, detectedMainClass: null, uploadedAt: '2026-04-23T10:00:00Z', }; const stoppedDep: Deployment = { id: 'd1', appId: 'a', appVersionId: 'v6id', environmentId: 'e', status: 'STOPPED', targetState: 'STOPPED', deploymentStrategy: 'BLUE_GREEN', replicaStates: [{ index: 0, containerId: 'c', containerName: 'n', status: 'STOPPED' }], deployStage: null, containerId: null, containerName: null, errorMessage: null, deployedAt: '2026-04-23T10:35:00Z', stoppedAt: '2026-04-23T10:52:00Z', createdAt: '2026-04-23T10:35:00Z', createdBy: 'alice', deployedConfigSnapshot: { jarVersionId: 'v6id', agentConfig: null, containerConfig: {}, sensitiveKeys: null }, }; function expand() { fireEvent.click(screen.getByRole('button', { name: /expand|collapse/i })); } describe('CheckpointsTable', () => { it('defaults to collapsed — label + trigger visible, rows hidden', () => { wrap( {}} />); expect(screen.getByText('Checkpoints')).toBeInTheDocument(); expect(screen.getByRole('button', { name: /expand \(1\)/i })).toBeInTheDocument(); expect(screen.queryByText('v6')).toBeNull(); expect(screen.queryByText('my-app-1.2.3.jar')).toBeNull(); }); it('clicking trigger expands to show rows; label flips to Collapse', () => { wrap( {}} />); expand(); expect(screen.getByText('v6')).toBeInTheDocument(); expect(screen.getByText('my-app-1.2.3.jar')).toBeInTheDocument(); expect(screen.getByText('alice')).toBeInTheDocument(); expect(screen.getByRole('button', { name: /collapse \(1\)/i })).toBeInTheDocument(); }); it('row click invokes onSelect with deploymentId', () => { const onSelect = vi.fn(); wrap(); expand(); fireEvent.click(screen.getByText('v6').closest('tr')!); expect(onSelect).toHaveBeenCalledWith('d1'); }); it('renders em-dash for null createdBy', () => { const noActor = { ...stoppedDep, createdBy: null }; wrap( {}} />); expand(); expect(screen.getByText('—')).toBeInTheDocument(); }); it('marks pruned-JAR rows as archived', () => { const pruned = { ...stoppedDep, appVersionId: 'unknown' }; wrap( {}} />); expand(); expect(screen.getByText(/archived/i)).toBeInTheDocument(); }); it('renders nothing when there are no checkpoints', () => { const { container } = wrap( {}} />); expect(container).toBeEmptyDOMElement(); }); it('caps visible rows at jarRetentionCount and shows expander', () => { const many: Deployment[] = Array.from({ length: 10 }, (_, i) => ({ ...stoppedDep, id: `d${i}`, deployedAt: `2026-04-23T10:${String(10 + i).padStart(2, '0')}:00Z`, })); wrap( {}} />); expand(); expect(screen.getAllByRole('row').length).toBeLessThanOrEqual(4); expect(screen.getByText(/show older \(7\)/i)).toBeInTheDocument(); }); it('shows all rows when jarRetentionCount >= total', () => { wrap( {}} />); expand(); expect(screen.queryByText(/show older/i)).toBeNull(); }); it('falls back to default cap of 10 when jarRetentionCount is 0 or null', () => { const fifteen: Deployment[] = Array.from({ length: 15 }, (_, i) => ({ ...stoppedDep, id: `d${i}`, deployedAt: `2026-04-23T10:${String(10 + i).padStart(2, '0')}:00Z`, })); wrap( {}} />); expand(); expect(screen.getByText(/show older \(5\)/i)).toBeInTheDocument(); }); it('Deployed sub-line is locale-formatted (not the raw ISO)', () => { wrap( {}} />); expand(); // Raw ISO would contain 'T' and end in 'Z' — localized form must not. const raw = '2026-04-23T10:35:00Z'; expect(screen.queryByText(raw)).toBeNull(); // The actual sub-line must exist as SOME locale-rendered string near the row. // Use the createdBy cell as an anchor and walk to the sibling Deployed cell. const row = screen.getByText('alice').closest('tr')!; const cells = row.querySelectorAll('td'); // Column order: Version, JAR, Deployed by, Deployed, Strategy, Outcome, chevron const deployedCell = cells[3]; expect(deployedCell).toBeDefined(); const text = deployedCell.textContent ?? ''; expect(text).not.toContain('T10:35'); expect(text).not.toContain('Z'); expect(text.length).toBeGreaterThan(0); }); }); ``` Key changes: - `expand()` now targets `/expand|collapse/i`. - The default-collapsed test checks for `/expand \(1\)/i` (trigger shows `Expand (1)` when closed). - A new post-expand assertion on `/collapse \(1\)/i` confirms the label flip. - One new test: `Deployed sub-line is locale-formatted (not the raw ISO)` — asserts neither `'T10:35'` nor `'Z'` appears in the Deployed cell's text content, and the cell has content. This avoids depending on the CI's locale while still proving the raw-ISO path is gone. - [ ] **Step 3.2: Run tests — expect pass** Run: `npx vitest run src/pages/AppsTab/AppDeploymentPage/CheckpointsTable.test.tsx` Expected: PASS (10 tests). If a test fails because the `Deployed sub-line is locale-formatted` anchor strategy doesn't find `alice` (e.g. because the parent still wrapped this in a plain `
` somewhere), STOP and report — the Task 2 rewrite is wrong. - [ ] **Step 3.3: Commit** ```bash git add ui/src/pages/AppsTab/AppDeploymentPage/CheckpointsTable.test.tsx git commit -m "test(ui): CheckpointsTable covers fragment layout + locale sub-line" ``` --- ## Task 4: CSS updates — add new classes, remove obsolete ones **Why:** Support the in-grid layout; clean up dead CSS from the retired row-list view and `HistoryDisclosure`. **Files:** - Modify: `ui/src/pages/AppsTab/AppDeploymentPage/AppDeploymentPage.module.css` - [ ] **Step 4.1: Add the three new classes** Open `ui/src/pages/AppsTab/AppDeploymentPage/AppDeploymentPage.module.css`. Append at the end of the file (after all existing rules): ```css /* Checkpoints row — lives inside .configGrid */ .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; } ``` - [ ] **Step 4.2: Remove the obsolete `.checkpointsSection` / `.checkpointsHeader` / `.checkpointsCount` block** Find and delete this block (currently near the bottom, added in a prior task): ```css /* Collapsible Checkpoints header */ .checkpointsSection { display: flex; flex-direction: column; } .checkpointsHeader { display: inline-flex; align-items: center; gap: 6px; background: none; border: none; padding: 8px 0; font-size: 13px; font-weight: 600; color: var(--text-primary); cursor: pointer; text-align: left; } .checkpointsHeader:hover { color: var(--amber); } .checkpointsChevron { color: var(--text-muted); font-size: 11px; width: 12px; text-align: center; } .checkpointsCount { color: var(--text-muted); font-weight: 400; } ``` **Important:** After deleting that block, re-add `.checkpointsChevron` (still used by the trigger button) as a standalone rule at the end of the file: ```css .checkpointsChevron { color: var(--text-muted); font-size: 11px; width: 12px; text-align: center; } ``` - [ ] **Step 4.3: Remove retired row-list + history classes** Still in the same file, find and delete these rules (left over from the retired `Checkpoints.tsx` row-list and the soon-to-be-deleted `HistoryDisclosure`): ```css .checkpointsRow { margin-top: 8px; } .disclosureToggle { background: none; border: none; color: var(--text-muted); cursor: pointer; font-size: 13px; padding: 4px 0; } .checkpointList { display: flex; flex-direction: column; gap: 4px; padding: 6px 0 0 12px; } .checkpointRow { display: flex; align-items: center; gap: 10px; font-size: 13px; } .checkpointMeta { color: var(--text-muted); } .checkpointArchived { color: var(--warning); font-size: 12px; } ``` **Do NOT delete** `.checkpointsTable tr.checkpointArchived { opacity: 0.55; }` — that variant IS still used by the table body. Also find and delete: ```css /* HistoryDisclosure */ .historyRow { margin-top: 16px; } ``` - [ ] **Step 4.4: Typecheck** Run: `npm run typecheck` Expected: PASS. - [ ] **Step 4.5: Run table tests to confirm no missing-class regressions** Run: `npx vitest run src/pages/AppsTab/AppDeploymentPage/CheckpointsTable.test.tsx` Expected: PASS (10 tests). - [ ] **Step 4.6: Commit** ```bash git add ui/src/pages/AppsTab/AppDeploymentPage/AppDeploymentPage.module.css git commit -m "refactor(ui): checkpoints in-grid styles + drop retired row-list/history CSS" ``` --- ## Task 5: Wire `CheckpointsTable` through `checkpointsSlot` in the page **Why:** Move the render from `children` into the new slot so the table ends up inside the grid; keep the drawer in `children`. **Files:** - Modify: `ui/src/pages/AppsTab/AppDeploymentPage/index.tsx` - [ ] **Step 5.1: Update the `` render** Open `ui/src/pages/AppsTab/AppDeploymentPage/index.tsx`. Find the `` JSX (around lines 437-474): ```tsx {app && ( <> {selectedDep && ( setSelectedCheckpointId(null)} deployment={selectedDep} version={selectedDepVersion} appSlug={app.slug} envSlug={selectedEnv ?? ''} currentForm={form} onRestore={(deploymentId) => { handleRestore(deploymentId); setSelectedCheckpointId(null); }} /> )} )} ``` Replace with: ```tsx ) : undefined } > {app && selectedDep && ( setSelectedCheckpointId(null)} deployment={selectedDep} version={selectedDepVersion} appSlug={app.slug} envSlug={selectedEnv ?? ''} currentForm={form} onRestore={(deploymentId) => { handleRestore(deploymentId); setSelectedCheckpointId(null); }} /> )} ``` Key difference: `` moves into the new `checkpointsSlot` prop; the drawer stays as the only `children`. - [ ] **Step 5.2: Typecheck** Run: `npm run typecheck` Expected: PASS. - [ ] **Step 5.3: Run all deployment page tests** Run: `npx vitest run src/pages/AppsTab/AppDeploymentPage` Expected: PASS. - [ ] **Step 5.4: Commit** ```bash git add ui/src/pages/AppsTab/AppDeploymentPage/index.tsx git commit -m "refactor(ui): route CheckpointsTable via IdentitySection.checkpointsSlot" ``` --- ## Task 6: Remove `HistoryDisclosure` from the Deployment tab **Why:** The checkpoints table + drawer now cover the same information; History is dead weight. **Files:** - Modify: `ui/src/pages/AppsTab/AppDeploymentPage/DeploymentTab/DeploymentTab.tsx` - Delete: `ui/src/pages/AppsTab/AppDeploymentPage/DeploymentTab/HistoryDisclosure.tsx` - [ ] **Step 6.1: Remove the import and render from `DeploymentTab.tsx`** Open `ui/src/pages/AppsTab/AppDeploymentPage/DeploymentTab/DeploymentTab.tsx`. It currently looks like: ```tsx import type { Deployment, AppVersion } from '../../../../api/queries/admin/apps'; import { DeploymentProgress } from '../../../../components/DeploymentProgress'; import { StartupLogPanel } from '../../../../components/StartupLogPanel'; import { EmptyState } from '@cameleer/design-system'; import { StatusCard } from './StatusCard'; import { HistoryDisclosure } from './HistoryDisclosure'; import styles from '../AppDeploymentPage.module.css'; interface Props { deployments: Deployment[]; versions: AppVersion[]; appSlug: string; envSlug: string; externalUrl: string; } export function DeploymentTab({ deployments, versions, appSlug, envSlug, externalUrl }: Props) { const latest = deployments .slice() .sort((a, b) => (b.createdAt ?? '').localeCompare(a.createdAt ?? ''))[0] ?? null; if (!latest) { return ; } const version = versions.find((v) => v.id === latest.appVersionId) ?? null; return (
{latest.status === 'STARTING' && ( )} {latest.status === 'FAILED' && ( )}
); } ``` Replace with: ```tsx import type { Deployment, AppVersion } from '../../../../api/queries/admin/apps'; import { DeploymentProgress } from '../../../../components/DeploymentProgress'; import { StartupLogPanel } from '../../../../components/StartupLogPanel'; import { EmptyState } from '@cameleer/design-system'; import { StatusCard } from './StatusCard'; import styles from '../AppDeploymentPage.module.css'; interface Props { deployments: Deployment[]; versions: AppVersion[]; appSlug: string; envSlug: string; externalUrl: string; } export function DeploymentTab({ deployments, versions, appSlug, envSlug, externalUrl }: Props) { const latest = deployments .slice() .sort((a, b) => (b.createdAt ?? '').localeCompare(a.createdAt ?? ''))[0] ?? null; if (!latest) { return ; } const version = versions.find((v) => v.id === latest.appVersionId) ?? null; return (
{latest.status === 'STARTING' && ( )} {latest.status === 'FAILED' && ( )}
); } ``` Changes: remove the `import { HistoryDisclosure }` line and the `` JSX render at the bottom. Note that `versions` is still used (line with `const version = ...`), so keep the `versions` prop in the destructure. - [ ] **Step 6.2: Delete `HistoryDisclosure.tsx`** Delete the file: ```bash rm ui/src/pages/AppsTab/AppDeploymentPage/DeploymentTab/HistoryDisclosure.tsx ``` - [ ] **Step 6.3: Typecheck** Run: `npm run typecheck` Expected: PASS. - [ ] **Step 6.4: Run deployment page tests** Run: `npx vitest run src/pages/AppsTab/AppDeploymentPage` Expected: PASS. - [ ] **Step 6.5: Commit** ```bash git add ui/src/pages/AppsTab/AppDeploymentPage/DeploymentTab/DeploymentTab.tsx \ ui/src/pages/AppsTab/AppDeploymentPage/DeploymentTab/HistoryDisclosure.tsx git commit -m "ui(deploy): remove redundant HistoryDisclosure from Deployment tab" ``` --- ## Task 7: Final verification + rules + reindex **Why:** Confirm the full suite is green; update `.claude/rules/ui.md` so future sessions know Checkpoints lives inside the Identity grid and History is gone. **Files:** - Modify: `.claude/rules/ui.md` - [ ] **Step 7.1: Run the entire UI test suite** Run (from `ui/`): `npx vitest run` Expected: PASS. - [ ] **Step 7.2: Typecheck** Run: `npm run typecheck` Expected: PASS. - [ ] **Step 7.3: Update `.claude/rules/ui.md`** Open `.claude/rules/ui.md` and locate the Deployments bullet that begins "Checkpoints render as a collapsible `CheckpointsTable`…". Replace the full sentence with: > "Checkpoints render as a collapsible `CheckpointsTable` (default **collapsed**) **inside the Identity & Artifact `configGrid`** as an in-grid row (`Checkpoints | ▸ Expand (N)`). When expanded, the table spans both grid columns via `grid-column: 1 / -1`. Columns: Version · JAR (filename) · Deployed by · Deployed (relative + user-locale sub-line via `toLocaleString()`) · Strategy · Outcome · ›. Row click opens `CheckpointDetailDrawer`. Drawer tabs are ordered **Config | Logs** with `Config` as the default. Replica filter uses DS `Select`. Restore lives in the drawer footer. Visible row cap = `Environment.jarRetentionCount` (default 10 if 0/null); older rows accessible via 'Show older (N)' expander. Currently-running deployment is excluded — represented separately by `StatusCard`. The empty-checkpoints case returns `null` (no row)." Then locate the Deployment-tab bullet that starts "Deployment tab: `StatusCard` + `DeploymentProgress` … + `HistoryDisclosure`." and replace with: > "Deployment tab: `StatusCard` + `DeploymentProgress` (during STARTING / FAILED) + flex-grow `StartupLogPanel` (no fixed maxHeight). Auto-activates when a deploy starts. `HistoryDisclosure` was retired — per-deployment config and logs live in the Checkpoints drawer. `StartupLogPanel` header mirrors the Runtime Application Log pattern: title + live/stopped badge + `N entries` + sort toggle (↑/↓, default **desc**) + refresh icon (`RefreshCw`). Sort drives the backend fetch via `useStartupLogs(…, sort)`. Refresh scrolls to the latest edge (top for desc, bottom for asc). Sort + refresh buttons disable while a refetch is in flight." - [ ] **Step 7.4: Commit rules update** ```bash git add .claude/rules/ui.md git commit -m "docs(rules): checkpoints live inside Identity grid; HistoryDisclosure retired" ``` - [ ] **Step 7.5: Run GitNexus re-index** Run (from repo root): `npx gitnexus analyze --embeddings` Expected: Indexed successfully. - [ ] **Step 7.6: Manual smoke (human-run, report in PR body)** Start `npm run dev` in `ui/` (backend on :8081). Verify: - Identity & Artifact section shows `Checkpoints | ▸ Expand (N)` as the last row. Collapsed on load. - Clicking the trigger reveals the full table spanning both grid columns; button flips to `▾ Collapse (N)`. - Deployed column shows `5h ago` as primary; sub-line shows a localized date/time (e.g. `4/23/2026, 12:35:00 PM` in `en-US`). - Click a row → `CheckpointDetailDrawer` opens on Config tab (unchanged). - App with no checkpoints: no Checkpoints row at all. - Deployment tab no longer shows the History disclosure below Startup Logs. --- ## Self-review **Spec coverage:** - §2.1 Checkpoints row in the grid → Tasks 1, 2, 4, 5 ✓ - §2.2 Deployed locale sub-line → Task 2 (component change) + Task 3 (test) ✓ - §2.3 Remove History → Task 6 ✓ - §3 Page wiring (split slot from children) → Task 5 ✓ - §4 Files list — every file mentioned has a touch task ✓ - §5 Testing — Task 3 updates 9 existing tests + adds the locale assertion ✓ - §6 Non-goals respected: `CheckpointDetailDrawer`, `timeAgo`, drawer layout, other callers of `timeAgo` are untouched ✓ **Placeholder scan:** No "TBD"/"TODO"/"similar to Task N"/"add appropriate X". Every step has either a concrete code block or an exact shell command. Step 3.2's failure path names what would be wrong and says to STOP and report — concrete guidance, not a placeholder. **Type consistency:** - `checkpointsSlot?: ReactNode` appears consistently in Task 1 (prop), Task 5 (caller). - `.checkpointsChevron` is removed as part of a block in Task 4.2 and re-added standalone in the same step — explicit handling, no dangling references. `CheckpointsTable` in Task 2 references it. - `.checkpointsTriggerCell`, `.checkpointsTrigger`, `.checkpointsTableFullRow` are defined once in Task 4.1 and referenced in Task 2. - `new Date(iso).toLocaleString()` pattern appears once in Task 2 and is asserted in Task 3. No gaps or inconsistencies.