288 lines
8.9 KiB
Vue
288 lines
8.9 KiB
Vue
<template>
|
|
<div
|
|
class="relative overflow-hidden rounded-2xl bg-white dark:bg-gray-900 shadow-sm ring-1 ring-gray-100 dark:ring-gray-800 p-5"
|
|
>
|
|
<div class="flex items-start gap-4">
|
|
<div
|
|
class="shrink-0 w-12 h-12 rounded-xl bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-gray-800 dark:to-gray-700 flex items-center justify-center"
|
|
>
|
|
<UIcon
|
|
name="i-heroicons-device-phone-mobile"
|
|
class="w-6 h-6 text-[var(--rebreak-primary)]"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-start justify-between gap-3">
|
|
<div>
|
|
<h3 class="text-base font-bold text-gray-900 dark:text-white truncate">
|
|
{{ deviceName }}
|
|
</h3>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
|
{{ platformLabel }}
|
|
<span v-if="device.osVersion">· iOS {{ device.osVersion }}</span>
|
|
</p>
|
|
</div>
|
|
|
|
<UBadge
|
|
:color="statusColor"
|
|
:variant="statusVariant"
|
|
size="sm"
|
|
class="font-bold shrink-0"
|
|
>
|
|
{{ statusLabel }}
|
|
</UBadge>
|
|
</div>
|
|
|
|
<div
|
|
v-if="isConnected && iosStars"
|
|
class="mt-4 rounded-xl bg-gray-50 dark:bg-gray-800/50 p-4"
|
|
>
|
|
<div class="flex items-center justify-between mb-2">
|
|
<span class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
|
iOS Schutz-Status
|
|
</span>
|
|
<span class="text-xs text-green-600 dark:text-green-400 font-medium">
|
|
Verbunden
|
|
</span>
|
|
</div>
|
|
<IosStarRating
|
|
:enrollment="iosStars.enrollment"
|
|
:sideload="iosStars.sideload"
|
|
:app="iosStars.app"
|
|
/>
|
|
<ul class="mt-3 space-y-1.5 text-sm text-gray-700 dark:text-gray-200">
|
|
<li class="flex items-center justify-between">
|
|
<span>Supervised</span>
|
|
<span :class="iosStars.isSupervised ? 'text-green-600 dark:text-green-400' : 'text-gray-400 dark:text-gray-500'">
|
|
{{ iosStars.isSupervised ? "Ja" : "Nein" }}
|
|
</span>
|
|
</li>
|
|
<li class="flex items-center justify-between">
|
|
<span>Enrollment</span>
|
|
<span :class="iosStars.enrollment ? 'text-green-600 dark:text-green-400' : 'text-gray-400 dark:text-gray-500'">
|
|
{{ iosStars.enrollment ? "Ja" : "Nein" }}
|
|
</span>
|
|
</li>
|
|
<li class="flex items-center justify-between">
|
|
<span>Sideload/Lock</span>
|
|
<span :class="iosStars.sideload ? 'text-green-600 dark:text-green-400' : 'text-gray-400 dark:text-gray-500'">
|
|
{{ iosStars.sideload ? "Ja" : "Nein" }}
|
|
</span>
|
|
</li>
|
|
<li class="flex items-center justify-between">
|
|
<span>ReBreak App</span>
|
|
<span :class="iosStars.app ? 'text-green-600 dark:text-green-400' : 'text-gray-400 dark:text-gray-500'">
|
|
{{ iosStars.app ? "Ja" : "Nein" }}
|
|
</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div
|
|
v-else
|
|
class="mt-3 text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1.5"
|
|
>
|
|
<UIcon name="i-heroicons-information-circle" class="w-4 h-4" />
|
|
<span>Zum Live-Status iPhone per USB verbinden und aktualisieren</span>
|
|
</div>
|
|
|
|
<div class="mt-4 flex items-center gap-3">
|
|
<UButton
|
|
v-if="action.to"
|
|
:color="action.color"
|
|
:variant="action.variant"
|
|
size="sm"
|
|
:icon="action.icon"
|
|
:to="action.to"
|
|
>
|
|
{{ action.label }}
|
|
</UButton>
|
|
<UButton
|
|
v-else
|
|
:color="action.color"
|
|
:variant="action.variant"
|
|
size="sm"
|
|
:icon="action.icon"
|
|
:loading="syncing"
|
|
@click="onActionClick"
|
|
>
|
|
{{ action.label }}
|
|
</UButton>
|
|
|
|
<UButton
|
|
color="neutral"
|
|
variant="ghost"
|
|
size="sm"
|
|
trailing-icon="i-heroicons-chevron-right"
|
|
@click="emit('open', device)"
|
|
>
|
|
Details
|
|
</UButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref } from "vue";
|
|
import type { ComputedDevice, DeviceStatus } from "~/composables/useDeviceStatus";
|
|
import { REBREAK_MDM_VERSION, getInstalledMdmVersion, type IphoneDeviceState } from "~/composables/useTauri";
|
|
|
|
const props = defineProps<{
|
|
device: ComputedDevice;
|
|
iphone: IphoneDeviceState | null;
|
|
isConnected: boolean;
|
|
inGracePeriod?: boolean;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
(e: "sync", device: ComputedDevice): void;
|
|
(e: "open", device: ComputedDevice): void;
|
|
(e: "remove", device: ComputedDevice): void;
|
|
}>();
|
|
|
|
const ENROLLMENT_PROFILE_ID = "org.rebreak.mdm.enrollment";
|
|
const LOCK_PROFILE_ID = "org.rebreak.protection.contentfilter.sideload";
|
|
const APP_BUNDLE_ID = "org.rebreak.app";
|
|
|
|
const syncing = ref(false);
|
|
|
|
const iosStars = computed(() => {
|
|
if (!props.isConnected || !props.iphone) return null;
|
|
return {
|
|
enrollment: props.iphone.installedProfileIDs?.includes(ENROLLMENT_PROFILE_ID) ?? false,
|
|
sideload: props.iphone.installedProfileIDs?.includes(LOCK_PROFILE_ID) ?? false,
|
|
app: props.iphone.installedAppBundleIDs?.includes(APP_BUNDLE_ID) ?? false,
|
|
isSupervised: props.iphone.isSupervised,
|
|
};
|
|
});
|
|
|
|
interface IosAction {
|
|
label: string;
|
|
icon: string;
|
|
color: "primary" | "warning" | "success" | "error" | "neutral";
|
|
variant: "solid" | "soft" | "outline" | "ghost";
|
|
to?: string;
|
|
}
|
|
|
|
const action = computed<IosAction>(() => {
|
|
if (props.inGracePeriod) {
|
|
return {
|
|
label: "ReBreak entfernen",
|
|
icon: "i-heroicons-trash",
|
|
color: "error",
|
|
variant: "soft",
|
|
};
|
|
}
|
|
|
|
if (!props.isConnected || !props.iphone) {
|
|
return {
|
|
label: "iPhone verbinden",
|
|
icon: "i-heroicons-link",
|
|
color: "primary",
|
|
variant: "solid",
|
|
to: "/detect",
|
|
};
|
|
}
|
|
|
|
if (!props.iphone.isSupervised) {
|
|
return {
|
|
label: "Supervisen",
|
|
icon: "i-heroicons-shield-check",
|
|
color: "primary",
|
|
variant: "solid",
|
|
to: "/supervise",
|
|
};
|
|
}
|
|
|
|
if (!props.iphone.installedProfileIDs?.includes(ENROLLMENT_PROFILE_ID)) {
|
|
return {
|
|
label: "Enrollen",
|
|
icon: "i-heroicons-document-check",
|
|
color: "primary",
|
|
variant: "solid",
|
|
to: "/enroll",
|
|
};
|
|
}
|
|
|
|
if (!props.iphone.installedProfileIDs?.includes(LOCK_PROFILE_ID)) {
|
|
return {
|
|
label: "Sideload installieren",
|
|
icon: "i-heroicons-lock-closed",
|
|
color: "warning",
|
|
variant: "solid",
|
|
to: "/sideload",
|
|
};
|
|
}
|
|
|
|
if (!props.iphone.installedAppBundleIDs?.includes(APP_BUNDLE_ID)) {
|
|
return {
|
|
label: "App installieren",
|
|
icon: "i-heroicons-arrow-down-tray",
|
|
color: "primary",
|
|
variant: "solid",
|
|
to: "/sideload",
|
|
};
|
|
}
|
|
|
|
const installedMdmVersion = getInstalledMdmVersion(props.iphone.installedProfileIDs ?? []);
|
|
if (installedMdmVersion && installedMdmVersion !== REBREAK_MDM_VERSION) {
|
|
return {
|
|
label: "MDM-Update installieren",
|
|
icon: "i-heroicons-arrow-up-tray",
|
|
color: "warning",
|
|
variant: "soft",
|
|
};
|
|
}
|
|
|
|
return {
|
|
label: "Synchronisieren",
|
|
icon: "i-heroicons-arrow-path",
|
|
color: "success",
|
|
variant: "soft",
|
|
};
|
|
});
|
|
|
|
const deviceName = computed(() => {
|
|
if (props.device.name) return props.device.name;
|
|
return "iOS-Gerät";
|
|
});
|
|
|
|
const platformLabel = computed(() => {
|
|
const model = (props.device.model ?? "").toLowerCase();
|
|
if (model.startsWith("ipad")) return "iPad";
|
|
if (model.startsWith("iphone") || model.startsWith("ios")) return "iPhone";
|
|
return "iOS";
|
|
});
|
|
|
|
const statusConfig: Record<
|
|
DeviceStatus,
|
|
{ label: string; color: "success" | "warning" | "error" | "neutral"; variant: "subtle" | "outline" }
|
|
> = {
|
|
active: { label: "Aktiv", color: "success", variant: "subtle" },
|
|
cooldown: { label: "Schlafmodus", color: "warning", variant: "subtle" },
|
|
revoked: { label: "Widerrufen", color: "error", variant: "subtle" },
|
|
pending: { label: "Ausstehend", color: "neutral", variant: "subtle" },
|
|
unprotected: { label: "Ungeschützt", color: "neutral", variant: "outline" },
|
|
};
|
|
|
|
const statusLabel = computed(() => statusConfig[props.device.status].label);
|
|
const statusColor = computed(() => statusConfig[props.device.status].color);
|
|
const statusVariant = computed(() => statusConfig[props.device.status].variant);
|
|
|
|
function onActionClick() {
|
|
if (props.inGracePeriod) {
|
|
emit("remove", props.device);
|
|
return;
|
|
}
|
|
syncing.value = true;
|
|
emit("sync", props.device);
|
|
// Parent is responsible for resetting syncing state via prop/loading if needed.
|
|
setTimeout(() => {
|
|
syncing.value = false;
|
|
}, 800);
|
|
}
|
|
</script>
|