feat(magic): Hub vereinigt Magic-Bindings + alte ProtectedDevices

- GET /api/magic/devices fetcht jetzt parallel listMagicDevices()
  + listProtectedDevices() und merged beide Quellen in eine
  Response. Items haben neues 'source' Feld (magic|protected).
- ProtectedDevice (alter Native-DNS-Flow) wird auf gleiche
  Shape gemappt: label->hostname, platform->model.
- Mac-App MagicDevice: source-Feld optional + resolvedSource
  Fallback fuer Backwards-Compat. id mit source-Prefix gegen
  Collisions zwischen Tabellen.
- DeviceHubView Row: protected-Geraete bekommen graues
  'Native-App' Badge und Hinweis 'Verwaltung in der
  ReBreak-App' statt Trash-Button (Release laeuft dort).
This commit is contained in:
chahinebrini 2026-06-03 11:05:15 +02:00
parent dbc62b98ca
commit ac72fabc34
8 changed files with 64 additions and 18 deletions

View File

@ -8,7 +8,13 @@ struct MagicRegistration: Codable {
let existing: Bool
}
enum MagicDeviceSource: String, Codable {
case magic
case protected
}
struct MagicDevice: Codable, Identifiable {
let source: MagicDeviceSource?
let deviceId: String
let hostname: String
let model: String?
@ -16,8 +22,11 @@ struct MagicDevice: Codable, Identifiable {
let magicEnrolledAt: String
let releaseRequestedAt: String?
let releaseAvailableAt: String?
var id: String { deviceId }
var id: String { "\(source?.rawValue ?? "magic"):\(deviceId)" }
/// Default zu `.magic` falls Backend (alte Version) das Feld nicht setzt.
var resolvedSource: MagicDeviceSource { source ?? .magic }
var enrolledDate: Date? {
ISO8601DateFormatter().date(from: magicEnrolledAt)

View File

@ -279,8 +279,19 @@ private struct HubDeviceRow: View {
.frame(width: 28)
VStack(alignment: .leading, spacing: 2) {
Text(device.hostname)
.font(.callout.bold())
HStack(spacing: 6) {
Text(device.hostname)
.font(.callout.bold())
if device.resolvedSource == .protected {
Text("Native-App")
.font(.caption2.bold())
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.gray.opacity(0.15))
.foregroundStyle(.secondary)
.clipShape(Capsule())
}
}
HStack(spacing: 8) {
if let model = device.model {
Text(model).font(.caption2).foregroundStyle(.secondary)
@ -294,7 +305,11 @@ private struct HubDeviceRow: View {
Spacer()
if device.isReleasing {
if device.resolvedSource == .protected {
Text("Verwaltung in der ReBreak-App")
.font(.caption2)
.foregroundStyle(.tertiary)
} else if device.isReleasing {
VStack(alignment: .trailing, spacing: 2) {
Label("Freigabe läuft", systemImage: "hourglass")
.font(.caption2.bold())

View File

@ -221,7 +221,7 @@ struct MacRegistrationView: View {
await MainActor.run {
isInstallingProfile = false
successMessage = "System Settings → Profile geöffnet. Bitte dort „Installieren" klicken und Admin-Passwort eingeben."
successMessage = "System Settings → Profile geöffnet. Bitte dort „Installieren“ klicken und Admin-Passwort eingeben."
}
// Re-check profile status nach kurzer Wartezeit (User muss in UI bestätigen)

View File

@ -36,7 +36,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
ios: {
supportsTablet: true,
bundleIdentifier: MAIN_BUNDLE,
buildNumber: "67",
buildNumber: "68",
// Apple Sign-In Entitlement — Pflicht für expo-apple-authentication nativen
// signInAsync()-Flow. Ohne flag generiert Expo's prebuild den
// com.apple.developer.applesignin-Entitlement nicht in die .entitlements.
@ -59,7 +59,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
android: {
package: "org.rebreak.app",
versionCode: 50,
versionCode: 51,
adaptiveIcon: {
// Foreground muss in der ~66%-Safe-Zone bleiben (Launcher-Mask clippt den
// Außenring) → adaptive-foreground.png ist das Logo auf transparentem

View File

@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>0.3.13</string>
<key>CFBundleVersion</key>
<string>67</string>
<string>68</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>

View File

@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>0.3.13</string>
<key>CFBundleVersion</key>
<string>67</string>
<string>68</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>

View File

@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>0.3.13</string>
<key>CFBundleVersion</key>
<string>67</string>
<string>68</string>
<key>EXAppExtensionAttributes</key>
<dict>
<key>EXExtensionPointIdentifier</key>

View File

@ -1,18 +1,27 @@
import { listMagicDevices } from "../../db/devices";
import { listProtectedDevices } from "../../db/protectedDevices";
import { requireUser } from "../../utils/auth";
/**
* GET /api/magic/devices
*
* Listet alle aktiven Magic-Bindings des Users für UI.
* Response: [{ deviceId, hostname, model, osVersion, magicEnrolledAt, releaseRequestedAt, releaseAvailableAt }]
* Listet alle gesch\u00fctzten Ger\u00e4te des Users f\u00fcr den Magic-Hub. Vereinigt:
* - Magic-Bindings (UserDevice.magicEnrolledAt) \u2014 via Magic-App registriert
* - ProtectedDevices \u2014 alter Native-App-DNS-Schutz-Flow (Multi-Device)
*
* Response-Items haben ein `source`-Flag:
* "magic" \u2192 voll verwaltet, unterst\u00fctzt request-release
* "protected" \u2192 alter Flow, nur Anzeige + revoke (TODO: own action)
*/
export default defineEventHandler(async (event) => {
const user = await requireUser(event);
const devices = await listMagicDevices(user.id);
// Berechne releaseAvailableAt (releaseRequestedAt + 24h)
const enriched = devices.map((d) => {
const [magic, protectedDevices] = await Promise.all([
listMagicDevices(user.id),
listProtectedDevices(user.id),
]);
const magicItems = magic.map((d) => {
let releaseAvailableAt: string | null = null;
if (d.releaseRequestedAt) {
const availableAt = new Date(
@ -22,8 +31,9 @@ export default defineEventHandler(async (event) => {
}
return {
source: "magic" as const,
deviceId: d.deviceId,
hostname: d.hostname,
hostname: d.hostname ?? "Unbenanntes Ger\u00e4t",
model: d.model,
osVersion: d.osVersion,
magicEnrolledAt: d.magicEnrolledAt.toISOString(),
@ -32,8 +42,20 @@ export default defineEventHandler(async (event) => {
};
});
const protectedItems = protectedDevices.map((d) => ({
source: "protected" as const,
deviceId: d.id,
hostname: d.label,
model: d.platform,
osVersion: null as string | null,
magicEnrolledAt: (d.installedAt ?? d.createdAt).toISOString(),
releaseRequestedAt: null as string | null,
releaseAvailableAt: null as string | null,
}));
// Magic-Bindings zuerst (neuste), dann alte ProtectedDevices
return {
success: true,
data: enriched,
data: [...magicItems, ...protectedItems],
};
});