feat: rebuild warm operations workbench
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
# Compact Fleet Cards 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:** Remove the Fleet aggregate health strip, restore the upstream SimAdmin version on every card, and change the responsive card matrix to 1/2/2/4/5 columns.
|
||||
|
||||
**Architecture:** Keep the existing API and Fleet view-model pipeline because `/api/v1/instances/:id/resources` already supplies `version`. Make the presentation fix in `FleetPage`, remove the now-unused aggregate calculation, and update only the final Fleet CSS cascade and browser geometry assertions.
|
||||
|
||||
**Tech Stack:** React 19, TypeScript, animal-island-ui, Vitest, Testing Library, Vite, real Chrome DevTools Protocol E2E.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Keep overview and search in the desktop left sidebar.
|
||||
- Keep per-instance CPU, memory, temperature, phone, SMS, status, tags, and operation menu.
|
||||
- Render `SimAdmin <version>` when available and `版本未知` when absent.
|
||||
- Use exactly 1/2/2/4/5 columns at 390/768/1024/1440/1920 pixels.
|
||||
- Do not change API contracts, database code, instance-detail telemetry, or unrelated screens.
|
||||
- Preserve the current cream, warm-brown, mint, shadowless card treatment.
|
||||
- The shared worktree contains unrelated changes; do not create implementation commits unless explicitly requested.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Lock The Fleet Content Contract
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/web/src/fleet/fleet-page.test.tsx`
|
||||
- Modify: `apps/web/src/app-shell.integration.test.tsx`
|
||||
- Test: `apps/web/src/fleet/fleet-page.test.tsx`
|
||||
- Test: `apps/web/src/app-shell.integration.test.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: `FleetSnapshot.statuses[*].summary.version` and the accessible Fleet card/region names.
|
||||
- Produces: regression coverage for version rendering, missing-version fallback, and aggregate-strip removal.
|
||||
|
||||
- [ ] **Step 1: Add the upstream version to the Fleet fixture and assert visible card copy**
|
||||
|
||||
```tsx
|
||||
summary: {
|
||||
version: '1.1.6',
|
||||
freshness: 'fresh',
|
||||
resources: { cpuPercent: 24, memoryPercent: 51, maxTemperatureCelsius: 42 },
|
||||
},
|
||||
|
||||
expect(within(card).getByText('SimAdmin 1.1.6')).toBeTruthy();
|
||||
expect(screen.queryByRole('region', { name: '节点资源健康' })).toBeNull();
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add a missing-version fallback assertion using a separate card fixture**
|
||||
|
||||
```tsx
|
||||
const withoutVersion: FleetSnapshot = {
|
||||
...snapshot,
|
||||
statuses: new Map([
|
||||
['alpha', { ...snapshot.statuses.get('alpha')!, summary: { resources: { cpuPercent: 24 } } }],
|
||||
]),
|
||||
};
|
||||
render(<FleetPage initialData={withoutVersion} />);
|
||||
expect(screen.getByText('版本未知')).toBeTruthy();
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update AppShell integration expectations**
|
||||
|
||||
Assert `SimAdmin 2.0` is present in the Bravo card and `节点资源健康` is absent. Remove the old expectation that intentionally hid the version text.
|
||||
|
||||
- [ ] **Step 4: Run the focused tests and verify RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
corepack pnpm --filter @multi-simadmin/web exec vitest run src/fleet/fleet-page.test.tsx src/app-shell.integration.test.tsx
|
||||
```
|
||||
|
||||
Expected: FAIL because cards do not render `row.version`, the missing-version fallback is absent, and the aggregate region still exists.
|
||||
|
||||
### Task 2: Implement Compact Fleet Content
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/web/src/fleet/fleet-page.tsx`
|
||||
- Test: `apps/web/src/fleet/fleet-page.test.tsx`
|
||||
- Test: `apps/web/src/app-shell.integration.test.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: existing `FleetRow.version: string | null` from `buildFleetTableViewModel`.
|
||||
- Produces: `.fleet-card-version` identity text and no `节点资源健康` region.
|
||||
|
||||
- [ ] **Step 1: Remove the aggregate-only calculation and formatting helpers**
|
||||
|
||||
Delete `average`, `healthPercent`, `healthRate`, `healthTemperature`, the `fleetHealth` memo, and the complete `.fleet-health-strip` JSX block. Keep `fleetSummary`, which owns the left-sidebar totals.
|
||||
|
||||
- [ ] **Step 2: Render the real version under the card name**
|
||||
|
||||
```tsx
|
||||
<span className="fleet-card-version">{row.version ? `SimAdmin ${row.version}` : '版本未知'}</span>
|
||||
```
|
||||
|
||||
Place it inside `.fleet-card-entry` after the `<h2>` and before the dashboard affordance so it remains part of the card identity link.
|
||||
|
||||
- [ ] **Step 3: Run the focused tests and verify GREEN**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
corepack pnpm --filter @multi-simadmin/web exec vitest run src/fleet/fleet-page.test.tsx src/app-shell.integration.test.tsx
|
||||
```
|
||||
|
||||
Expected: both test files pass with no React warnings.
|
||||
|
||||
### Task 3: Lock And Implement Narrow Responsive Geometry
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `scripts/real-browser-e2e.mjs`
|
||||
- Modify: `apps/web/src/styles.css`
|
||||
- Test: `scripts/real-browser-e2e.mjs`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: `.fleet-card-grid`, `.fleet-card-version`, `.fleet-group-tabs`, `.fleet-sidebar`, and `.fleet-card` DOM selectors.
|
||||
- Produces: deterministic 1/2/2/4/5 responsive geometry and screenshots.
|
||||
|
||||
- [ ] **Step 1: Change the E2E viewport contract before CSS**
|
||||
|
||||
```js
|
||||
{ width: 390, height: 844, columns: 1, sidebarMode: 'stacked', mobile: true },
|
||||
{ width: 768, height: 900, columns: 2, sidebarMode: 'stacked', mobile: false },
|
||||
{ width: 1024, height: 900, columns: 2, sidebarMode: 'left', mobile: false },
|
||||
{ width: 1440, height: 1000, columns: 4, sidebarMode: 'left', mobile: false },
|
||||
{ width: 1920, height: 1080, columns: 5, sidebarMode: 'left', mobile: false },
|
||||
```
|
||||
|
||||
Remove health-strip selectors and assertions. Add `.fleet-card-version` bounds and text-presence checks to the existing layout snapshot.
|
||||
|
||||
- [ ] **Step 2: Run browser E2E and verify RED**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
$env:E2E_SCREENSHOT_DIR='C:\Users\86135\Downloads\multi-simadmin\artifacts\compact-fleet-cards'
|
||||
corepack pnpm run test:e2e:browser
|
||||
```
|
||||
|
||||
Expected: FAIL at the 1440px column count because current CSS renders 3 columns.
|
||||
|
||||
- [ ] **Step 3: Update the final Fleet cascade**
|
||||
|
||||
Set the default desktop `.fleet-card-grid` to four columns, change the wide breakpoint to five columns, preserve two columns through 1024px, and preserve one column at 390px. Remove the unused `.fleet-health-strip` rules. Add compact `.fleet-card-version` typography with ellipsis protection and no new card shadow.
|
||||
|
||||
- [ ] **Step 4: Run browser E2E and verify GREEN**
|
||||
|
||||
Run the same command from Step 2.
|
||||
|
||||
Expected: PASS at all five viewport widths with no overflow, clipped menus, wrapped hardware values, or moved desktop sidebar.
|
||||
|
||||
### Task 4: Full Verification And Local Handoff
|
||||
|
||||
**Files:**
|
||||
|
||||
- Verify: `apps/web/src/fleet/fleet-page.tsx`
|
||||
- Verify: `apps/web/src/styles.css`
|
||||
- Verify: `scripts/real-browser-e2e.mjs`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: the completed Fleet UI and existing local canary gateway on port 8789.
|
||||
- Produces: fresh automated evidence, reviewed screenshots, and an accessible local build.
|
||||
|
||||
- [ ] **Step 1: Run the full Web quality gates**
|
||||
|
||||
```powershell
|
||||
corepack pnpm --filter @multi-simadmin/web test
|
||||
corepack pnpm --filter @multi-simadmin/web typecheck
|
||||
corepack pnpm lint
|
||||
corepack pnpm format:check
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Inspect generated screenshots**
|
||||
|
||||
Review 390px, 1440px, and 1920px captures. Confirm compact cards, readable versions, no aggregate strip, no overlap, and no excessive horizontal gutters.
|
||||
|
||||
- [ ] **Step 3: Rebuild after E2E cleanup and verify local responses**
|
||||
|
||||
```powershell
|
||||
corepack pnpm --filter @multi-simadmin/web build
|
||||
Invoke-WebRequest -UseBasicParsing http://127.0.0.1:8789/
|
||||
Invoke-WebRequest -UseBasicParsing http://127.0.0.1:8789/api/v1/instances
|
||||
```
|
||||
|
||||
Expected: both URLs return HTTP 200 and the UI is available at `http://127.0.0.1:8789/`.
|
||||
@@ -0,0 +1,121 @@
|
||||
# Fluid Wide Workbench 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:** Remove fixed-width outer whitespace from all top-level workspaces and render four Fleet columns at 1920px without changing existing narrower layouts.
|
||||
|
||||
**Architecture:** Consolidate top-level width ownership in the canonical `.app-layout` rule and delete obsolete width constraints from later visual experiments. Extend the existing real-Chrome acceptance test to measure the application container on Nodes, Automation, and Settings at 1920px, while leaving focused-content limits untouched.
|
||||
|
||||
**Tech Stack:** CSS Grid, React 19, Vite, Chrome DevTools Protocol E2E.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Nodes, Automation, and Settings use a fluid top-level container with 16px to 24px desktop gutters.
|
||||
- Fleet remains one column at 390px, two at 768px and 1024px, three at 1440px, and four at 1920px.
|
||||
- The 17rem Fleet sidebar and its responsive stacking behavior remain unchanged.
|
||||
- Forms, dialogs, drawers, message bubbles, and empty states retain their existing content-specific width limits.
|
||||
- Do not add dependencies or change application behavior.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Lock and implement fluid wide-screen geometry
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `scripts/real-browser-e2e.mjs:384`
|
||||
- Modify: `apps/web/src/styles.css:1946`
|
||||
- Modify: `apps/web/src/styles.css:2917`
|
||||
- Modify: `apps/web/src/styles.css:3238`
|
||||
- Modify: `apps/web/src/styles.css:4178`
|
||||
- Test: `scripts/real-browser-e2e.mjs`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: `.app-layout`, `.app-layout-single`, `.fleet-card-grid`, and the existing Chrome viewport matrix.
|
||||
- Produces: `pageLayout(cdp)` geometry for reusable outer-gutter assertions and a 1920px four-column Fleet layout.
|
||||
|
||||
- [x] **Step 1: Extend the browser geometry probe**
|
||||
|
||||
Add a helper that returns the viewport width and `.app-layout` bounds:
|
||||
|
||||
```js
|
||||
async function pageLayout(cdp) {
|
||||
return evaluate(
|
||||
cdp,
|
||||
`(() => { const layout = document.querySelector('.app-layout'); if (!layout) return null; const rect = layout.getBoundingClientRect(); return { viewport: innerWidth, left: rect.left, right: rect.right, width: rect.width }; })()`,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Add an assertion helper that requires both outer gaps to be at least 16px and no more than 24px:
|
||||
|
||||
```js
|
||||
async function assertWidePageGutters(cdp, label) {
|
||||
const layout = await pageLayout(cdp);
|
||||
assert.ok(layout, `${label} application layout must render`);
|
||||
const rightGap = layout.viewport - layout.right;
|
||||
assert.equal(
|
||||
layout.left >= 16 && layout.left <= 24 && rightGap >= 16 && rightGap <= 24,
|
||||
true,
|
||||
`${label} must keep 16px to 24px outer gutters: ${JSON.stringify(layout)}`,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **Step 2: Add 1920px acceptance coverage**
|
||||
|
||||
Add `{ width: 1920, height: 1080, columns: 4, sidebarMode: 'left', mobile: false }` to `viewportCases`. After the Fleet interaction checks, assert wide gutters for Nodes, navigate to Settings and assert them there, then navigate to Automation and assert them there before the existing drawer flow changes the viewport to 390px.
|
||||
|
||||
- [x] **Step 3: Run RED verification**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
corepack pnpm --filter @multi-simadmin/web build
|
||||
node scripts/real-browser-e2e.mjs --clean-dist
|
||||
```
|
||||
|
||||
Expected: FAIL at 1920px because Fleet still computes three columns or because `.app-layout` leaves approximately 224px on each side.
|
||||
|
||||
- [x] **Step 4: Consolidate top-level container ownership**
|
||||
|
||||
Make the canonical rule cover both layout variants:
|
||||
|
||||
```css
|
||||
.app-layout,
|
||||
.app-layout.app-layout-single {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
padding: clamp(0.85rem, 1.35vw, 1.45rem) clamp(0.8rem, 1.6vw, 1.75rem);
|
||||
}
|
||||
```
|
||||
|
||||
Remove top-level `width: min(100%, 92rem)`, `max-width: 92rem`, `width: min(100%, 106rem)`, and centering declarations from the obsolete `.app-layout` experiment blocks. Keep their unrelated padding and minimum-height declarations intact.
|
||||
|
||||
Add the wide Fleet breakpoint after the base grid rule:
|
||||
|
||||
```css
|
||||
@media (min-width: 112rem) {
|
||||
.fleet-card-grid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **Step 5: Run GREEN verification**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
corepack pnpm --filter @multi-simadmin/web test
|
||||
corepack pnpm --filter @multi-simadmin/web typecheck
|
||||
corepack pnpm --filter @multi-simadmin/web build
|
||||
node scripts/real-browser-e2e.mjs --clean-dist
|
||||
corepack pnpm lint
|
||||
```
|
||||
|
||||
Expected: Web tests, typecheck, build, lint, and real Chrome E2E pass. The browser test reports correct 390/768/1024/1440/1920 layouts without horizontal overflow.
|
||||
|
||||
- [x] **Step 6: Inspect wide-screen screenshots and restore the local build**
|
||||
|
||||
Run E2E with `E2E_SCREENSHOT_DIR=artifacts/fluid-wide-review`, inspect the 1440px and 1920px Fleet screenshots, then rebuild without `--clean-dist`. Verify `http://127.0.0.1:8789/` and `/api/v1/automation/schedules` return 200 and that the served CSS contains `repeat(4,minmax(0,1fr))`.
|
||||
@@ -0,0 +1,98 @@
|
||||
# Komari-Inspired Fleet Workbench 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:** Improve Fleet density and scanability with a five-metric resource strip, tag groups, and a flatter node-card hierarchy while preserving the approved left sidebar and fluid responsive geometry.
|
||||
|
||||
**Architecture:** Extend `FleetPage` using values already present in `FleetSnapshot` and `messageStates`, then keep all new visual ownership in the existing final Fleet cascade section. Existing search, advanced tag filtering, operations, and responsive grid contracts remain the source of behavior.
|
||||
|
||||
**Tech Stack:** React 19, TypeScript, animal-island-ui, CSS Grid, Vitest, Testing Library, Chrome DevTools Protocol E2E.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Keep overview and search in the desktop left sidebar.
|
||||
- Keep 16px to 24px outer gutters and the existing 390 / 768 / 1024 / 1440 / 1920 responsive matrix.
|
||||
- Use only existing snapshot and message-summary data; render missing measurements as `--`.
|
||||
- Do not add dependencies, backgrounds, glass cards, diagonal stripes, fixed footers, or another theme override block.
|
||||
- Keep checkboxes hidden outside batch mode and position them beside card status in batch mode.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Lock Fleet information architecture in component tests
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/web/src/fleet/fleet-page.test.tsx`
|
||||
- Modify: `apps/web/src/fleet/fleet-page.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: `FleetSnapshot`, `FleetMessageLoadState`, existing `tag` filter state, and `Progress`.
|
||||
- Produces: `.fleet-health-strip`, `.fleet-group-tabs`, `.fleet-card-facts`, `.fleet-card-resources`, and stable accessible region/group names.
|
||||
|
||||
- [x] **Step 1: Write failing component tests**
|
||||
|
||||
Add assertions for a `节点资源健康` region containing `在线率`, `平均 CPU`, `平均内存`, `最高温度`, and `短信通道`; verify tag-group buttons filter cards; verify card facts precede resource rows and resource rows precede latest SMS; verify the checkbox is inside `.fleet-card-header-meta` only in batch mode.
|
||||
|
||||
- [x] **Step 2: Run the focused test and verify RED**
|
||||
|
||||
Run: `corepack pnpm --filter @multi-simadmin/web test -- src/fleet/fleet-page.test.tsx`
|
||||
|
||||
Expected: FAIL because the health strip, tag-group row, facts/resources structure, and header checkbox placement do not exist.
|
||||
|
||||
- [x] **Step 3: Implement the minimal React structure**
|
||||
|
||||
Compute known CPU, memory, and temperature aggregates with `useMemo`; derive online rate and readable SMS-channel count; render missing aggregate values as `--`. Add tag buttons that reuse `setTag`, move the batch checkbox into `.fleet-card-header-meta`, place metadata tags before facts, and split facts from full-width CPU/memory resource rows.
|
||||
|
||||
- [x] **Step 4: Run focused tests and verify GREEN**
|
||||
|
||||
Run: `corepack pnpm --filter @multi-simadmin/web test -- src/fleet/fleet-page.test.tsx`
|
||||
|
||||
Expected: all Fleet tests pass.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Consolidate the warm, dense visual hierarchy
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/web/src/styles.css`
|
||||
- Modify: `scripts/real-browser-e2e.mjs`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: the Task 1 class names and the existing `.fleet-workspace`, `.fleet-sidebar`, `.fleet-card-grid`, and `.app-layout` contracts.
|
||||
- Produces: one final Fleet style owner and browser geometry checks for the new structures.
|
||||
|
||||
- [x] **Step 1: Add failing Chrome geometry assertions**
|
||||
|
||||
Extend `fleetLayout(cdp)` to return health-strip, group-row, first-card, first-resource-row, and checkbox bounds. Require each structure to remain within the results pane and viewport; in batch mode require the checkbox center to sit in the card's upper-right quadrant.
|
||||
|
||||
- [x] **Step 2: Run browser verification and verify RED**
|
||||
|
||||
Run: `corepack pnpm --filter @multi-simadmin/web build` followed by `node scripts/real-browser-e2e.mjs --clean-dist`.
|
||||
|
||||
Expected: FAIL because the new structures do not yet have stable responsive geometry.
|
||||
|
||||
- [x] **Step 3: Implement styles in the existing final cascade section**
|
||||
|
||||
Style the health strip as a compact five-column hairline surface, the tag row as an overflow-safe segmented control, and cards as flat warm panels with a 12px radius. Make facts a compact two-column rail, resources two full-width aligned rows, and checkbox/status a stable upper-right group. Add 960px and 560px adaptations without changing the established card-column breakpoints.
|
||||
|
||||
- [x] **Step 4: Run complete verification**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
corepack pnpm --filter @multi-simadmin/web test
|
||||
corepack pnpm --filter @multi-simadmin/web typecheck
|
||||
corepack pnpm --filter @multi-simadmin/web build
|
||||
$env:E2E_SCREENSHOT_DIR='artifacts/komari-fleet-review'; node scripts/real-browser-e2e.mjs --clean-dist
|
||||
corepack pnpm lint
|
||||
corepack pnpm exec prettier --check apps/web/src/fleet/fleet-page.tsx apps/web/src/fleet/fleet-page.test.tsx apps/web/src/styles.css scripts/real-browser-e2e.mjs docs/superpowers/specs/2026-07-30-komari-inspired-fleet-workbench-design.md docs/superpowers/plans/2026-07-30-komari-inspired-fleet-workbench.md
|
||||
```
|
||||
|
||||
Expected: tests, typecheck, build, Chrome E2E, lint, and formatting pass.
|
||||
|
||||
- [x] **Step 5: Inspect screenshots and restore the local build**
|
||||
|
||||
Inspect 390px, 1440px, and 1920px Fleet screenshots for clipping, hierarchy, and card density. Rebuild without `--clean-dist`, confirm `http://127.0.0.1:8789/` returns 200, and confirm the automation schedules endpoint remains reachable.
|
||||
@@ -0,0 +1,128 @@
|
||||
# Restore Fleet Sidebar 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:** Restore the Fleet overview and search controls to a persistent left sidebar on desktop while preserving a compact stacked disclosure on tablet and mobile.
|
||||
|
||||
**Architecture:** Keep the existing `FleetPage` DOM and state because it already separates the sidebar from the results pane and provides a working disclosure control. Correct only the final CSS cascade that currently overrides the two-column layout, then extend the real-browser geometry checks so future style changes cannot silently move the sidebar back into the main flow.
|
||||
|
||||
**Tech Stack:** React 19, TypeScript, CSS Grid, Vitest, Chrome DevTools Protocol E2E.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Preserve the warm cream, brown, and mint workbench palette and restrained Apple-style hierarchy.
|
||||
- Do not change Fleet filtering, sorting, selection, or operation behavior.
|
||||
- Keep desktop sidebar collapse behavior and mobile/tablet stacked disclosure behavior accessible.
|
||||
- Keep the existing viewport acceptance matrix: 390px, 768px, 1024px, and 1440px.
|
||||
- Do not add dependencies.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Lock the responsive Fleet geometry and restore the sidebar
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `scripts/real-browser-e2e.mjs:384`
|
||||
- Modify: `apps/web/src/styles.css:4185`
|
||||
- Test: `scripts/real-browser-e2e.mjs`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: Existing `.fleet-workspace`, `.fleet-sidebar`, `.fleet-sidebar-header`, `.fleet-sidebar-content`, `.fleet-results-pane`, and `.fleet-card-grid` DOM classes.
|
||||
- Produces: A desktop two-column geometry above 60rem and a stacked disclosure at 60rem and below, verified through the existing `fleetLayout(cdp)` helper.
|
||||
|
||||
- [x] **Step 1: Extend the browser layout snapshot**
|
||||
|
||||
Update `fleetLayout(cdp)` to return the workspace display mode plus sidebar and results bounding rectangles:
|
||||
|
||||
```js
|
||||
const workspace = document.querySelector('.fleet-workspace');
|
||||
const sidebar = document.querySelector('.fleet-sidebar');
|
||||
const results = document.querySelector('.fleet-results-pane');
|
||||
return {
|
||||
workspaceDisplay: workspace ? getComputedStyle(workspace).display : null,
|
||||
sidebar: sidebar ? sidebar.getBoundingClientRect().toJSON() : null,
|
||||
results: results ? results.getBoundingClientRect().toJSON() : null,
|
||||
sidebarHeaderDisplay: document.querySelector('.fleet-sidebar-header')
|
||||
? getComputedStyle(document.querySelector('.fleet-sidebar-header')).display
|
||||
: null,
|
||||
};
|
||||
```
|
||||
|
||||
- [x] **Step 2: Add viewport-specific assertions**
|
||||
|
||||
For 1024px and 1440px, assert `workspaceDisplay === 'grid'`, `sidebar.right < results.left`, and `sidebarHeaderDisplay !== 'none'`. For 390px and 768px, assert `sidebar.top < results.top`, the horizontal spans overlap, and the sidebar heading remains visible.
|
||||
|
||||
- [x] **Step 3: Run the browser test and verify the regression is detected**
|
||||
|
||||
Run: `corepack pnpm --filter @multi-simadmin/web build && node scripts/real-browser-e2e.mjs --clean-dist`
|
||||
|
||||
Expected: FAIL at the desktop geometry assertion because the final CSS currently computes `.fleet-workspace` as `display: block`.
|
||||
|
||||
- [x] **Step 4: Restore the final CSS cascade**
|
||||
|
||||
Replace the final single-column Fleet overrides with explicit responsive rules:
|
||||
|
||||
```css
|
||||
.fleet-workspace,
|
||||
.fleet-workspace.is-sidebar-collapsed {
|
||||
display: grid;
|
||||
grid-template-columns: 17rem minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: clamp(0.9rem, 1.25vw, 1.25rem);
|
||||
}
|
||||
|
||||
.fleet-workspace.is-sidebar-collapsed {
|
||||
grid-template-columns: 4.5rem minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.fleet-sidebar {
|
||||
position: sticky;
|
||||
top: 5.3rem;
|
||||
width: auto;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
border: 1px solid rgba(147, 127, 95, 0.18);
|
||||
border-radius: 16px;
|
||||
background: rgba(250, 247, 232, 0.78);
|
||||
box-shadow: 0 8px 28px rgba(77, 62, 43, 0.06);
|
||||
backdrop-filter: blur(22px) saturate(130%);
|
||||
}
|
||||
|
||||
.fleet-sidebar-header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media (max-width: 60rem) {
|
||||
.fleet-workspace,
|
||||
.fleet-workspace.is-sidebar-collapsed {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.fleet-sidebar,
|
||||
.fleet-sidebar.is-collapsed {
|
||||
position: static;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Retain the existing collapsed-content rules and card-column breakpoints so behavior remains unchanged.
|
||||
|
||||
- [x] **Step 5: Run focused verification**
|
||||
|
||||
Run: `corepack pnpm --filter @multi-simadmin/web test -- fleet/fleet-page.test.tsx`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
Run: `corepack pnpm --filter @multi-simadmin/web typecheck`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
Run: `corepack pnpm --filter @multi-simadmin/web build && node scripts/real-browser-e2e.mjs --clean-dist`
|
||||
|
||||
Expected: PASS at all four viewport sizes, with no horizontal overflow and the intended 1/2/2/3 card columns.
|
||||
|
||||
- [x] **Step 6: Review generated screenshots**
|
||||
|
||||
Run the browser test with `E2E_SCREENSHOT_DIR=artifacts/fleet-sidebar-review` and inspect the 390px, 768px, 1024px, and 1440px Fleet screenshots. Confirm that the summary and search are left of results on desktop, stacked above results at smaller widths, and that no text or controls overlap.
|
||||
@@ -0,0 +1,412 @@
|
||||
# Warm Operations Workbench 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:** Deliver a warm, compact operations UI and a durable Beijing-time automation center for service restarts, system reboots, and SMS delivery across fixed or tag-selected instances.
|
||||
|
||||
**Architecture:** Add strict shared contracts, a versioned SQLite schedule store, a Cron/time service, and a lifecycle-managed coordinator that claims immutable run occurrences before dispatch. Reuse the existing instance, Job, Audit, secret-store, and secure-operation boundaries, then expose the behavior through authenticated Fastify routes and a React Automation workspace. The frontend keeps Jobs and Audit as secondary Automation tabs while Fleet becomes a compact responsive node grid with opt-in batch selection.
|
||||
|
||||
**Tech Stack:** TypeScript 5.9, React 19, Fastify 5, SQLite/better-sqlite3, Drizzle schema declarations, `cron-parser`, Vitest, Testing Library, Vite, animal-island-ui 1.3.0.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Timezone is fixed to `Asia/Shanghai`; APIs reject any other timezone and UI copy displays Beijing time (`UTC+8`).
|
||||
- Cron is standard five-field syntax; seconds are unsupported and no minimum interval is enforced.
|
||||
- Misfire defaults to `skip`; catch-up runs at most once. Overlap defaults to `skip`; queueing is bounded to one occurrence.
|
||||
- Dynamic selectors resolve on every claim and support `any` or `all`; every run persists an immutable target snapshot and schedule version.
|
||||
- System reboot defaults to zero retries. All retry counts and intervals are bounded by contracts.
|
||||
- SMS recipients and content live only in the existing secret store. SQLite, logs, Audit, and HTTP reads expose references or redacted summaries only.
|
||||
- Deleting a schedule is soft deletion for configuration and never removes Jobs, Audit, or scheduled-run history.
|
||||
- Preserve existing dirty-worktree changes. Do not commit overlapping user-owned modifications; use test checkpoints instead.
|
||||
- UI uses cream solid surfaces, warm brown text, mint actions, thin warm borders, 14-16px panel radii, no floating card shadows, and restrained 120-200ms feedback.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Automation Contracts
|
||||
|
||||
**Files:**
|
||||
- Create: `packages/contracts/src/automation.ts`
|
||||
- Create: `packages/contracts/src/automation.test.ts`
|
||||
- Modify: `packages/contracts/src/index.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `ScheduledTask`, `ScheduledRun`, `CreateScheduledTaskRequest`, `UpdateScheduledTaskRequest`, `ScheduleTargetSelector`, `CronPreview`, policy and outcome constants.
|
||||
- Produces: `parseCreateScheduledTaskRequest(value: unknown)` and `parseUpdateScheduledTaskRequest(value: unknown)` with exact-key, length, enum, and numeric-bound validation.
|
||||
|
||||
- [ ] **Step 1: Write the failing contract tests**
|
||||
|
||||
```ts
|
||||
expect(parseCreateScheduledTaskRequest(validRestart)).toMatchObject({
|
||||
timezone: 'Asia/Shanghai', misfirePolicy: 'skip', overlapPolicy: 'skip'
|
||||
});
|
||||
expect(() => parseCreateScheduledTaskRequest({ ...validRestart, timezone: 'UTC' })).toThrow();
|
||||
expect(() => parseCreateScheduledTaskRequest({ ...validRestart, cronExpression: '* * * * * *' })).toThrow();
|
||||
expect(() => parseCreateScheduledTaskRequest({ ...validSms, sms: { recipients: [], content: '' } })).toThrow();
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify RED**
|
||||
|
||||
Run: `corepack pnpm vitest run packages/contracts/src/automation.test.ts`
|
||||
|
||||
Expected: FAIL because `automation.ts` and parsers do not exist.
|
||||
|
||||
- [ ] **Step 3: Implement strict contracts and parsers**
|
||||
|
||||
Use literal unions for `restart-service | reboot-system | send-sms`, `fixed | tags`, `skip | catch-up-once`, `skip | queue-once`, and the six approved run outcomes. Bound names to 120 characters, recipients to 50, fixed IDs/tags to 200, SMS content to 2,000 characters, retries to 10, and retry interval to 86,400 seconds.
|
||||
|
||||
- [ ] **Step 4: Verify GREEN**
|
||||
|
||||
Run: `corepack pnpm vitest run packages/contracts/src/automation.test.ts packages/contracts/src/index.test.ts`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
### Task 2: Scheduling Persistence
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/api/src/infrastructure/database/migrations.ts`
|
||||
- Modify: `apps/api/src/infrastructure/database/schema.ts`
|
||||
- Modify: `apps/api/src/infrastructure/database/database.test.ts`
|
||||
- Create: `apps/api/src/application/automation/scheduled-task-repository.ts`
|
||||
- Create: `apps/api/src/application/automation/scheduled-task-repository.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: contracts from Task 1.
|
||||
- Produces: `ScheduledTaskRepository.create/get/list/update/setEnabled/softDelete`, `claimOccurrence`, `startRun`, and `finishRun`.
|
||||
- Produces: migration 8 tables `scheduled_tasks` and `scheduled_runs`, with unique `(scheduled_task_id, schedule_version, due_at, trigger_source)` claim identity.
|
||||
|
||||
- [ ] **Step 1: Write failing migration and repository tests**
|
||||
|
||||
```ts
|
||||
expect(tableNames(db)).toContain('scheduled_tasks');
|
||||
expect(tableNames(db)).toContain('scheduled_runs');
|
||||
expect(repository.claimOccurrence(task.id, task.version, dueAt, 'scheduled', targets)).not.toBeNull();
|
||||
expect(repository.claimOccurrence(task.id, task.version, dueAt, 'scheduled', targets)).toBeNull();
|
||||
expect(repository.getRun(firstRun.id)?.targetSnapshot).toEqual(['instance-a', 'instance-b']);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify RED**
|
||||
|
||||
Run: `corepack pnpm vitest run apps/api/src/application/automation/scheduled-task-repository.test.ts apps/api/src/infrastructure/database/database.test.ts -t "scheduled"`
|
||||
|
||||
Expected: FAIL because migration 8 and repository are absent.
|
||||
|
||||
- [ ] **Step 3: Add migration 8, Drizzle declarations, and repository**
|
||||
|
||||
Store selectors and policies as validated JSON, timestamps as UTC ISO strings, the fixed timezone marker, a nullable SMS secret reference, `deleted_at`, optimistic `version`, and immutable run snapshots. Wrap claim insert and overlap inspection in one better-sqlite3 transaction.
|
||||
|
||||
- [ ] **Step 4: Verify GREEN**
|
||||
|
||||
Run: `corepack pnpm vitest run apps/api/src/application/automation/scheduled-task-repository.test.ts apps/api/src/infrastructure/database/database.test.ts -t "scheduled|business tables"`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
### Task 3: Cron and Target Resolution
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/api/package.json`
|
||||
- Modify: `pnpm-lock.yaml`
|
||||
- Create: `apps/api/src/application/automation/schedule-time.ts`
|
||||
- Create: `apps/api/src/application/automation/schedule-time.test.ts`
|
||||
- Create: `apps/api/src/application/automation/target-resolver.ts`
|
||||
- Create: `apps/api/src/application/automation/target-resolver.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `previewCron(expression, count, from?)`, `nextOccurrence(expression, after)`, and `reconcileOccurrence(task, now)` using `Asia/Shanghai`.
|
||||
- Produces: `resolveTargets(db, selector): readonly ResolvedTarget[]` returning enabled instances with revisions and capability states.
|
||||
|
||||
- [ ] **Step 1: Add failing time and selector tests**
|
||||
|
||||
```ts
|
||||
expect(previewCron('0 9 * * *', 2, new Date('2026-07-30T00:30:00Z'))).toEqual([
|
||||
'2026-07-30T01:00:00.000Z', '2026-07-31T01:00:00.000Z'
|
||||
]);
|
||||
expect(resolveTargets(db, { mode: 'tags', match: 'all', tags: ['lab', 'east'] }).map(x => x.id)).toEqual(['a']);
|
||||
expect(resolveTargets(db, { mode: 'tags', match: 'any', tags: ['lab', 'east'] }).map(x => x.id)).toEqual(['a', 'b']);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify RED**
|
||||
|
||||
Run: `corepack pnpm vitest run apps/api/src/application/automation/schedule-time.test.ts apps/api/src/application/automation/target-resolver.test.ts`
|
||||
|
||||
Expected: FAIL because services are absent.
|
||||
|
||||
- [ ] **Step 3: Install and implement**
|
||||
|
||||
Run: `corepack pnpm --filter @multi-simadmin/api add cron-parser`
|
||||
|
||||
Parse with explicit `tz: 'Asia/Shanghai'`, reject seconds fields before invoking the library, cap preview count at 10, and use parameterized SQL with `GROUP BY/HAVING` for tag matching.
|
||||
|
||||
- [ ] **Step 4: Verify GREEN**
|
||||
|
||||
Run: `corepack pnpm vitest run apps/api/src/application/automation/schedule-time.test.ts apps/api/src/application/automation/target-resolver.test.ts`
|
||||
|
||||
Expected: PASS, including Beijing day-boundary fixtures.
|
||||
|
||||
### Task 4: Schedule Service and SMS Secrets
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/api/src/application/automation/scheduled-task-service.ts`
|
||||
- Create: `apps/api/src/application/automation/scheduled-task-service.test.ts`
|
||||
- Modify: `apps/api/src/infrastructure/secrets/secret-store.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `ScheduledTaskService.create/update/pause/resume/remove/duplicate/preview/runNow/list/listRuns`.
|
||||
- Uses a single JSON SMS secret payload `{ recipients: string[]; content: string }` under a schedule-scoped `SecretKey`; reads never return the payload.
|
||||
|
||||
- [ ] **Step 1: Write failing lifecycle and redaction tests**
|
||||
|
||||
```ts
|
||||
const created = await service.create(actor, smsRequest);
|
||||
expect(created.sms).toEqual({ configured: true, recipientCount: 2 });
|
||||
expect(JSON.stringify(repository.get(created.id))).not.toContain('hello');
|
||||
expect(secretStore.getCalls).toHaveLength(0);
|
||||
expect((await service.update(actor, created.id, created.version, changedTargets)).version).toBe(2);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify RED**
|
||||
|
||||
Run: `corepack pnpm vitest run apps/api/src/application/automation/scheduled-task-service.test.ts`
|
||||
|
||||
Expected: FAIL because the service is absent.
|
||||
|
||||
- [ ] **Step 3: Implement lifecycle and secret compensation**
|
||||
|
||||
Write the secret before the task transaction, compensate by deleting it when persistence fails, rotate and cleanup old references after successful sensitive edits, require optimistic versions, and preserve runs on soft delete.
|
||||
|
||||
- [ ] **Step 4: Verify GREEN**
|
||||
|
||||
Run: `corepack pnpm vitest run apps/api/src/application/automation/scheduled-task-service.test.ts`
|
||||
|
||||
Expected: PASS with no plaintext SMS values in serialized task or errors.
|
||||
|
||||
### Task 5: Scheduled Dispatch and Coordinator
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/api/src/application/automation/scheduled-operation-dispatcher.ts`
|
||||
- Create: `apps/api/src/application/automation/scheduled-operation-dispatcher.test.ts`
|
||||
- Create: `apps/api/src/application/automation/scheduler-coordinator.ts`
|
||||
- Create: `apps/api/src/application/automation/scheduler-coordinator.test.ts`
|
||||
- Modify: `apps/api/src/application/messages/instance-message-service.ts`
|
||||
- Modify: `apps/api/src/application/messages/instance-message-service.test.ts`
|
||||
- Modify: `apps/api/src/application/operations/secure-operation-execution.ts`
|
||||
- Modify: `apps/api/src/application/operations/secure-operation-execution.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `ScheduledOperationDispatcher.dispatch(run, task, targets)` and `SchedulerCoordinator.start/stop/tick`.
|
||||
- Extends message execution with a Job/Audit-recorded scheduled SMS method whose body digest is computed before dispatch and whose summaries contain only redacted fields.
|
||||
|
||||
- [ ] **Step 1: Write failing aggregate execution tests**
|
||||
|
||||
```ts
|
||||
expect(await dispatch(twoTargetsOneFailure)).toMatchObject({ outcome: 'partially-succeeded' });
|
||||
expect(await dispatch(noTargets)).toMatchObject({ outcome: 'no-targets' });
|
||||
expect(await dispatch(missingSmsSecret)).toMatchObject({ outcome: 'needs-attention' });
|
||||
expect(auditRows(db).every(row => !JSON.stringify(row).includes(phoneOrContent))).toBe(true);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify RED**
|
||||
|
||||
Run: `corepack pnpm vitest run apps/api/src/application/automation/scheduled-operation-dispatcher.test.ts apps/api/src/application/automation/scheduler-coordinator.test.ts`
|
||||
|
||||
Expected: FAIL because dispatcher and coordinator are absent.
|
||||
|
||||
- [ ] **Step 3: Implement dispatch and coordinator lifecycle**
|
||||
|
||||
Dispatch each target independently, create correlated Jobs, aggregate outcomes, bound retries, default reboot retries to zero, reconcile at startup, claim before dispatch, skip or queue one overlap, and expose injected clock/timer dependencies for deterministic tests. Never persist interactive confirmation tokens.
|
||||
|
||||
- [ ] **Step 4: Verify GREEN**
|
||||
|
||||
Run: `corepack pnpm vitest run apps/api/src/application/automation apps/api/src/application/messages/instance-message-service.test.ts apps/api/src/application/operations/secure-operation-execution.test.ts`
|
||||
|
||||
Expected: PASS for duplicates, misfire, overlap, retry, partial success, and redaction.
|
||||
|
||||
### Task 6: Automation HTTP API
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/api/src/interface/http/automation-routes.ts`
|
||||
- Create: `apps/api/src/interface/http/automation-routes.test.ts`
|
||||
- Modify: `apps/api/src/control-plane.ts`
|
||||
- Modify: `apps/api/src/control-plane.test.ts`
|
||||
- Modify: `apps/api/src/production-control-plane.ts`
|
||||
- Modify: `apps/api/src/index.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces authenticated `/api/v1/automation/schedules`, `/:id`, `/:id/state`, `/:id/run`, `/cron/preview`, and `/runs` routes.
|
||||
- Returns RFC 9457-style sanitized problems and ETags based on schedule version.
|
||||
|
||||
- [ ] **Step 1: Write failing route tests**
|
||||
|
||||
```ts
|
||||
expect((await app.inject({ method: 'POST', url: '/api/v1/automation/cron/preview', payload: { cronExpression: '0 9 * * *' } })).statusCode).toBe(200);
|
||||
expect(create.statusCode).toBe(201);
|
||||
expect(updateWithoutIfMatch.statusCode).toBe(428);
|
||||
expect(await getJson(create)).not.toHaveProperty('sms.content');
|
||||
expect(deleteResponse.statusCode).toBe(204);
|
||||
expect(runHistoryAfterDelete.items).toHaveLength(1);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify RED**
|
||||
|
||||
Run: `corepack pnpm vitest run apps/api/src/interface/http/automation-routes.test.ts`
|
||||
|
||||
Expected: FAIL with 404/unregistered routes.
|
||||
|
||||
- [ ] **Step 3: Implement routes and app lifecycle**
|
||||
|
||||
Use exact-key body parsing, authenticated actor IDs, request IDs, If-Match optimistic updates, bounded pagination, and coordinator close hooks. Register routes with the same auth scope as Jobs and Audit.
|
||||
|
||||
- [ ] **Step 4: Verify GREEN**
|
||||
|
||||
Run: `corepack pnpm vitest run apps/api/src/interface/http/automation-routes.test.ts apps/api/src/control-plane.test.ts apps/api/src/production-control-plane.test.ts`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
### Task 7: Automation Web Workspace
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/web/src/automation/automation-api-data-source.ts`
|
||||
- Create: `apps/web/src/automation/automation-api-data-source.test.ts`
|
||||
- Create: `apps/web/src/automation/automation-page.tsx`
|
||||
- Create: `apps/web/src/automation/automation-page.test.tsx`
|
||||
- Modify: `apps/web/src/jobs/jobs-page.tsx`
|
||||
- Modify: `apps/web/src/audit/audit-page.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `AutomationApiDataSource` CRUD/preview/run methods and `AutomationPage` with `schedules | runs | records` tabs.
|
||||
- Consumes existing `JobsPage`, `AuditPage`, instance summaries, and new automation contracts.
|
||||
|
||||
- [ ] **Step 1: Write failing UI behavior tests**
|
||||
|
||||
```tsx
|
||||
expect(screen.getByRole('tab', { name: 'Schedules' })).toHaveAttribute('aria-selected', 'true');
|
||||
await user.click(screen.getByRole('button', { name: 'Create schedule' }));
|
||||
expect(screen.getByRole('dialog', { name: 'Create schedule' })).toBeVisible();
|
||||
await user.selectOptions(screen.getByLabelText('Target mode'), 'tags');
|
||||
expect(screen.getByLabelText('Tag matching')).toBeVisible();
|
||||
expect(screen.queryByLabelText('Timezone')).not.toBeInTheDocument();
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify RED**
|
||||
|
||||
Run: `corepack pnpm vitest run apps/web/src/automation/automation-page.test.tsx apps/web/src/automation/automation-api-data-source.test.ts`
|
||||
|
||||
Expected: FAIL because Automation UI is absent.
|
||||
|
||||
- [ ] **Step 3: Implement dense list and progressive drawer**
|
||||
|
||||
Use native semantic controls where the library API does not fit; use animal-island-ui Button, Switch, Tag, Tabs, and Drawer where accessible. Include five-run Beijing preview, high-frequency restart warning plus confirmation, fixed/tag targets, SMS fields, optional window, policies, retries, row menu actions, empty/error/loading states, and redacted summaries.
|
||||
|
||||
- [ ] **Step 4: Verify GREEN**
|
||||
|
||||
Run: `corepack pnpm vitest run apps/web/src/automation`
|
||||
|
||||
Expected: PASS for create/edit/pause/run/delete and keyboard focus behavior.
|
||||
|
||||
### Task 8: Shell and Navigation
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/web/src/app-shell.tsx`
|
||||
- Modify: `apps/web/src/app-shell.integration.test.tsx`
|
||||
- Modify: `apps/web/src/main.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Routes `/automation` to `AutomationPage`; aliases `/jobs` to Runs and `/audit` to Operation records.
|
||||
- Primary navigation contains Nodes, Automation, and Settings only.
|
||||
|
||||
- [ ] **Step 1: Write failing navigation tests**
|
||||
|
||||
```tsx
|
||||
expect(within(screen.getByRole('navigation', { name: 'Primary' })).getAllByRole('link')).toHaveLength(3);
|
||||
expect(screen.queryByRole('link', { name: 'Audit' })).not.toBeInTheDocument();
|
||||
history.pushState({}, '', '/jobs');
|
||||
expect(await screen.findByRole('tab', { name: 'Runs' })).toHaveAttribute('aria-selected', 'true');
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify RED**
|
||||
|
||||
Run: `corepack pnpm vitest run apps/web/src/app-shell.integration.test.tsx`
|
||||
|
||||
Expected: FAIL against the old primary navigation.
|
||||
|
||||
- [ ] **Step 3: Implement compact shell and aliases**
|
||||
|
||||
Keep the header near 56px, preserve deep instance routes and settings subroutes, show connection state at the trailing edge, and use the existing icon component for navigation symbols.
|
||||
|
||||
- [ ] **Step 4: Verify GREEN**
|
||||
|
||||
Run: `corepack pnpm vitest run apps/web/src/app-shell.integration.test.tsx`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
### Task 9: Fleet Workbench
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/web/src/fleet/fleet-page.tsx`
|
||||
- Modify: `apps/web/src/fleet/fleet-page.test.tsx`
|
||||
- Modify: `apps/web/src/styles.css`
|
||||
|
||||
**Interfaces:**
|
||||
- Preserves existing Fleet data-source and operation-client props.
|
||||
- Adds explicit `selectionMode` UI state; checkboxes exist only while it is true.
|
||||
|
||||
- [ ] **Step 1: Write failing Fleet interaction tests**
|
||||
|
||||
```tsx
|
||||
expect(screen.queryByRole('checkbox', { name: /select/i })).not.toBeInTheDocument();
|
||||
await user.click(screen.getByRole('button', { name: 'Batch select' }));
|
||||
expect(screen.getAllByRole('checkbox', { name: /select/i })).toHaveLength(visibleNodeCount + 1);
|
||||
await user.click(screen.getByRole('button', { name: 'Exit batch selection' }));
|
||||
expect(screen.queryByRole('checkbox', { name: /select/i })).not.toBeInTheDocument();
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify RED**
|
||||
|
||||
Run: `corepack pnpm vitest run apps/web/src/fleet/fleet-page.test.tsx`
|
||||
|
||||
Expected: FAIL because selection controls are permanently visible or no batch mode exists.
|
||||
|
||||
- [ ] **Step 3: Implement the warm dense Fleet layout**
|
||||
|
||||
Remove the persistent sidebar and decorative page title, combine counts/search/filter/sort/refresh/add/batch controls, use stable node panels with identity, endpoint/phone, CPU/memory/temperature, tags/recent SMS, restart/reboot actions, and responsive 3/2/1 columns. Define semantic tokens and reduced-motion/contrast/transparency media queries in `styles.css`.
|
||||
|
||||
- [ ] **Step 4: Verify GREEN**
|
||||
|
||||
Run: `corepack pnpm vitest run apps/web/src/fleet/fleet-page.test.tsx apps/web/src/app-shell.integration.test.tsx`
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
### Task 10: Integrated Verification and Visual QA
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/real-browser-e2e.mjs`
|
||||
- Test: relevant API/Web suites and production build output.
|
||||
|
||||
**Interfaces:**
|
||||
- Adds real-browser acceptance for Automation lifecycle, Fleet selection mode, route aliases, and responsive column/overflow checks.
|
||||
|
||||
- [ ] **Step 1: Extend browser assertions before UI fixes**
|
||||
|
||||
Add literal checks for three primary navigation items, Automation tabs, drawer creation flow, selection-checkbox absence/presence, and viewport screenshots at 390, 768, 1024, and 1440 pixels.
|
||||
|
||||
- [ ] **Step 2: Run focused and broad verification**
|
||||
|
||||
```powershell
|
||||
corepack pnpm vitest run packages/contracts/src/automation.test.ts apps/api/src/application/automation apps/api/src/interface/http/automation-routes.test.ts apps/web/src/automation apps/web/src/fleet/fleet-page.test.tsx apps/web/src/app-shell.integration.test.tsx
|
||||
corepack pnpm typecheck
|
||||
corepack pnpm lint
|
||||
corepack pnpm format:check
|
||||
corepack pnpm --filter @multi-simadmin/web build
|
||||
corepack pnpm test:e2e:browser
|
||||
```
|
||||
|
||||
Expected: all task-related checks PASS. Record known unrelated Windows baseline failures separately rather than suppressing them.
|
||||
|
||||
- [ ] **Step 3: Inspect visual output and correct defects**
|
||||
|
||||
Verify solid cream panels, no card shadows, correct 3/2/1 columns, no horizontal overflow, no text overlap, visible focus, 44px touch controls, and usable reduced-motion/high-contrast variants. Re-run affected tests after each correction.
|
||||
|
||||
- [ ] **Step 4: Run final regression**
|
||||
|
||||
Run: `corepack pnpm test:unit`
|
||||
|
||||
Expected: no new failures relative to the recorded 636-pass/24-fail Windows baseline, and every new or modified task-specific suite passes.
|
||||
@@ -0,0 +1,138 @@
|
||||
# Warm Telemetry Node Card 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:** Replace the segmented Fleet node card with the approved warm telemetry card while preserving navigation, selection, restart behavior, and responsive matrix geometry.
|
||||
|
||||
**Architecture:** Keep Fleet data and operation execution in `FleetPage`; only restructure the card DOM and add one local menu-open state. Let the existing final Fleet cascade remain the single styling owner, replacing its card selectors instead of appending a new theme block. Extend the existing component and real-browser tests to lock semantic order, menu behavior, and geometry.
|
||||
|
||||
**Tech Stack:** React 19, TypeScript, animal-island-ui, Vitest, Testing Library, Vite, Playwright-driven real Chrome script.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Preserve the desktop left sidebar and the 1 / 2 / 2 / 3 / 4 card matrix at 390 / 768 / 1024 / 1440 / 1920 pixels.
|
||||
- Use the existing cream, warm-brown, mint, warning, and danger tokens; do not add photographic backgrounds, glass blur, diagonal stripes, red decorative borders, or card shadows.
|
||||
- Keep the current dashboard link, external origin link, confirmation prompts, and operation-client behavior.
|
||||
- The checkbox remains hidden outside batch mode and stays in the header control cluster when shown.
|
||||
- Do not add dependencies, do not refactor unrelated modules, and do not commit the existing dirty worktree.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Lock The Card Semantic Contract
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/web/src/fleet/fleet-page.test.tsx`
|
||||
- Test: `apps/web/src/fleet/fleet-page.test.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: `FleetPage`, `FleetSnapshot`, existing accessible card names and operation client injection.
|
||||
- Produces: regression coverage for `.fleet-card-metadata`, `.fleet-card-hardware`, `.fleet-card-telemetry`, `.fleet-card-footer`, and the `实例操作 <name>` menu trigger.
|
||||
|
||||
- [ ] **Step 1: Replace the old section-order assertions with the approved scan order**
|
||||
|
||||
```tsx
|
||||
const metadata = card.querySelector('.fleet-card-metadata')!;
|
||||
const hardware = within(card).getByRole('group', { name: '节点硬件信息' });
|
||||
const telemetry = within(card).getByRole('region', { name: '资源遥测' });
|
||||
const footer = within(card).getByRole('region', { name: '短信状态' });
|
||||
expect(metadata.compareDocumentPosition(hardware) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
expect(hardware.compareDocumentPosition(telemetry) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
expect(telemetry.compareDocumentPosition(footer) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Assert restart controls are menu-only**
|
||||
|
||||
```tsx
|
||||
expect(within(card).queryByRole('button', { name: '重启服务 Alpha modem' })).toBeNull();
|
||||
const trigger = within(card).getByRole('button', { name: '实例操作 Alpha modem' });
|
||||
expect(trigger.getAttribute('aria-expanded')).toBe('false');
|
||||
fireEvent.click(trigger);
|
||||
expect(within(card).getByRole('menuitem', { name: '重启服务 Alpha modem' })).toBeTruthy();
|
||||
expect(within(card).getByRole('menuitem', { name: '系统重启 Alpha modem' })).toBeTruthy();
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the focused test and verify RED**
|
||||
|
||||
Run: `corepack pnpm --filter @multi-simadmin/web test -- src/fleet/fleet-page.test.tsx`
|
||||
|
||||
Expected: FAIL because the new regions and menu trigger do not exist and the old buttons are still visible.
|
||||
|
||||
### Task 2: Build The Warm Telemetry Card Structure
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/web/src/fleet/fleet-page.tsx`
|
||||
- Test: `apps/web/src/fleet/fleet-page.test.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: `runCardAction(id, kind)`, `messageStates`, `Progress`, `Tag`, and `Icon`.
|
||||
- Produces: `cardMenuId: string | undefined` state and accessible card action menus.
|
||||
|
||||
- [ ] **Step 1: Add local action-menu state**
|
||||
|
||||
```tsx
|
||||
const [cardMenuId, setCardMenuId] = useState<string>();
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Replace the segmented card body**
|
||||
|
||||
Create the approved DOM order: `header`, `.fleet-card-metadata`, `.fleet-card-hardware`, `.fleet-card-telemetry`, and `.fleet-card-footer`. Move origin and both tag groups into metadata; keep phone and temperature in the hardware group; reuse the two existing `Progress` components in telemetry rows.
|
||||
|
||||
- [ ] **Step 3: Replace the bottom action bar with a header menu**
|
||||
|
||||
Use the existing `Icon name="more"`. The trigger must expose `aria-label`, `aria-haspopup="menu"`, and `aria-expanded`. Each menu item calls `setCardMenuId(undefined)` before `runCardAction` and preserves the current per-operation accessible labels.
|
||||
|
||||
- [ ] **Step 4: Run the focused test and verify GREEN**
|
||||
|
||||
Run: `corepack pnpm --filter @multi-simadmin/web test -- src/fleet/fleet-page.test.tsx`
|
||||
|
||||
Expected: all Fleet tests pass with no warnings.
|
||||
|
||||
### Task 3: Replace Card Styling And Verify Browser Geometry
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/web/src/styles.css`
|
||||
- Modify: `scripts/real-browser-e2e.mjs`
|
||||
- Test: `scripts/real-browser-e2e.mjs`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: final Fleet cascade selectors and the new card class names from Task 2.
|
||||
- Produces: a shadowless warm telemetry card, compact floating menu, and geometry assertions for every supported viewport.
|
||||
|
||||
- [ ] **Step 1: Update browser selectors before CSS**
|
||||
|
||||
Add assertions that `.fleet-card-telemetry`, `.fleet-card-footer`, and `.fleet-card-menu-trigger` stay inside each card and viewport, and that desktop card columns remain 3 at 1440px and 4 at 1920px.
|
||||
|
||||
- [ ] **Step 2: Run browser E2E and verify RED**
|
||||
|
||||
Run: `corepack pnpm run test:e2e:browser`
|
||||
|
||||
Expected: FAIL because the new selectors are not yet styled to the approved geometry.
|
||||
|
||||
- [ ] **Step 3: Replace the final Fleet card selector group**
|
||||
|
||||
Use one hairline outer border, 12px radius, no card shadow, compact header controls, one muted hardware strip, uniform telemetry grid rows, one footer hairline, and a floating warm menu. Remove final-cascade ownership of `.fleet-card-facts`, `.fleet-card-resources`, `.fleet-card-sms`, and `.fleet-card-admin-actions`.
|
||||
|
||||
- [ ] **Step 4: Run focused and full web verification**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
corepack pnpm --filter @multi-simadmin/web test
|
||||
corepack pnpm --filter @multi-simadmin/web typecheck
|
||||
corepack pnpm lint
|
||||
corepack pnpm format:check
|
||||
corepack pnpm --filter @multi-simadmin/web build
|
||||
corepack pnpm run test:e2e:browser
|
||||
```
|
||||
|
||||
Expected: all commands exit 0; real Chrome passes at 390, 768, 1024, 1440, and 1920 pixels and writes updated review screenshots.
|
||||
|
||||
- [ ] **Step 5: Inspect 390px, 1440px, and 1920px screenshots**
|
||||
|
||||
Confirm the left sidebar remains left on desktop, outer gutters remain fluid, no text or menu overlaps, no card has the former bottom action bar, and telemetry values align consistently.
|
||||
Reference in New Issue
Block a user