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 let existing: Bool
} }
enum MagicDeviceSource: String, Codable {
case magic
case protected
}
struct MagicDevice: Codable, Identifiable { struct MagicDevice: Codable, Identifiable {
let source: MagicDeviceSource?
let deviceId: String let deviceId: String
let hostname: String let hostname: String
let model: String? let model: String?
@ -16,8 +22,11 @@ struct MagicDevice: Codable, Identifiable {
let magicEnrolledAt: String let magicEnrolledAt: String
let releaseRequestedAt: String? let releaseRequestedAt: String?
let releaseAvailableAt: 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? { var enrolledDate: Date? {
ISO8601DateFormatter().date(from: magicEnrolledAt) ISO8601DateFormatter().date(from: magicEnrolledAt)

View File

@ -279,8 +279,19 @@ private struct HubDeviceRow: View {
.frame(width: 28) .frame(width: 28)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(device.hostname) HStack(spacing: 6) {
.font(.callout.bold()) 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) { HStack(spacing: 8) {
if let model = device.model { if let model = device.model {
Text(model).font(.caption2).foregroundStyle(.secondary) Text(model).font(.caption2).foregroundStyle(.secondary)
@ -294,7 +305,11 @@ private struct HubDeviceRow: View {
Spacer() 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) { VStack(alignment: .trailing, spacing: 2) {
Label("Freigabe läuft", systemImage: "hourglass") Label("Freigabe läuft", systemImage: "hourglass")
.font(.caption2.bold()) .font(.caption2.bold())

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +1,27 @@
import { listMagicDevices } from "../../db/devices"; import { listMagicDevices } from "../../db/devices";
import { listProtectedDevices } from "../../db/protectedDevices";
import { requireUser } from "../../utils/auth"; import { requireUser } from "../../utils/auth";
/** /**
* GET /api/magic/devices * GET /api/magic/devices
* *
* Listet alle aktiven Magic-Bindings des Users für UI. * Listet alle gesch\u00fctzten Ger\u00e4te des Users f\u00fcr den Magic-Hub. Vereinigt:
* Response: [{ deviceId, hostname, model, osVersion, magicEnrolledAt, releaseRequestedAt, releaseAvailableAt }] * - 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) => { export default defineEventHandler(async (event) => {
const user = await requireUser(event); const user = await requireUser(event);
const devices = await listMagicDevices(user.id);
// Berechne releaseAvailableAt (releaseRequestedAt + 24h) const [magic, protectedDevices] = await Promise.all([
const enriched = devices.map((d) => { listMagicDevices(user.id),
listProtectedDevices(user.id),
]);
const magicItems = magic.map((d) => {
let releaseAvailableAt: string | null = null; let releaseAvailableAt: string | null = null;
if (d.releaseRequestedAt) { if (d.releaseRequestedAt) {
const availableAt = new Date( const availableAt = new Date(
@ -22,8 +31,9 @@ export default defineEventHandler(async (event) => {
} }
return { return {
source: "magic" as const,
deviceId: d.deviceId, deviceId: d.deviceId,
hostname: d.hostname, hostname: d.hostname ?? "Unbenanntes Ger\u00e4t",
model: d.model, model: d.model,
osVersion: d.osVersion, osVersion: d.osVersion,
magicEnrolledAt: d.magicEnrolledAt.toISOString(), 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 { return {
success: true, success: true,
data: enriched, data: [...magicItems, ...protectedItems],
}; };
}); });