---
phase: 02-data-backup-recovery
plan: "02"
type: execute
wave: 2
depends_on:
  - "02-01"
files_modified:
  - client/src/components/BackupWidget.jsx
  - client/src/pages/Dashboard.jsx
autonomous: false
requirements:
  - BAK-04
  - BAK-05
  - BAK-06
  - BAK-07

must_haves:
  truths:
    - "Dashboard displays a backup widget showing last backup time, next scheduled time, and health status with green/yellow/red indicator"
    - "User can click a button in the widget to trigger a manual backup and sees toast feedback"
    - "User can view a list of snapshots and initiate a restore with dry-run preview"
    - "User can selectively restore a specific directory (e.g., brain/) from a snapshot"
  artifacts:
    - path: "client/src/components/BackupWidget.jsx"
      provides: "Dashboard widget: status display, manual trigger, health indicator, snapshot list, restore UI"
      min_lines: 80
    - path: "client/src/pages/Dashboard.jsx"
      provides: "BackupWidget imported and rendered in dashboard grid"
      contains: "BackupWidget"
  key_links:
    - from: "client/src/components/BackupWidget.jsx"
      to: "client/src/services/api.js"
      via: "import and call getBackupStatus, triggerBackup, getBackupSnapshots, restoreBackup"
      pattern: "api\\.getBackupStatus|api\\.triggerBackup"
    - from: "client/src/components/BackupWidget.jsx"
      to: "client/src/hooks/useAutoRefetch.js"
      via: "useAutoRefetch hook for status polling"
      pattern: "useAutoRefetch"
    - from: "client/src/pages/Dashboard.jsx"
      to: "client/src/components/BackupWidget.jsx"
      via: "import and render in JSX grid"
      pattern: "import BackupWidget"
---

<objective>
Build the dashboard backup widget with health status display, manual backup trigger, snapshot listing, and restore UI with dry-run preview and selective directory restore.

Purpose: Give the user visibility into backup health and one-click control over backup/restore operations directly from the dashboard. This completes the user-facing half of the backup system.

Output: BackupWidget component rendered on the Dashboard, providing all BAK-04 through BAK-07 functionality.
</objective>

<execution_context>
@/Users/antic/.claude/get-shit-done/workflows/execute-plan.md
@/Users/antic/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/02-data-backup-recovery/02-RESEARCH.md
@.planning/phases/02-data-backup-recovery/02-01-SUMMARY.md

<interfaces>
<!-- Key types and contracts the executor needs from Plan 02-01. -->

From client/src/services/api.js (added by Plan 02-01):
```javascript
export const getBackupStatus = (options) => request('/backup/status', options);
export const triggerBackup = () => request('/backup/run', { method: 'POST' });
export const getBackupSnapshots = (options) => request('/backup/snapshots', options);
export const restoreBackup = (data) => request('/backup/restore', { method: 'POST', body: JSON.stringify(data) });
```

API response shapes:

GET /api/backup/status:
```json
{
  "lastRun": "2026-02-26T02:00:00.000Z",
  "lastSnapshotId": "2026-02-26T02-00-00",
  "status": "ok" | "running" | "error" | "never",
  "filesChanged": 12,
  "error": null | "error message",
  "destPath": "/Volumes/BackupDrive/PortOS",
  "nextRun": "2026-02-27T02:00:00.000Z"
}
```

GET /api/backup/snapshots:
```json
[
  { "id": "2026-02-26T02-00-00", "createdAt": "2026-02-26T02-00-00", "fileCount": 245 },
  { "id": "2026-02-25T02-00-00", "createdAt": "2026-02-25T02-00-00", "fileCount": 240 }
]
```

POST /api/backup/restore (request):
```json
{ "snapshotId": "2026-02-26T02-00-00", "dryRun": true, "subdirFilter": "brain" }
```

POST /api/backup/restore (response):
```json
{ "dryRun": true, "snapshotId": "2026-02-26T02-00-00", "subdirFilter": "brain", "changedFiles": [">f..t...... brain/captures.json"] }
```

