# Follow-up 1 — SheetForm isDirty wiring (worker output) ## Task Wire the new `isDirty` prop of `SheetForm` into three remaining form consumers (Settings machine editor, message compose, WidgetConfigDialog) so unsaved edits trigger a "Discard changes?" confirm before closing. ## Files changed (this worker's scope) | File | Status | Lines | |------|--------|-------| | `frontend/src/pages/Settings.tsx` | modified | +25 (isMachineDraftDirty helper + isDirty prop) | | `frontend/src/pages/UsersPage.impl.tsx` | modified | +5 (isDirty prop on compose SheetForm) | | `frontend/src/components/WidgetConfigDialog.tsx` | modified | +1 (isDirty prop) | | `frontend/src/pages/__tests__/Settings.test.tsx` | modified | +23 (dirty guard test) | | `frontend/src/pages/__tests__/UsersPage.test.tsx` | modified | +31 (compose dirty guard test) | | `frontend/src/components/__tests__/WidgetConfigDialog.test.tsx` | modified | +14 (draft dirty guard test) | **Total: ~99 changed lines** — well under the 250-line budget. ## isDirty expressions per consumer ### 1. Settings machine editor (`Settings.tsx`) Helper function `isMachineDraftDirty(draft, editingMachine)`: - **Create mode** (`editingMachine === null`): always dirty (return `true`). - **Edit mode**: field-by-field comparison of user-editable fields: - `name`, `host`, `mode`, `port`, `username`, `ssh_key_id`, `enabled`, `notes` - `services` array (sorted JSON.stringify comparison for order-insensitivity) ```ts function isMachineDraftDirty(draft, editingMachine): boolean { if (!editingMachine) return true; return ( draft.name !== editingMachine.name || draft.host !== editingMachine.host || draft.mode !== editingMachine.mode || draft.port !== editingMachine.port || draft.username !== editingMachine.username || draft.ssh_key_id !== editingMachine.ssh_key_id || draft.enabled !== editingMachine.enabled || draft.notes !== editingMachine.notes || JSON.stringify([...draft.services].sort()) !== JSON.stringify([...editingMachine.services].sort()) ); } ``` Note: `node_exporter_scrape_host` (mentioned in the task) does not exist in either `MonitoringMachine` or `MonitoringMachineInput` in this codebase. The comparable editable fields were used instead. Secret fields (`ssh_private_key`, `password`) are excluded because they're write-only (the original only has `*_set` booleans, not values). ### 2. Message compose (`UsersPage.impl.tsx`) ```ts isDirty={ subject.trim() !== "" || htmlBody.trim() !== DEFAULT_HTML_BODY.trim() || attachments.length > 0 } ``` ### 3. WidgetConfigDialog (`WidgetConfigDialog.tsx`) ```ts isDirty={draft !== null} ``` Dirty only in draft mode (when adding/editing a widget). In list mode, `draft === null` → `isDirty = false` (nothing to discard). In draft mode, `onCancel={reset}` returns to the list (does NOT close the sheet), so `isDirty` prompts before resetting the draft. ## Validation ``` npm run lint → 0 errors, 2 pre-existing warnings (UsersPage.impl.tsx exhaustive-deps) npm run build → ✓ built (tsc -b + vite) npm run test → 28 files / 122 tests passed (was 119; +3 new dirty-guard tests) ``` ## Deviations from task 1. **`node_exporter_scrape_host` field**: mentioned in the task but does not exist in the type definitions. Used the actual editable fields that exist on both `MonitoringMachine` and `MonitoringMachineInput`. 2. **Secret fields excluded from dirty check**: `ssh_private_key`, `password`, `ssh_private_key_passphrase` are write-only on the draft and have no comparable value on `editingMachine` (which only has `*_set` booleans). Including them would make the form always dirty in edit mode. ## skill_resolution `none` — no project/user SKILL.md paths were injected; no `.atl/skill-registry.md` found. ## Residual risks - None for this worker's scope. The SheetForm primitive and ServicePage wiring were done by the parent and are not touched here.