# 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.