From client/src/hooks/useAutoRefetch.js:
```javascript
export function useAutoRefetch(fetchFn, intervalMs) {
  // Returns { data, loading }
}
```

From client/src/pages/Dashboard.jsx (existing imports and widget grid):
```jsx
import SystemHealthWidget from '../components/SystemHealthWidget';
import CosDashboardWidget from '../components/CosDashboardWidget';
// ... other widget imports ...
// Widgets rendered inside a grid layout
```

Design tokens from CLAUDE.md:
```
port-bg: #0f0f0f       port-card: #1a1a1a
port-border: #2a2a2a   port-accent: #3b82f6
port-success: #22c55e  port-warning: #f59e0b
port-error: #ef4444
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create BackupWidget component and add to Dashboard</name>
  <files>
    client/src/components/BackupWidget.jsx
    client/src/pages/Dashboard.jsx
  </files>
  <action>
**Create `client/src/components/BackupWidget.jsx`:**

A functional React component using `memo` (consistent with other widgets). Structure:

**1. Status Section (always visible):**
- Use `useAutoRefetch(() => api.getBackupStatus({ silent: true }).catch(() => null), 60000)` for status polling.
- Show health indicator dot: green (`port-success`) if last run < 25 hours ago with no error, yellow (`port-warning`) if 25-49 hours or status is 'never', red (`port-error`) if > 49 hours or status is 'error'.
- Display: "Last backup: {relative time}" (e.g., "2 hours ago"), "Next: {relative time}" (e.g., "in 22 hours"), destination path.
- If `status === 'never'`, show "No backups yet" with guidance text.
- If `status === 'running'`, show a spinner/indicator.
- If `status === 'error'`, show the error message in red text.

**2. Manual Trigger Button (BAK-05):**
- Button labeled "Backup Now".
- On click: call `api.triggerBackup()`, show `toast('Backup started', { icon: '...' })` on success, `toast.error('Backup failed: ' + err.message)` on failure. Use react-hot-toast (already imported pattern in api.js).
- Disable button while status is 'running'.
- No `window.alert` or `window.confirm` per CLAUDE.md.

**3. Snapshots & Restore Section (expandable/collapsible):**
- A toggle to show/hide snapshot list (keeps widget compact by default).
- When expanded, fetch snapshots via `api.getBackupSnapshots()`.
- List snapshots with date and file count.
- Each snapshot has a "Restore" action that expands an inline restore panel (not a modal without URL per CLAUDE.md, but since this is a widget action within the dashboard, an inline expandable is acceptable).

**4. Restore Panel (BAK-06, BAK-07):**
- Shows for the selected snapshot.
- Has a text input for `subdirFilter` (optional, placeholder: "e.g., brain" — for selective directory restore).
- "Preview" button: calls `api.restoreBackup({ snapshotId, dryRun: true, subdirFilter: filter || null })`. Displays the list of files that would change.
- "Restore" button (only enabled after preview): calls `api.restoreBackup({ snapshotId, dryRun: false, subdirFilter })`. Shows toast on completion.
- The dry-run preview list shows each changed file path in a scrollable container.

**5. Health computation helper function (not a hook, just a pure function):**
```javascript
function computeHealth(status) {
  if (!status || status.status === 'error') return 'critical';
  if (status.status === 'never') return 'warning';
  if (status.status === 'running') return 'healthy';
  if (!status.lastRun) return 'warning';
  const hoursSince = (Date.now() - new Date(status.lastRun).getTime()) / (1000 * 60 * 60);
  if (hoursSince < 25) return 'healthy';
  if (hoursSince < 49) return 'warning';
  return 'critical';
}
```

**6. Relative time helper:**
Use a simple relative time formatter (compute from Date.now() - target). Examples: "2 hours ago", "in 22 hours", "3 days ago". No external dependency needed — a small pure function is sufficient.

**Styling:** Use Tailwind classes with the project's design tokens:
- Widget card: `bg-port-card border border-port-border rounded-lg p-4`
- Health dot: `w-2.5 h-2.5 rounded-full` with `bg-port-success`, `bg-port-warning`, or `bg-port-error`
- Buttons: Match existing button patterns in the codebase (look at SystemHealthWidget or other widgets for consistent styling)
- Text: `text-gray-400` for secondary, `text-white` for primary

**Wire into `client/src/pages/Dashboard.jsx`:**

- Add import alphabetically among widget imports (after `BrailleSpinner`, before `CosDashboardWidget`):
  `import BackupWidget from '../components/BackupWidget';`

- Add `<BackupWidget />` in the dashboard grid. Place it near SystemHealthWidget (it's infrastructure status). Look at the existing grid layout and add it in a logical position — after SystemHealthWidget is a good spot since both are system-status widgets.
  </action>
  <verify>
    <automated>cd /Users/antic/github.com/atomantic/PortOS && grep -c 'BackupWidget' client/src/pages/Dashboard.jsx && grep -c 'useAutoRefetch\|getBackupStatus\|triggerBackup\|restoreBackup' client/src/components/BackupWidget.jsx</automated>
  </verify>
  <done>
    - BackupWidget.jsx exists with health status display, manual trigger button, snapshot list, and restore panel
    - Dashboard.jsx imports and renders BackupWidget
    - Health indicator uses green/yellow/red color coding
    - Manual trigger uses toast notifications (no window.alert)
    - Restore supports dry-run preview and selective directory filter
  </done>
</task>

<task type="checkpoint:human-verify" gate="blocking">
  <name>Task 2: Verify complete backup system end-to-end</name>
  <files>client/src/components/BackupWidget.jsx</files>
  <action>
Human verification of the complete backup system. Before requesting verification, run these automated pre-checks:

1. `curl -s http://localhost:5554/api/backup/status | node -e "process.stdin.on('data',d=>{const j=JSON.parse(d);console.log('status:',j.status,'nextRun:',j.nextRun)})"` -- confirms API responds
2. `mkdir -p /tmp/portos-backup-test && curl -s -X PUT http://localhost:5554/api/settings -H 'Content-Type: application/json' -d '{"backup":{"destPath":"/tmp/portos-backup-test","enabled":true}}'` -- configure test destination
3. `curl -s -X POST http://localhost:5554/api/backup/run | node -e "process.stdin.on('data',d=>{const j=JSON.parse(d);console.log('snapshot:',j.snapshotId,'files:',j.fileCount)})"` -- run a test backup
4. `ls /tmp/portos-backup-test/snapshots/` -- verify snapshot directory created

