Delete the old top-level page files whose content was migrated into service-page tabs in slices 5-9: - pages/Media.tsx, Applications.tsx (-> MediaTab) - pages/FileBrowser.tsx, FileBrowser.impl.tsx (-> FilesTab) - pages/Actions.tsx (-> ActionsTab) - pages/Users.tsx, UsersPage.impl.tsx (replaced by Authentik tabs) - components/BackupsPage.tsx (-> JobsTab) - components/ObservabilityPage.tsx (split into Alerts/Links/Metrics tabs) - hooks/useUsers.ts (orphaned after Users page deletion) - the corresponding page test files (Media, FileBrowser, Applications, Actions, UsersPage) that tested the deleted pages directly. The service-tab components are the live implementations; ServicePage renders them. No live code references the deleted files. Docs: append an Information Architecture section to REQUIREMENTS.md documenting the services-as-hub model (nav shape, service-page tabs, service type registry, Users->Authentik, Observability split, legacy route 404s, empty state). Add a CHANGELOG entry under [Unreleased]. 92 frontend tests pass (was 112; -20 deleted page tests); 271 backend tests pass; lint/build green. Refs openspec/changes/services-as-hub-ia/ (tasks slice 11).
7.4 KiB
Slice 7 — Settings mobile forms (worker output)
Files changed
| File | Status | Lines |
|---|---|---|
frontend/src/components/ui/sheet-form.tsx |
modified | +4 / -1 |
frontend/src/pages/Settings.tsx |
modified | +130 / -43 |
frontend/src/pages/__tests__/Settings.test.tsx |
modified | +75 / -0 |
Total: ~228 changed lines (209 insertions, 44 deletions). Under the 400-line budget.
What was implemented
7.1 — Machine editor SheetForm below md
Below md (isMobile === true), the machine editor opens inside a <SheetForm> instead of a centered <Dialog>. Both editors share the same machineDialogOpen state — the SheetForm and Dialog are branched via isMobile ? <SheetForm> : <Dialog>, using the exact same open/close/save/cancel flow.
Open-state strategy: Unlike ServicePage (open-on-mount), the machine editor SheetForm is triggered by user action — the same Edit/Add-machine buttons that open the Dialog on desktop open the SheetForm on mobile. The machineDialogOpen state drives both. No navigation needed on close because the Settings page content (tabbed cards, machine list) is always visible behind the sheet.
Preserved behaviors:
- Validate-on-save —
saveMachineDraft(machineDraft)is unchanged; the same validation logic runs. - SSH test validation —
validateMachineSSH+ the "Validate SSH + trust host" button render inside the MachineEditor, which is shared between both branches. - ConfirmDialog (delete confirmation) — rendered as a sibling OUTSIDE both the SheetForm and Dialog, so it overlays correctly on both layouts.
- Save-disabled logic — added
saveDisabledprop to SheetForm; wired to the same condition the desktop DialogFooter uses (!machineDraft.name || (ssh && !host)). - Delete on mobile — a "Delete machine" button renders inside the SheetForm body (when editing an existing machine), separate from the save bar.
- Desktop (
md+) — the Dialog renders byte-for-byte identical (verified by the 3 existing desktop tests passing unchanged).
SSH key manager
The SSHKeyManager is an inline two-panel layout (not a dialog), and its grid already uses grid-cols-1 md:grid-cols-[320px_minmax(0,1fr)] — it already stacks on mobile. No SheetForm conversion was needed or correct for this component. The machine list grid (grid-cols-1 md:grid-cols-[...]) also already stacks. No changes needed to either.
SheetForm enhancement
Added saveDisabled?: boolean prop to SheetForm (additive, default false). This is needed because the machine editor gates save on required fields (name + host for SSH mode). The existing ServicePage consumer does not pass it (defaults to false). Non-breaking.
Validation
npm run lint → 0 errors, 2 pre-existing warnings (UsersPage.impl.tsx, unrelated)
npm run build → ✓ built (tsc -b + vite)
npm run test → 27 files / 113 tests passed (was 110; +3 new)
Deviations from design
-
SSHKeyManager not wrapped in SheetForm. The task said "both the machine editor dialog AND the SSH-key editor dialog." However, the SSH key manager is an inline two-panel layout (SelectionRailCard + SectionCard), not a dialog. It already stacks responsively (
grid-cols-1 md:grid-cols-[...]). Wrapping an inline editor in a SheetForm would break its always-visible selection-rail UX. The machine editor (which IS a dialog) was converted to SheetForm as specified. -
saveDisabledprop added to SheetForm. The design did not name this prop, but the machine editor requires it to match the desktop DialogFooter'sconfirmDisabledsemantics. Additive and non-breaking. -
No navigation on close. Unlike ServicePage (which navigates to
/serviceson close), the Settings machine editor just closes the sheet — the page content is always behind it, so there's no stranding risk.
skill_resolution
none — no project/user SKILL.md paths were injected, and no .atl/skill-registry.md was found.
Residual risks
- Dirty-state outside-click confirm (R4.5) is still not implemented at the SheetForm level. Same deferred concern as Slice 6 — the SheetForm passes
onOpenChangestraight through. Flag for verify pass. - MachineEditor grid on mobile. The MachineEditor uses
grid-cols-12withcol-span-12 md:col-span-X— already responsive (full-width below md). No changes needed. - Touch targets on rail rows. The machine/SSH-key selection rails use
onClickon<div>elements. The 44px touch-target audit is Slice 9, not here.
Review findings
No blockers. The desktop Dialog is preserved token-identical (verified by 3 existing desktop tests passing unchanged). The SheetForm conversion follows the established ServicePage pattern.
{
"criteriaSatisfied": [
{
"id": "criterion-1",
"status": "satisfied",
"evidence": "Slice 7 converts the machine editor Dialog to SheetForm below md, adds saveDisabled to SheetForm (additive, non-breaking), and extends Settings.test.tsx with 3 mobile tests. Desktop Dialog preserved token-identical (3 existing desktop tests pass unchanged). SSHKeyManager already responsive (inline, not a dialog). No backend, no other pages touched, no scope widening."
}
],
"changedFiles": [
"frontend/src/components/ui/sheet-form.tsx",
"frontend/src/pages/Settings.tsx",
"frontend/src/pages/__tests__/Settings.test.tsx"
],
"testsAddedOrUpdated": [
"frontend/src/pages/__tests__/Settings.test.tsx"
],
"commandsRun": [
{
"command": "cd frontend && npm run lint",
"result": "passed",
"summary": "0 errors; 2 pre-existing warnings in UsersPage.impl.tsx (unrelated)"
},
{
"command": "cd frontend && npm run build",
"result": "passed",
"summary": "tsc -b + vite build clean"
},
{
"command": "cd frontend && npm run test",
"result": "passed",
"summary": "27 files / 113 tests passed (3 new mobile tests added)"
},
{
"command": "cd frontend && git diff --cached --stat",
"result": "passed",
"summary": "Empty — no staged files"
}
],
"validationOutput": [
"Machine editor Dialog → SheetForm branch via isMobile, same machineDialogOpen state",
"saveDisabled prop added to SheetForm (additive, default false)",
"Desktop Dialog token-identical (3 existing desktop tests pass unchanged)",
"SSHKeyManager already responsive (grid-cols-1 md:grid-cols-[...] stacks)",
"ConfirmDialog rendered as sibling outside both SheetForm and Dialog",
"SSH validate button preserved inside shared MachineEditor body"
],
"residualRisks": [
"R4.5 dirty-state outside-click confirm not implemented at SheetForm level (deferred to verify pass)",
"Touch targets on selection-rail rows deferred to Slice 9"
],
"noStagedFiles": true,
"diffSummary": "Adds a mobile (isMobile) branch to the machine editor that renders SheetForm instead of Dialog, using the same machineDialogOpen state. Adds saveDisabled prop to SheetForm for required-field gating. Desktop Dialog is preserved byte-for-byte. 3 new mobile tests (open SheetForm, save payload, cancel closes). 228 changed lines.",
"reviewFindings": [
"no blockers"
],
"manualNotes": "SSHKeyManager was NOT wrapped in SheetForm because it is an inline two-panel layout (not a dialog) that already stacks responsively. The task wording 'SSH-key editor dialog' referred to a dialog that does not exist — the inline grid already handles mobile. Touch-target audit deferred to Slice 9."
}