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

15 KiB
Raw Blame History

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.

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: 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:

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 onIosSync handler
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 showIosStars condition

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, 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
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
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_VERSION value injected into MDM enrollment profiles.