Then request human verification:
- Open the PortOS dashboard in browser
- Verify BackupWidget is visible with health indicator (green after test backup)
- Click "Backup Now" button -- verify toast notification appears
- Expand snapshot list -- verify snapshot appears with file count
- Select a snapshot, type "brain" in filter field, click "Preview" -- verify dry-run file list appears
  </action>
  <verify>
    <automated>curl -s http://localhost:5554/api/backup/status | node -e "process.stdin.on('data',d=>{const j=JSON.parse(d);console.log(j.status!=='never'?'PASS':'FAIL')})"</automated>
  </verify>
  <done>
    - Dashboard shows BackupWidget with correct health status
    - Manual backup trigger works end-to-end
    - Snapshot list displays correctly
    - Dry-run restore preview shows expected file changes
    - User has approved the visual and functional verification
  </done>
</task>

</tasks>

<verification>
1. Dashboard shows BackupWidget with health status indicator
2. Manual backup trigger works end-to-end (rsync copies files, manifest generated)
3. Snapshot list displays with file counts
4. Dry-run restore preview shows changed files
5. Selective directory filter limits restore scope
6. No window.alert or window.confirm usage
7. Toast notifications for all user actions
8. Health indicator correctly reflects backup age (green < 25h, yellow 25-49h, red > 49h)
</verification>

<success_criteria>
- BackupWidget renders on Dashboard with current backup status
- Health indicator shows green/yellow/red based on backup recency
- "Backup Now" triggers manual backup with toast feedback
- Snapshot list is viewable from the widget
- Restore with dry-run preview works for both full and selective (subdirectory) restore
- All existing tests still pass
</success_criteria>

<output>
After completion, create `.planning/phases/02-data-backup-recovery/02-02-SUMMARY.md`
</output>
