15 KiB
Magic Dashboard iOS Section – Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use
superpowers:subagent-driven-development(recommended) orsuperpowers:executing-plansto 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.
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
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:
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:
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
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:
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:
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:
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:
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: renderUnknownIosDeviceCardfirst. -
Render
IosDeviceCardfor each device withisConnected = 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
otherDevicesusage withiosDevicesanddesktopDevices
Update import from useDeviceStatus:
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:
<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
onIosSynchandler
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:
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
showIosStarscondition
Accept a new prop or use the existing iosStars prop directly. The current condition is:
const showIosStars = computed(() => props.device?.isCurrent && props.device?.platform === "ios");
Change to:
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, keepshowIosStarsdefensive
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
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.vueaction 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
subscriptionInGracePeriodprop
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:
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
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
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_VERSIONvalue injected into MDM enrollment profiles.