Include recent Magic app work: Tauri native shell, iOS device detection via supervise-magic sidecar, MDM client, local HTTP server, new pages (detect, enroll, supervise, sideload, pair, preflight, configure, done), and updated device section/status UI.
116 lines
3.9 KiB
Vue
116 lines
3.9 KiB
Vue
<template>
|
|
<div>
|
|
<!-- Toggle button (small, bottom-right) -->
|
|
<UButton icon="i-heroicons-bug-ant" color="neutral" variant="ghost" size="xs"
|
|
class="fixed bottom-3 right-3 z-50 opacity-60 hover:opacity-100" @click="open = true">
|
|
Logs
|
|
</UButton>
|
|
|
|
<UDrawer v-model:open="open" direction="bottom" :handle="true" :dismissible="true">
|
|
<template #body>
|
|
<div class="flex flex-col h-full">
|
|
<div class="flex items-center justify-end gap-2 pb-3 border-b border-gray-200">
|
|
<UButton size="xs" color="neutral" variant="ghost" icon="i-heroicons-clipboard-document" @click="copyLogs">
|
|
Kopieren
|
|
</UButton>
|
|
<UButton size="xs" color="error" variant="ghost" icon="i-heroicons-trash" @click="clear">
|
|
Löschen
|
|
</UButton>
|
|
</div>
|
|
|
|
<div class="flex-1 overflow-auto pt-3 space-y-2 bg-gray-950 -mx-4 px-4">
|
|
<div v-for="entry in logs" :key="entry.id" class="text-xs font-mono p-2 rounded border"
|
|
:class="entryClass(entry.level)">
|
|
<div class="flex items-center gap-2 opacity-70">
|
|
<span>{{ formatTime(entry.timestamp) }}</span>
|
|
<UBadge :color="badgeColor(entry.level)" size="xs" variant="solid">
|
|
{{ entry.level.toUpperCase() }}
|
|
</UBadge>
|
|
</div>
|
|
<div class="mt-1 whitespace-pre-wrap break-all">{{ entry.message }}</div>
|
|
<details v-if="entry.details" class="mt-2 group">
|
|
<summary class="cursor-pointer hover:underline flex items-center gap-2">
|
|
<span>Details</span>
|
|
<UButton size="2xs" color="neutral" variant="ghost" icon="i-heroicons-clipboard-document"
|
|
class="opacity-0 group-hover:opacity-100 transition-opacity"
|
|
@click.stop="copyDetails(entry.details)">
|
|
Kopieren
|
|
</UButton>
|
|
</summary>
|
|
<div class="mt-1 p-2 bg-black/30 rounded whitespace-pre-wrap break-all">{{ entry.details }}</div>
|
|
</details>
|
|
</div>
|
|
|
|
<div v-if="logs.length === 0" class="text-gray-500 text-center py-8">
|
|
Noch keine Logs vorhanden.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</UDrawer>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, onUnmounted } from "vue";
|
|
import { useLogger, useLoggerState, type LogEntry } from "~/composables/useLogger";
|
|
|
|
const open = ref(false);
|
|
const logs = useLoggerState();
|
|
const { clear, exportLogs } = useLogger();
|
|
|
|
function entryClass(level: LogEntry["level"]) {
|
|
switch (level) {
|
|
case "error": return "bg-red-950/50 border-red-800 text-red-100";
|
|
case "warn": return "bg-yellow-950/50 border-yellow-800 text-yellow-100";
|
|
case "info": return "bg-blue-950/50 border-blue-800 text-blue-100";
|
|
default: return "bg-gray-900 border-gray-800 text-gray-200";
|
|
}
|
|
}
|
|
|
|
function badgeColor(level: LogEntry["level"]) {
|
|
switch (level) {
|
|
case "error": return "red";
|
|
case "warn": return "yellow";
|
|
case "info": return "blue";
|
|
default: return "neutral";
|
|
}
|
|
}
|
|
|
|
function formatTime(date: Date) {
|
|
return date.toLocaleTimeString("de-DE", { hour12: false }) + "." + String(date.getMilliseconds()).padStart(3, "0");
|
|
}
|
|
|
|
async function copyLogs() {
|
|
try {
|
|
await navigator.clipboard.writeText(exportLogs());
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
async function copyDetails(details: string) {
|
|
try {
|
|
await navigator.clipboard.writeText(details);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
// Keyboard shortcut: Cmd/Ctrl + Shift + L
|
|
function onKeyDown(e: KeyboardEvent) {
|
|
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "l") {
|
|
e.preventDefault();
|
|
open.value = !open.value;
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
window.addEventListener("keydown", onKeyDown);
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener("keydown", onKeyDown);
|
|
});
|
|
</script>
|