rebreak-monorepo/docs/superpowers/plans/2026-06-16-magic-dashboard-ios-section.md

501 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Magic Dashboard iOS Section 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:** Redesign the Magic dashboard so iOS devices are shown in a dedicated section under the desktop hero, with live USB status, action buttons, and sync/offboarding flows while keeping detection on-demand only.
**Architecture:** A new `IosDeviceSection` component owns the iOS list. `useDeviceStatus` derives `iosDevices` and `desktopDevices` from the backend list. `IosDeviceCard` renders each device and its action button. `UnknownIosDeviceCard` handles USB-connected devices that are not registered in the backend. Existing `DeviceDetailSheet` is extended to show iOS stars for any connected iOS device.
**Tech Stack:** Nuxt 3, Vue 3, Nuxt UI v4, Tauri 2, TypeScript, pnpm.
---
## Task 1: Extend `useDeviceStatus.ts` to split devices by platform
**Files:**
- Modify: `apps/rebreak-magic/app/composables/useDeviceStatus.ts`
**Why:** The dashboard needs separate `iosDevices` and `desktopDevices` lists instead of one mixed `otherDevices` list.
- [ ] **Step 1: Add platform-based derived lists**
Replace the single `otherDevices` derived with `iosDevices` and `desktopDevices`. Keep `otherDevices` as the union for backward compatibility or remove it if unused after status.vue update.
```ts
const iosDevices = computed<ComputedDevice[]>(() =>
devices.value
.filter((d) => normalizePlatform(d.model ?? d.hostname) === "ios")
.map((d) => mapToComputedDevice(d, false)),
);
const desktopDevices = computed<ComputedDevice[]>(() =>
devices.value
.filter((d) => {
const p = normalizePlatform(d.model ?? d.hostname);
return p === "mac" || p === "windows";
})
.filter((d) => d.deviceId !== currentBackendDevice.value?.deviceId)
.map((d) => mapToComputedDevice(d, false)),
);
```
Introduce a small helper `mapToComputedDevice(d, isCurrent)` to avoid duplicating the mapping object.
- [ ] **Step 2: Update return object**
```ts
return {
currentBackendDevice,
iosDevices,
desktopDevices,
otherDevices: desktopDevices, // temporary alias until status.vue is updated
iosStars,
};
```
- [ ] **Step 3: Typecheck**
Run: `cd apps/rebreak-magic && pnpm nuxi typecheck`
Expected: same pre-existing errors as before, no new ones.
---
## Task 2: Create `IosDeviceCard.vue`
**Files:**
- Create: `apps/rebreak-magic/app/components/IosDeviceCard.vue`
**Why:** Each backend iOS device needs its own card with status, stars (if USB-connected), and a context-aware action button.
- [ ] **Step 1: Write the component**
Props:
```ts
const props = defineProps<{
device: ComputedDevice;
iphone: IphoneDeviceState | null;
isConnected: boolean;
}>();
```
Compute `iosStars` from `iphone` when connected. Derive the action label/target from `device` and `iosStars`:
```ts
const action = computed(() => {
if (!props.isConnected || !props.iphone) {
return { label: "Verbinden", to: "/detect", icon: "i-heroicons-link" };
}
if (!props.iphone.isSupervised) {
return { label: "Supervisen", to: "/supervise", icon: "i-heroicons-shield-check" };
}
if (!props.iphone.installedProfileIDs?.includes("org.rebreak.mdm.enrollment")) {
return { label: "Enrollen", to: "/enroll", icon: "i-heroicons-document-check" };
}
if (!props.iphone.installedProfileIDs?.includes("org.rebreak.protection.contentfilter.sideload")) {
return { label: "Sideload installieren", to: "/sideload", icon: "i-heroicons-lock-closed" };
}
if (!props.iphone.installedAppBundleIDs?.includes("org.rebreak.app")) {
return { label: "App installieren", to: "/sideload", icon: "i-heroicons-arrow-down-tray" };
}
return { label: "Synchronisieren", icon: "i-heroicons-arrow-path" };
});
```
Template: device icon, name/model, status badge, optional `IosStarRating`, last-seen text, and the action button. If the label is "Synchronisieren" use `@click` to emit `sync`; otherwise use `to` navigation.
- [ ] **Step 2: Add emits**
```ts
const emit = defineEmits<{
(e: "sync", device: ComputedDevice): void;
(e: "open", device: ComputedDevice): void;
}>();
```
- [ ] **Step 3: Typecheck**
Run: `cd apps/rebreak-magic && pnpm nuxi typecheck`
Expected: no new errors.
---
## Task 3: Create `UnknownIosDeviceCard.vue`
**Files:**
- Create: `apps/rebreak-magic/app/components/UnknownIosDeviceCard.vue`
**Why:** A USB-connected iOS device that is not registered to the user's ReBreak account must be shown as unrecognizable with a clear next-step message.
- [ ] **Step 1: Write the component**
Props:
```ts
const props = defineProps<{
iphone: IphoneDeviceState;
}>();
```
Template: warning icon, title "Dieses iPhone ist nicht erkennbar", model/iOS version/UDID as read-only info, and helper text:
> "Mit keinem ReBreak-Konto verbunden. Um es zu verwalten: ReBreak-App installieren, anmelden und Gerät registrieren."
No action buttons.
- [ ] **Step 2: Typecheck**
Run: `cd apps/rebreak-magic && pnpm nuxi typecheck`
---
## Task 4: Create `IosDeviceSection.vue`
**Files:**
- Create: `apps/rebreak-magic/app/components/IosDeviceSection.vue`
**Why:** This component owns the iOS section header, list, matching logic, and empty/unknown states.
- [ ] **Step 1: Write the component**
Props:
```ts
const props = defineProps<{
devices: ComputedDevice[];
iphone: IphoneDeviceState | null;
loading: boolean;
hasRefreshed: boolean;
}>();
const emit = defineEmits<{
(e: "sync", device: ComputedDevice): void;
(e: "open", device: ComputedDevice): void;
}>();
```
Implement matching helper:
```ts
function matchesIphone(device: ComputedDevice, iphone: IphoneDeviceState): boolean {
const modelMatch = (device.model ?? "").toLowerCase() === iphone.productType.toLowerCase();
const nameMatch = (device.name ?? "").toLowerCase() === iphone.name.toLowerCase();
return modelMatch || nameMatch;
}
```
Compute:
```ts
const connectedDeviceId = computed(() => {
if (!props.iphone) return null;
return props.devices.find((d) => matchesIphone(d, props.iphone!))?.deviceId ?? null;
});
const hasUnknownUsbDevice = computed(() => {
return !!props.iphone && !connectedDeviceId.value;
});
```
Template:
- Section title "Meine iOS-Geräte"
- If `!hasRefreshed && devices.length === 0`: "Noch keine iOS-Geräte geladen."
- If `hasRefreshed && devices.length === 0`: "Keine iOS-Geräte registriert. ReBreak-App installieren und Gerät hinzufügen."
- If `hasUnknownUsbDevice`: render `UnknownIosDeviceCard` first.
- Render `IosDeviceCard` for each device with `isConnected = device.deviceId === connectedDeviceId`.
- [ ] **Step 2: Typecheck**
Run: `cd apps/rebreak-magic && pnpm nuxi typecheck`
---
## Task 5: Update `status.vue`
**Files:**
- Modify: `apps/rebreak-magic/app/pages/status.vue`
**Why:** The page must render the new iOS section and use `desktopDevices` instead of `otherDevices` for the remaining list.
- [ ] **Step 1: Replace `otherDevices` usage with `iosDevices` and `desktopDevices`**
Update import from `useDeviceStatus`:
```ts
const { currentBackendDevice, iosDevices, desktopDevices, iosStars } =
useDeviceStatus(devices, localHostname, iphone, currentDeviceId);
```
- [ ] **Step 2: Insert iOS section under hero**
After the hero section and before "Weitere Geräte", add:
```vue
<IosDeviceSection
:devices="iosDevices"
:iphone="iphone"
:loading="loading"
:has-refreshed="hasRefreshed"
@sync="onIosSync"
@open="openDevice"
/>
```
- [ ] **Step 3: Change "Weitere Geräte" list to `desktopDevices`**
Replace `otherDevices` references in the list with `desktopDevices`. Update empty copy to "Keine weiteren Computer geladen." / "Keine weiteren Computer registriert." depending on `hasRefreshed`.
- [ ] **Step 4: Add `onIosSync` handler**
```ts
async function onIosSync(device: ComputedDevice) {
loading.value = true;
error.value = null;
try {
await protection.refreshIphone();
// TODO: push missing MDM components and compare MDM version once the backend exposes it.
await protection.refreshBackendDevices();
} catch (e: any) {
error.value = e?.message ?? "Synchronisierung fehlgeschlagen";
} finally {
loading.value = false;
hasRefreshed.value = true;
}
}
```
Expose `refreshIphone` and `refreshBackendDevices` from `useProtectionStatus` if not already exported.
- [ ] **Step 5: Typecheck**
Run: `cd apps/rebreak-magic && pnpm nuxi typecheck`
Expected: no new errors.
---
## Task 6: Extend `useProtectionStatus.ts` exports
**Files:**
- Modify: `apps/rebreak-magic/app/composables/useProtectionStatus.ts`
**Why:** `status.vue` needs to call `refreshIphone` and `refreshBackendDevices` independently for the sync action.
- [ ] **Step 1: Export the two refresh functions**
Add to the return object:
```ts
return {
// ... existing returns
refreshIphone,
refreshBackendDevices,
};
```
- [ ] **Step 2: Typecheck**
Run: `cd apps/rebreak-magic && pnpm nuxi typecheck`
---
## Task 7: Adjust `DeviceDetailSheet.vue` for iOS stars
**Files:**
- Modify: `apps/rebreak-magic/app/components/DeviceDetailSheet.vue`
**Why:** iOS devices are never `isCurrent`, but the sheet should still show stars when the opened device is connected via USB.
- [ ] **Step 1: Change `showIosStars` condition**
Accept a new prop or use the existing `iosStars` prop directly. The current condition is:
```ts
const showIosStars = computed(() => props.device?.isCurrent && props.device?.platform === "ios");
```
Change to:
```ts
const showIosStars = computed(() => props.device?.platform === "ios" && !!props.iosStars);
```
Ensure the parent passes `iosStars` for the opened iOS device when it is connected.
- [ ] **Step 2: Hide desktop-only sections for iOS devices**
`showDesktopToggle` should remain as is (only mac/windows + isCurrent).
Cooldown controls should only show for `device.isCurrent` (desktop), which is already the case.
- [ ] **Step 3: Typecheck**
Run: `cd apps/rebreak-magic && pnpm nuxi typecheck`
---
## Task 8: Update `DeviceHeroCard.vue` and `DeviceListItem.vue`
**Files:**
- Modify: `apps/rebreak-magic/app/components/DeviceHeroCard.vue`
- Modify: `apps/rebreak-magic/app/components/DeviceListItem.vue`
**Why:** These components are now desktop-only. Remove iOS-specific rendering if it is no longer needed or keep it defensive.
- [ ] **Step 1: In `DeviceHeroCard.vue`, keep `showIosStars` defensive**
No functional change needed because the hero only receives desktop devices, but confirm `showIosStars` still computes correctly.
- [ ] **Step 2: In `DeviceListItem.vue`, no change required**
It will only be rendered with desktop devices.
---
## Task 9: Add MDM version awareness (frontend foundation)
**Files:**
- Modify: `apps/rebreak-magic/app/composables/useTauri.ts`
- Modify: `apps/rebreak-magic/app/components/IosDeviceCard.vue`
**Why:** The sync action must later compare installed MDM version with the latest ReBreak MDM version.
- [ ] **Step 1: Add a constant and helper in `useTauri.ts`**
```ts
export const REBREAK_MDM_VERSION = "0.1";
export function getInstalledMdmVersion(installedProfileIDs: string[]): string | null {
const versionId = installedProfileIDs.find((id) => id.startsWith("org.rebreak.mdm.version."));
return versionId?.replace("org.rebreak.mdm.version.", "") ?? null;
}
```
- [ ] **Step 2: Use it in `IosDeviceCard.vue` action logic**
When connected and all core checks pass, compare `getInstalledMdmVersion(...)` with `REBREAK_MDM_VERSION`. If outdated or missing, return `{ label: "MDM-Update installieren", icon: "i-heroicons-arrow-up-tray" }` and emit `sync`.
- [ ] **Step 3: Typecheck**
Run: `cd apps/rebreak-magic && pnpm nuxi typecheck`
---
## Task 10: Grace-period / Offboarding placeholder
**Files:**
- Modify: `apps/rebreak-magic/app/components/IosDeviceCard.vue`
- Modify: `apps/rebreak-magic/app/pages/status.vue`
**Why:** The spec requires a "ReBreak entfernen" action during the 3-day grace period after cancellation. The backend endpoint does not exist yet, so we add a safe placeholder.
- [ ] **Step 1: Add `subscriptionInGracePeriod` prop**
```ts
const props = defineProps<{
device: ComputedDevice;
iphone: IphoneDeviceState | null;
isConnected: boolean;
inGracePeriod?: boolean;
}>();
```
- [ ] **Step 2: Show offboarding button when in grace period**
At the top of the action derivation:
```ts
if (props.inGracePeriod) {
return { label: "ReBreak entfernen", icon: "i-heroicons-trash", variant: "danger" };
}
```
Emit a new `remove` event. The parent shows a placeholder toast or logs until the backend endpoint is ready.
- [ ] **Step 3: Stub grace-period state in `status.vue`**
```ts
const subscriptionInGracePeriod = ref(false);
// TODO: populate from backend once subscription status endpoint exists.
```
Pass it to `IosDeviceSection` and down to each card.
- [ ] **Step 4: Typecheck**
Run: `cd apps/rebreak-magic && pnpm nuxi typecheck`
---
## Task 11: Verification and build
**Files:** n/a
**Why:** Ensure the frontend compiles and the Tauri bundle can be built.
- [ ] **Step 1: Typecheck**
Run: `cd apps/rebreak-magic && pnpm nuxi typecheck`
Expected: only pre-existing errors.
- [ ] **Step 2: Build Tauri bundle**
Run: `cd apps/rebreak-magic && pnpm tauri:build`
Expected: completes without new frontend errors. This may take several minutes on first run.
- [ ] **Step 3: Manual smoke test**
Launch the built app with the debug pairing code `000000`, open the dashboard, click **Aktualisieren**, and confirm:
- Desktop hero still renders.
- iOS section appears.
- If no iOS devices: correct empty message.
- If a USB iPhone is connected and registered: stars and action button render.
---
## Task 12: Commit changes
**Files:** n/a
- [ ] **Step 1: Stage and commit**
```bash
git add apps/rebreak-magic/app/composables/useDeviceStatus.ts \
apps/rebreak-magic/app/composables/useProtectionStatus.ts \
apps/rebreak-magic/app/composables/useTauri.ts \
apps/rebreak-magic/app/components/IosDeviceSection.vue \
apps/rebreak-magic/app/components/IosDeviceCard.vue \
apps/rebreak-magic/app/components/UnknownIosDeviceCard.vue \
apps/rebreak-magic/app/components/DeviceDetailSheet.vue \
apps/rebreak-magic/app/components/DeviceHeroCard.vue \
apps/rebreak-magic/app/components/DeviceListItem.vue \
apps/rebreak-magic/app/pages/status.vue \
docs/superpowers/specs/2026-06-16-magic-dashboard-ios-section-design.md \
docs/superpowers/plans/2026-06-16-magic-dashboard-ios-section.md
git commit -m "feat(magic): dedicated iOS section in dashboard with on-demand sync"
```
---
## Spec Coverage Check
| Spec requirement | Task |
|---|---|
| Desktop hero remains | Task 5 |
| Dedicated iOS section under hero | Tasks 4, 5 |
| Backend iOS devices listed | Tasks 1, 4 |
| USB live status synced to matching device | Tasks 2, 4 |
| Unknown USB device shown as unrecognizable | Task 3 |
| Action buttons for supervise/enroll/sideload/app/sync | Task 2 |
| On-demand detection preserved | Task 5, existing code |
| Grace-period offboarding placeholder | Task 10 |
| MDM version foundation | Task 9 |
| DeviceDetailSheet iOS stars | Task 7 |
## Known Backend Dependencies (out of scope for this frontend plan)
- Subscription cancellation / grace-period endpoint.
- Offboarding endpoint: remove MDM profiles, unsupervise, clean DB entry.
- Central `REBREAK_MDM_VERSION` value injected into MDM enrollment profiles.