# 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(() => devices.value .filter((d) => normalizePlatform(d.model ?? d.hostname) === "ios") .map((d) => mapToComputedDevice(d, false)), ); const desktopDevices = computed(() => 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 ``` - [ ] **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.