feat(magic): redesign status dashboard with hero cards and device sheet
This commit is contained in:
parent
b5e89b5973
commit
298a0089bb
203
apps/rebreak-magic/app/composables/useProtectionStatus.ts
Normal file
203
apps/rebreak-magic/app/composables/useProtectionStatus.ts
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||||
|
import { useTauri, type IphoneDeviceState, type DesktopProtectionState, type MagicDeviceInfo } from "~/composables/useTauri";
|
||||||
|
import { useIphoneDevice, useMagicDevices, useMagicSession } from "~/composables/useMagicState";
|
||||||
|
|
||||||
|
export interface ProtectionStatus {
|
||||||
|
overall: "protected" | "partial" | "unprotected" | "unknown";
|
||||||
|
label: string;
|
||||||
|
message: string;
|
||||||
|
iosConnected: boolean;
|
||||||
|
iosProtected: boolean;
|
||||||
|
desktopProtected: boolean;
|
||||||
|
anyBackendDevice: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IPHONE_POLL_INTERVAL = 5000;
|
||||||
|
const DEVICE_REFRESH_INTERVAL = 30000;
|
||||||
|
|
||||||
|
function normalizeHostname(value: string): string {
|
||||||
|
return (value.toLowerCase().split(".")[0] ?? "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useProtectionStatus() {
|
||||||
|
const {
|
||||||
|
detectIphoneState,
|
||||||
|
getDesktopProtectionStatus,
|
||||||
|
getMagicDevices,
|
||||||
|
getMagicStatus,
|
||||||
|
getHostname,
|
||||||
|
} = useTauri();
|
||||||
|
|
||||||
|
const iphone = useIphoneDevice();
|
||||||
|
const devices = useMagicDevices();
|
||||||
|
const session = useMagicSession();
|
||||||
|
|
||||||
|
const desktopProtection = ref<DesktopProtectionState | null>(null);
|
||||||
|
const localHostname = ref<string | null>(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
const lastError = ref<string | null>(null);
|
||||||
|
const lastUpdated = ref<Date | null>(null);
|
||||||
|
|
||||||
|
let iphoneTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
let deviceTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
const iosConnected = computed(() => !!iphone.value);
|
||||||
|
const iosProtected = computed(() => {
|
||||||
|
if (!iphone.value) return false;
|
||||||
|
const hasEnrollment = iphone.value.installedProfileIDs?.includes("org.rebreak.mdm.enrollment");
|
||||||
|
const hasLock = iphone.value.installedProfileIDs?.includes("org.rebreak.protection.contentfilter.sideload");
|
||||||
|
return iphone.value.isSupervised && hasEnrollment && hasLock;
|
||||||
|
});
|
||||||
|
const currentBackendDevice = computed<MagicDeviceInfo | null>(() => {
|
||||||
|
if (!localHostname.value) return null;
|
||||||
|
const local = normalizeHostname(localHostname.value);
|
||||||
|
return devices.value.find((d) => normalizeHostname(d.hostname) === local) ?? null;
|
||||||
|
});
|
||||||
|
const desktopProtected = computed(() => !!desktopProtection.value?.active || !!currentBackendDevice.value);
|
||||||
|
const anyBackendDevice = computed(() => devices.value.length > 0);
|
||||||
|
|
||||||
|
const status = computed<ProtectionStatus>(() => {
|
||||||
|
const ios = iosConnected.value;
|
||||||
|
const iosOk = iosProtected.value;
|
||||||
|
const desktopOk = desktopProtected.value;
|
||||||
|
const backendOk = anyBackendDevice.value;
|
||||||
|
|
||||||
|
if (!ios && !desktopOk && !backendOk) {
|
||||||
|
return {
|
||||||
|
overall: "unprotected",
|
||||||
|
label: "Kein Schutz aktiv",
|
||||||
|
message: "Verbinde ein iPhone oder aktiviere den Desktop-Schutz.",
|
||||||
|
iosConnected: false,
|
||||||
|
iosProtected: false,
|
||||||
|
desktopProtected: false,
|
||||||
|
anyBackendDevice: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ios && !iosOk) {
|
||||||
|
return {
|
||||||
|
overall: "partial",
|
||||||
|
label: "iOS-Schutz unvollständig",
|
||||||
|
message: "Supervision oder Schutz-Profile fehlen. Führe das Setup fort.",
|
||||||
|
iosConnected: true,
|
||||||
|
iosProtected: false,
|
||||||
|
desktopProtected: desktopOk,
|
||||||
|
anyBackendDevice: backendOk,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iosOk && desktopOk) {
|
||||||
|
return {
|
||||||
|
overall: "protected",
|
||||||
|
label: "Vollständig geschützt",
|
||||||
|
message: "iPhone und Desktop sind aktiv geschützt.",
|
||||||
|
iosConnected: true,
|
||||||
|
iosProtected: true,
|
||||||
|
desktopProtected: true,
|
||||||
|
anyBackendDevice: backendOk,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iosOk || desktopOk || backendOk) {
|
||||||
|
return {
|
||||||
|
overall: "partial",
|
||||||
|
label: "Teilweise geschützt",
|
||||||
|
message: iosOk
|
||||||
|
? "iPhone geschützt. Desktop-Schutz ist optional."
|
||||||
|
: desktopOk
|
||||||
|
? "Desktop geschützt. iPhone noch nicht verbunden."
|
||||||
|
: "Ein registriertes Gerät vorhanden, aber keine lokale Schutzkontrolle.",
|
||||||
|
iosConnected: ios,
|
||||||
|
iosProtected: iosOk,
|
||||||
|
desktopProtected: desktopOk,
|
||||||
|
anyBackendDevice: backendOk,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
overall: "unknown",
|
||||||
|
label: "Status unbekannt",
|
||||||
|
message: "Schutzstatus konnte nicht ermittelt werden.",
|
||||||
|
iosConnected: false,
|
||||||
|
iosProtected: false,
|
||||||
|
desktopProtected: false,
|
||||||
|
anyBackendDevice: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
async function refreshIphone() {
|
||||||
|
try {
|
||||||
|
const state = await detectIphoneState();
|
||||||
|
iphone.value = state;
|
||||||
|
} catch (e: any) {
|
||||||
|
// Bei Fehler nicht crashen — Gerät ist einfach nicht verbunden
|
||||||
|
iphone.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshDesktop() {
|
||||||
|
try {
|
||||||
|
desktopProtection.value = await getDesktopProtectionStatus();
|
||||||
|
} catch (e: any) {
|
||||||
|
lastError.value = e?.message ?? "Desktop-Schutzstatus konnte nicht geladen werden";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshBackendDevices() {
|
||||||
|
try {
|
||||||
|
devices.value = await getMagicDevices();
|
||||||
|
if (session.value?.dnsToken) {
|
||||||
|
await getMagicStatus(session.value.dnsToken);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
lastError.value = e?.message ?? "Geräte konnten nicht geladen werden";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
loading.value = true;
|
||||||
|
lastError.value = null;
|
||||||
|
await Promise.all([
|
||||||
|
refreshIphone(),
|
||||||
|
refreshDesktop(),
|
||||||
|
refreshBackendDevices(),
|
||||||
|
refreshHostname(),
|
||||||
|
]);
|
||||||
|
lastUpdated.value = new Date();
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshHostname() {
|
||||||
|
try {
|
||||||
|
localHostname.value = await getHostname();
|
||||||
|
} catch (e: any) {
|
||||||
|
localHostname.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
refresh();
|
||||||
|
iphoneTimer = setInterval(refreshIphone, IPHONE_POLL_INTERVAL);
|
||||||
|
deviceTimer = setInterval(refreshBackendDevices, DEVICE_REFRESH_INTERVAL);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (iphoneTimer) clearInterval(iphoneTimer);
|
||||||
|
if (deviceTimer) clearInterval(deviceTimer);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
iphone: computed(() => iphone.value),
|
||||||
|
desktopProtection,
|
||||||
|
localHostname,
|
||||||
|
loading,
|
||||||
|
lastError,
|
||||||
|
lastUpdated,
|
||||||
|
iosConnected,
|
||||||
|
iosProtected,
|
||||||
|
desktopProtected,
|
||||||
|
anyBackendDevice,
|
||||||
|
refresh,
|
||||||
|
};
|
||||||
|
}
|
||||||
342
apps/rebreak-magic/app/pages/status.vue
Normal file
342
apps/rebreak-magic/app/pages/status.vue
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/40">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="bg-white/80 backdrop-blur-md border-b border-white/60 sticky top-0 z-10">
|
||||||
|
<div class="max-w-3xl mx-auto px-6 py-4">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="relative">
|
||||||
|
<img :src="rebreakIcon" alt="ReBreak" class="w-11 h-11 rounded-xl shadow-lg shadow-blue-500/20">
|
||||||
|
<div class="absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 bg-green-500 rounded-full border-2 border-white" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h1 class="text-lg font-extrabold text-gray-900 truncate">
|
||||||
|
{{ profile?.nickname || 'ReBreak Magic' }}
|
||||||
|
</h1>
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-500">
|
||||||
|
<UBadge v-if="profile?.plan" color="primary" variant="subtle" size="xs" class="font-bold">
|
||||||
|
{{ profile.plan }}
|
||||||
|
</UBadge>
|
||||||
|
<span v-if="session?.label" class="truncate">{{ session.label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-arrow-right-start-on-rectangle"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
@click="logout"
|
||||||
|
>
|
||||||
|
Abmelden
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="max-w-3xl mx-auto px-6 py-8 space-y-6">
|
||||||
|
<!-- Overall status card -->
|
||||||
|
<UCard class="shadow-sm border-gray-100 overflow-hidden">
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<div
|
||||||
|
class="w-14 h-14 rounded-2xl bg-gradient-to-br flex items-center justify-center shrink-0"
|
||||||
|
:class="statusCardClass"
|
||||||
|
>
|
||||||
|
<UIcon
|
||||||
|
:name="protection.status.value.overall === 'protected'
|
||||||
|
? 'i-heroicons-shield-check'
|
||||||
|
: protection.status.value.overall === 'partial'
|
||||||
|
? 'i-heroicons-shield-exclamation'
|
||||||
|
: 'i-heroicons-shield-slash'"
|
||||||
|
class="w-7 h-7 text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
|
<h2 class="text-lg font-bold text-gray-900">{{ protection.status.value.label }}</h2>
|
||||||
|
<UBadge
|
||||||
|
:color="badgeColorForOverall"
|
||||||
|
variant="subtle"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
{{ overallBadgeLabel }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-500 mt-0.5">{{ protection.status.value.message }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="protection.lastUpdated.value" class="hidden sm:block text-xs text-gray-400 text-right">
|
||||||
|
Aktualisiert<br>{{ formatTime(protection.lastUpdated.value) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<!-- Hero section -->
|
||||||
|
<section>
|
||||||
|
<h2 class="text-sm font-bold text-gray-500 uppercase tracking-wider mb-3">Aktives Gerät</h2>
|
||||||
|
|
||||||
|
<DeviceHeroCard
|
||||||
|
v-if="currentBackendDevice"
|
||||||
|
:device="currentBackendDevice"
|
||||||
|
:is-current="true"
|
||||||
|
:ios-stars="currentBackendDevice.platform === 'ios' ? iosStars : null"
|
||||||
|
@open="openDevice"
|
||||||
|
@toggle-protection="toggleProtection"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Empty / not registered state -->
|
||||||
|
<div v-else class="rounded-2xl bg-white ring-1 ring-gray-100 shadow-sm p-6 text-center">
|
||||||
|
<div class="w-16 h-16 mx-auto rounded-2xl bg-blue-50 flex items-center justify-center mb-4">
|
||||||
|
<UIcon name="i-heroicons-shield-exclamation" class="w-8 h-8 text-[var(--rebreak-primary)]" />
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-bold text-gray-900">Dieses Gerät ist nicht registriert</h3>
|
||||||
|
<p class="text-sm text-gray-500 mt-1 mb-4">Füge es hinzu, um den Schutz zu aktivieren.</p>
|
||||||
|
<div class="flex justify-center gap-3">
|
||||||
|
<UButton color="primary" icon="i-heroicons-device-phone-mobile" to="/detect">iPhone / iPad</UButton>
|
||||||
|
<UButton color="neutral" variant="soft" icon="i-heroicons-computer-desktop" to="/desktop-enroll">Diesen Computer</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Other devices list -->
|
||||||
|
<section>
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<h2 class="text-sm font-bold text-gray-500 uppercase tracking-wider">Weitere Geräte</h2>
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-arrow-path"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
size="xs"
|
||||||
|
:loading="loading"
|
||||||
|
@click="refresh"
|
||||||
|
>
|
||||||
|
Aktualisieren
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error" class="text-sm text-red-600 mb-4 p-4 bg-red-50 rounded-xl">{{ error }}</div>
|
||||||
|
|
||||||
|
<div v-if="loading && otherDevices.length === 0" class="py-12 text-center text-gray-500">
|
||||||
|
<UIcon name="i-heroicons-arrow-path" class="w-8 h-8 animate-spin mx-auto mb-3 text-[var(--rebreak-primary)]" />
|
||||||
|
<p class="font-semibold">Lade Geräte…</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="otherDevices.length === 0" class="py-10 text-center rounded-2xl bg-white ring-1 ring-gray-100">
|
||||||
|
<p class="text-sm text-gray-500">Keine weiteren Geräte registriert.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="space-y-3">
|
||||||
|
<DeviceListItem
|
||||||
|
v-for="device in otherDevices"
|
||||||
|
:key="device.deviceId"
|
||||||
|
:device="device"
|
||||||
|
:is-current="false"
|
||||||
|
@open="openDevice"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Add device shortcuts -->
|
||||||
|
<section class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<UButton
|
||||||
|
color="primary"
|
||||||
|
variant="soft"
|
||||||
|
size="lg"
|
||||||
|
block
|
||||||
|
icon="i-heroicons-device-phone-mobile"
|
||||||
|
to="/detect"
|
||||||
|
>
|
||||||
|
iPhone / iPad hinzufügen
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
color="neutral"
|
||||||
|
variant="soft"
|
||||||
|
size="lg"
|
||||||
|
block
|
||||||
|
icon="i-heroicons-computer-desktop"
|
||||||
|
to="/desktop-enroll"
|
||||||
|
>
|
||||||
|
Diesen Computer schützen
|
||||||
|
</UButton>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DeviceDetailSheet
|
||||||
|
v-model:open="sheetOpen"
|
||||||
|
:device="selectedDevice"
|
||||||
|
:ios-stars="selectedDevice?.platform === 'ios' && selectedDevice?.isCurrent ? iosStars : null"
|
||||||
|
@close="sheetOpen = false"
|
||||||
|
@toggle-protection="toggleProtection"
|
||||||
|
@start-cooldown="startCooldown"
|
||||||
|
@cancel-cooldown="cancelCooldown"
|
||||||
|
@remove="requestDeviceRelease"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from "vue";
|
||||||
|
import { useTauri, type UserProfile } from "~/composables/useTauri";
|
||||||
|
import { useMagicSession, useMagicDevices, useIphoneDevice } from "~/composables/useMagicState";
|
||||||
|
import { useProtectionStatus } from "~/composables/useProtectionStatus";
|
||||||
|
import { useDeviceStatus, type ComputedDevice } from "~/composables/useDeviceStatus";
|
||||||
|
import rebreakIcon from "~/assets/rebreak-icon.png";
|
||||||
|
|
||||||
|
const {
|
||||||
|
getMagicDevices,
|
||||||
|
requestRelease,
|
||||||
|
startCooldown: apiStartCooldown,
|
||||||
|
cancelCooldown: apiCancelCooldown,
|
||||||
|
logoutMagic,
|
||||||
|
fetchMe,
|
||||||
|
setDesktopProtectionStatus,
|
||||||
|
getPlatform,
|
||||||
|
} = useTauri();
|
||||||
|
|
||||||
|
const session = useMagicSession();
|
||||||
|
const devices = useMagicDevices();
|
||||||
|
const iphone = useIphoneDevice();
|
||||||
|
const protection = useProtectionStatus();
|
||||||
|
|
||||||
|
const profile = ref<UserProfile | null>(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
const sheetOpen = ref(false);
|
||||||
|
const selectedDevice = ref<ComputedDevice | null>(null);
|
||||||
|
const platformInfo = ref<{ platform: string } | null>(null);
|
||||||
|
|
||||||
|
// Share localHostname from protection composable with device status logic.
|
||||||
|
const localHostname = protection.localHostname;
|
||||||
|
const { currentBackendDevice, otherDevices, iosStars } = useDeviceStatus(devices, localHostname, iphone);
|
||||||
|
|
||||||
|
const statusCardClass = computed(() => {
|
||||||
|
switch (protection.status.value.overall) {
|
||||||
|
case "protected": return "from-emerald-500 to-teal-500";
|
||||||
|
case "partial": return "from-amber-500 to-orange-500";
|
||||||
|
case "unprotected": return "from-slate-400 to-slate-500";
|
||||||
|
default: return "from-blue-500 to-indigo-500";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const badgeColorForOverall = computed(() => {
|
||||||
|
switch (protection.status.value.overall) {
|
||||||
|
case "protected": return "success";
|
||||||
|
case "partial": return "warning";
|
||||||
|
case "unprotected": return "neutral";
|
||||||
|
default: return "primary";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const overallBadgeLabel = computed(() => {
|
||||||
|
switch (protection.status.value.overall) {
|
||||||
|
case "protected": return "Aktiv";
|
||||||
|
case "partial": return "Teilweise";
|
||||||
|
case "unprotected": return "Inaktiv";
|
||||||
|
default: return "Unbekannt";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadProfile();
|
||||||
|
await refresh();
|
||||||
|
try {
|
||||||
|
const info = await getPlatform();
|
||||||
|
platformInfo.value = { platform: info.platform };
|
||||||
|
} catch (e) {
|
||||||
|
platformInfo.value = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadProfile() {
|
||||||
|
try {
|
||||||
|
profile.value = await fetchMe();
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("Failed to load profile:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
try {
|
||||||
|
await protection.refresh();
|
||||||
|
devices.value = await getMagicDevices();
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e?.message ?? "Geräte konnten nicht geladen werden";
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(date: Date) {
|
||||||
|
return date.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDevice(device: ComputedDevice) {
|
||||||
|
selectedDevice.value = device;
|
||||||
|
sheetOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleProtection(device: ComputedDevice) {
|
||||||
|
if (!device.isCurrent) return;
|
||||||
|
if (device.platform === "mac" || device.platform === "windows") {
|
||||||
|
if (protection.status.value.desktopProtected) {
|
||||||
|
try {
|
||||||
|
await setDesktopProtectionStatus(false, device.platform);
|
||||||
|
await refresh();
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e?.message ?? "Schutz konnte nicht deaktiviert werden";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await navigateTo("/desktop-enroll");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startCooldown(device: ComputedDevice, minutes: number) {
|
||||||
|
if (!device.isCurrent) return;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
await apiStartCooldown(device.deviceId, minutes);
|
||||||
|
await refresh();
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e?.message ?? "Cooldown konnte nicht gestartet werden";
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cancelCooldown(device: ComputedDevice) {
|
||||||
|
if (!device.isCurrent) return;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
await apiCancelCooldown(device.deviceId);
|
||||||
|
await refresh();
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e?.message ?? "Cooldown konnte nicht beendet werden";
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestDeviceRelease(device: ComputedDevice) {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
await requestRelease(device.deviceId);
|
||||||
|
sheetOpen.value = false;
|
||||||
|
await refresh();
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e?.message ?? "Freigabe fehlgeschlagen";
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
try {
|
||||||
|
await logoutMagic();
|
||||||
|
session.value = null;
|
||||||
|
devices.value = [];
|
||||||
|
await navigateTo("/");
|
||||||
|
} catch (e: any) {
|
||||||
|
error.value = e?.message ?? "Abmelden fehlgeschlagen";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Loading…
x
Reference in New Issue
Block a user