Compare commits
2 commits
280a973476
...
a77bcd0355
| Author | SHA1 | Date | |
|---|---|---|---|
| a77bcd0355 | |||
| fca59545cc |
8 changed files with 320 additions and 44 deletions
|
|
@ -215,3 +215,17 @@ export function syncProtocol(protocol: Protocol): Promise<SyncResult> {
|
||||||
export function pdfUrl(serverProtocolId: number): string {
|
export function pdfUrl(serverProtocolId: number): string {
|
||||||
return `${serverUrl}${API_PATH}/pdf.php?id=${serverProtocolId}&jwt=${encodeURIComponent(token)}`;
|
return `${serverUrl}${API_PATH}/pdf.php?id=${serverProtocolId}&jwt=${encodeURIComponent(token)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DebugLogUpload {
|
||||||
|
entries: { ts: number; level: string; msg: string }[];
|
||||||
|
appVersion: string;
|
||||||
|
device: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Debug-Log-Einträge zum Server übertragen (Endpoint applog.php) */
|
||||||
|
export function uploadDebugLog(payload: DebugLogUpload): Promise<{ ok: boolean; stored: number }> {
|
||||||
|
return request<{ ok: boolean; stored: number }>('applog.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { page } from '$app/stores';
|
||||||
import { sync } from '$lib/sync.svelte';
|
import { ChevronLeft, Settings } from 'lucide-svelte';
|
||||||
import { ChevronLeft, RefreshCw } from 'lucide-svelte';
|
|
||||||
|
|
||||||
let {
|
let {
|
||||||
title,
|
title,
|
||||||
|
|
@ -9,14 +8,8 @@
|
||||||
subtitle = '',
|
subtitle = '',
|
||||||
}: { title: string; back?: boolean; subtitle?: string } = $props();
|
}: { title: string; back?: boolean; subtitle?: string } = $props();
|
||||||
|
|
||||||
// Sync-Ampel: grün=ok, gelb=läuft/offen, rot=Fehler
|
// Zahnrad auf der Einstellungs-Seite selbst ausblenden
|
||||||
const dot = $derived(
|
const onSettings = $derived($page.url.pathname.startsWith('/einstellungen'));
|
||||||
sync.status === 'error'
|
|
||||||
? 'bg-red-500'
|
|
||||||
: sync.pendingCount > 0 || sync.status === 'syncing'
|
|
||||||
? 'bg-amber-400'
|
|
||||||
: 'bg-emerald-500',
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class="flex items-center gap-2 border-b border-zinc-800 bg-zinc-900 px-3 pb-3 safe-top">
|
<header class="flex items-center gap-2 border-b border-zinc-800 bg-zinc-900 px-3 pb-3 safe-top">
|
||||||
|
|
@ -31,13 +24,13 @@
|
||||||
<p class="truncate text-xs text-zinc-400">{subtitle}</p>
|
<p class="truncate text-xs text-zinc-400">{subtitle}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<button
|
{#if !onSettings}
|
||||||
class="flex items-center gap-1 rounded px-2 py-1 text-xs text-zinc-300 active:bg-zinc-800"
|
<a
|
||||||
onclick={() => sync.syncManual()}
|
class="rounded p-1.5 text-zinc-300 active:bg-zinc-800"
|
||||||
title="Synchronisieren"
|
href="/einstellungen/"
|
||||||
>
|
aria-label="Einstellungen"
|
||||||
<span class="h-2.5 w-2.5 rounded-full {dot}"></span>
|
>
|
||||||
{#if sync.pendingCount > 0}<span>{sync.pendingCount}</span>{/if}
|
<Settings size={22} />
|
||||||
<RefreshCw size={14} class={sync.status === 'syncing' ? 'animate-spin' : ''} />
|
</a>
|
||||||
</button>
|
{/if}
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Tool } from '$lib/tools/types';
|
import type { Tool } from '$lib/tools/types';
|
||||||
import type { Device, Protocol } from '$lib/types';
|
import type { Device, Protocol } from '$lib/types';
|
||||||
|
import { debugLog } from '$lib/debuglog.svelte';
|
||||||
import { X } from 'lucide-svelte';
|
import { X } from 'lucide-svelte';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
|
|
@ -32,6 +33,7 @@
|
||||||
onclose();
|
onclose();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e instanceof Error ? e.message : 'Fehler beim Ausführen';
|
error = e instanceof Error ? e.message : 'Fehler beim Ausführen';
|
||||||
|
debugLog.add('error', `Tool "${tool.id}":`, e);
|
||||||
} finally {
|
} finally {
|
||||||
busy = false;
|
busy = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
159
src/lib/debuglog.svelte.ts
Normal file
159
src/lib/debuglog.svelte.ts
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
/**
|
||||||
|
* Debug-Log (Svelte 5 Runes).
|
||||||
|
*
|
||||||
|
* Fängt globale Fehler (window.error / unhandledrejection) und alle
|
||||||
|
* console.*-Ausgaben in einem Ringpuffer ab. So lässt sich nachvollziehen,
|
||||||
|
* warum z.B. ein Scan oder die Synchronisierung scheitert — ohne Kabel,
|
||||||
|
* ohne Logcat.
|
||||||
|
*
|
||||||
|
* Zwei Wege:
|
||||||
|
* - lokal sichtbar unter Einstellungen → Debug-Log
|
||||||
|
* - automatischer Upload zum Server (api/applog.php → llx_netdiag_applog),
|
||||||
|
* damit die Logs auch ohne Gerät auswertbar sind.
|
||||||
|
*
|
||||||
|
* Der Puffer wird in den Preferences gespiegelt und übersteht so einen
|
||||||
|
* App-Neustart (inkl. der Markierung, was schon hochgeladen wurde).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Preferences } from '@capacitor/preferences';
|
||||||
|
import { isLoggedIn, uploadDebugLog } from './api';
|
||||||
|
import { APP_VERSION } from './updater';
|
||||||
|
|
||||||
|
export type LogLevel = 'info' | 'warn' | 'error';
|
||||||
|
|
||||||
|
export interface LogEntry {
|
||||||
|
ts: number;
|
||||||
|
level: LogLevel;
|
||||||
|
msg: string;
|
||||||
|
/** true, sobald der Eintrag erfolgreich zum Server übertragen wurde */
|
||||||
|
sent?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_ENTRIES = 200;
|
||||||
|
const STORE_KEY = 'netdiag_debuglog';
|
||||||
|
const UPLOAD_THROTTLE_MS = 10_000;
|
||||||
|
|
||||||
|
/** Beliebigen Wert für die Log-Zeile lesbar machen */
|
||||||
|
function fmt(v: unknown): string {
|
||||||
|
if (typeof v === 'string') return v;
|
||||||
|
if (v instanceof Error) return `${v.name}: ${v.message}`;
|
||||||
|
try {
|
||||||
|
return JSON.stringify(v);
|
||||||
|
} catch {
|
||||||
|
return String(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DebugLog {
|
||||||
|
entries = $state<LogEntry[]>([]);
|
||||||
|
uploadState = $state<'idle' | 'sending' | 'error'>('idle');
|
||||||
|
|
||||||
|
private saveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
private uploadTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
/** Neuen Eintrag anhängen (Ringpuffer) */
|
||||||
|
add(level: LogLevel, ...parts: unknown[]): void {
|
||||||
|
const msg = parts.map(fmt).join(' ').trim();
|
||||||
|
this.entries = [...this.entries.slice(-(MAX_ENTRIES - 1)), { ts: Date.now(), level, msg }];
|
||||||
|
this.scheduleSave();
|
||||||
|
this.scheduleUpload();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Log leeren */
|
||||||
|
clear(): void {
|
||||||
|
this.entries = [];
|
||||||
|
void Preferences.remove({ key: STORE_KEY });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gesamtes Log als Klartext */
|
||||||
|
asText(): string {
|
||||||
|
return this.entries
|
||||||
|
.map((e) => `${new Date(e.ts).toISOString()} [${e.level.toUpperCase()}] ${e.msg}`)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Persistierten Puffer beim Start laden */
|
||||||
|
async load(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const raw = (await Preferences.get({ key: STORE_KEY })).value;
|
||||||
|
if (raw) this.entries = JSON.parse(raw) as LogEntry[];
|
||||||
|
} catch {
|
||||||
|
/* defektes Log ignorieren */
|
||||||
|
}
|
||||||
|
this.scheduleUpload();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Noch nicht übertragene Einträge zum Server schieben.
|
||||||
|
* Best effort — wirft nie und loggt eigene Fehler NICHT (sonst Endlosschleife).
|
||||||
|
*/
|
||||||
|
async flush(): Promise<void> {
|
||||||
|
if (this.uploadState === 'sending' || !isLoggedIn()) return;
|
||||||
|
const batch = this.entries.filter((e) => !e.sent).slice(0, 200);
|
||||||
|
if (batch.length === 0) return;
|
||||||
|
|
||||||
|
this.uploadState = 'sending';
|
||||||
|
try {
|
||||||
|
await uploadDebugLog({
|
||||||
|
entries: batch.map((e) => ({ ts: e.ts, level: e.level, msg: e.msg })),
|
||||||
|
appVersion: APP_VERSION,
|
||||||
|
device: typeof navigator !== 'undefined' ? navigator.userAgent.slice(0, 128) : '',
|
||||||
|
});
|
||||||
|
const sent = new Set(batch);
|
||||||
|
this.entries = this.entries.map((e) => (sent.has(e) ? { ...e, sent: true } : e));
|
||||||
|
this.uploadState = 'idle';
|
||||||
|
void Preferences.set({ key: STORE_KEY, value: JSON.stringify(this.entries) });
|
||||||
|
} catch {
|
||||||
|
this.uploadState = 'error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleSave(): void {
|
||||||
|
if (this.saveTimer) clearTimeout(this.saveTimer);
|
||||||
|
this.saveTimer = setTimeout(() => {
|
||||||
|
void Preferences.set({ key: STORE_KEY, value: JSON.stringify(this.entries) });
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Upload drosseln: spätestens 10 s nach der ersten neuen Meldung senden */
|
||||||
|
private scheduleUpload(): void {
|
||||||
|
if (this.uploadTimer) return;
|
||||||
|
this.uploadTimer = setTimeout(() => {
|
||||||
|
this.uploadTimer = null;
|
||||||
|
void this.flush();
|
||||||
|
}, UPLOAD_THROTTLE_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const debugLog = new DebugLog();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Globale Fehler-Hooks + console.*-Wrapper installieren.
|
||||||
|
* Muss einmalig und möglichst früh aufgerufen werden.
|
||||||
|
*/
|
||||||
|
let installed = false;
|
||||||
|
export function initDebugLog(): void {
|
||||||
|
if (installed || typeof window === 'undefined') return;
|
||||||
|
installed = true;
|
||||||
|
|
||||||
|
void debugLog.load();
|
||||||
|
|
||||||
|
window.addEventListener('error', (ev) => {
|
||||||
|
const ort = ev.filename ? ` (${ev.filename}:${ev.lineno})` : '';
|
||||||
|
debugLog.add('error', (ev.message || 'Fehler') + ort);
|
||||||
|
});
|
||||||
|
window.addEventListener('unhandledrejection', (ev) => {
|
||||||
|
debugLog.add('error', 'Unbehandelt:', ev.reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
// console.* mitschneiden, Originalausgabe bleibt erhalten
|
||||||
|
for (const level of ['log', 'warn', 'error'] as const) {
|
||||||
|
const orig = console[level].bind(console);
|
||||||
|
console[level] = (...args: unknown[]) => {
|
||||||
|
debugLog.add(level === 'log' ? 'info' : level, ...args);
|
||||||
|
orig(...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog.add('info', `Debug-Log gestartet — App ${APP_VERSION}`);
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ import { Network } from '@capacitor/network';
|
||||||
import { ApiError, isLoggedIn, syncProtocol } from './api';
|
import { ApiError, isLoggedIn, syncProtocol } from './api';
|
||||||
import { getDirtyProtocols, saveProtocol } from './db';
|
import { getDirtyProtocols, saveProtocol } from './db';
|
||||||
import { toast } from './toast.svelte';
|
import { toast } from './toast.svelte';
|
||||||
|
import { debugLog } from './debuglog.svelte';
|
||||||
|
|
||||||
type SyncStatus = 'idle' | 'syncing' | 'offline' | 'error';
|
type SyncStatus = 'idle' | 'syncing' | 'offline' | 'error';
|
||||||
|
|
||||||
|
|
@ -77,6 +78,7 @@ class SyncState {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.lastError = e instanceof ApiError ? e.message : 'Sync-Fehler';
|
this.lastError = e instanceof ApiError ? e.message : 'Sync-Fehler';
|
||||||
this.status = 'error';
|
this.status = 'error';
|
||||||
|
debugLog.add('error', `Sync (Protokoll ${p.clientUuid}):`, e);
|
||||||
await this.refreshPending();
|
await this.refreshPending();
|
||||||
return; // beim nächsten Lauf erneut versuchen
|
return; // beim nächsten Lauf erneut versuchen
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
import { initDb } from '$lib/db';
|
import { initDb } from '$lib/db';
|
||||||
import { registerBackListener, removeBackListener } from '$lib/backButton.svelte';
|
import { registerBackListener, removeBackListener } from '$lib/backButton.svelte';
|
||||||
import { checkForUpdate, openUpdate, type UpdateInfo } from '$lib/updater';
|
import { checkForUpdate, openUpdate, type UpdateInfo } from '$lib/updater';
|
||||||
|
import { initDebugLog } from '$lib/debuglog.svelte';
|
||||||
import Toast from '$lib/components/Toast.svelte';
|
import Toast from '$lib/components/Toast.svelte';
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
@ -19,6 +20,7 @@
|
||||||
const HOME = '/auftraege/';
|
const HOME = '/auftraege/';
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
initDebugLog(); // zuerst — damit auch Startfehler erfasst werden
|
||||||
await auth.init();
|
await auth.init();
|
||||||
await initDb();
|
await initDb();
|
||||||
if (auth.loggedIn) await sync.start();
|
if (auth.loggedIn) await sync.start();
|
||||||
|
|
|
||||||
62
src/routes/debug/+page.svelte
Normal file
62
src/routes/debug/+page.svelte
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import AppHeader from '$lib/components/AppHeader.svelte';
|
||||||
|
import { debugLog } from '$lib/debuglog.svelte';
|
||||||
|
|
||||||
|
const levelColor: Record<string, string> = {
|
||||||
|
info: 'text-zinc-400',
|
||||||
|
warn: 'text-amber-400',
|
||||||
|
error: 'text-red-400',
|
||||||
|
};
|
||||||
|
|
||||||
|
function fmtTime(ts: number): string {
|
||||||
|
const d = new Date(ts);
|
||||||
|
return (
|
||||||
|
d.toLocaleTimeString('de-DE') + '.' + String(d.getMilliseconds()).padStart(3, '0')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anzahl noch nicht übertragener Einträge
|
||||||
|
const offen = $derived(debugLog.entries.filter((e) => !e.sent).length);
|
||||||
|
|
||||||
|
const uploadText = $derived(
|
||||||
|
debugLog.uploadState === 'sending'
|
||||||
|
? 'sendet …'
|
||||||
|
: debugLog.uploadState === 'error'
|
||||||
|
? 'Upload fehlgeschlagen'
|
||||||
|
: offen > 0
|
||||||
|
? `${offen} noch nicht übertragen`
|
||||||
|
: 'alle übertragen',
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AppHeader title="Debug-Log" back subtitle={`${debugLog.entries.length} Einträge`} />
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 border-b border-zinc-800 bg-zinc-900 px-3 py-2">
|
||||||
|
<button
|
||||||
|
class="rounded bg-sky-600 px-3 py-1.5 text-sm text-white active:bg-sky-700 disabled:opacity-50"
|
||||||
|
onclick={() => debugLog.flush()}
|
||||||
|
disabled={debugLog.uploadState === 'sending'}
|
||||||
|
>
|
||||||
|
An Server senden
|
||||||
|
</button>
|
||||||
|
<span class="flex-1 text-xs text-zinc-500">{uploadText}</span>
|
||||||
|
<button
|
||||||
|
class="rounded bg-zinc-800 px-3 py-1.5 text-sm active:bg-zinc-700"
|
||||||
|
onclick={() => debugLog.clear()}
|
||||||
|
>
|
||||||
|
Leeren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-y-auto p-2 font-mono text-xs">
|
||||||
|
{#if debugLog.entries.length === 0}
|
||||||
|
<p class="p-4 text-center text-zinc-500">Keine Einträge.</p>
|
||||||
|
{:else}
|
||||||
|
{#each [...debugLog.entries].reverse() as e, i (i)}
|
||||||
|
<div class="border-b border-zinc-800/50 py-1">
|
||||||
|
<span class="text-zinc-600">{fmtTime(e.ts)}</span>
|
||||||
|
<span class="break-words {levelColor[e.level]}"> {e.msg}</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
@ -9,11 +9,18 @@
|
||||||
|
|
||||||
let checking = $state(false);
|
let checking = $state(false);
|
||||||
|
|
||||||
async function logout() {
|
// Sync-Status in Klartext
|
||||||
await auth.logout();
|
const syncLabel = $derived(
|
||||||
sync.stop();
|
sync.status === 'syncing'
|
||||||
goto('/login/');
|
? 'Wird synchronisiert …'
|
||||||
}
|
: sync.status === 'error'
|
||||||
|
? 'Letzter Sync fehlgeschlagen'
|
||||||
|
: sync.status === 'offline'
|
||||||
|
? 'Offline'
|
||||||
|
: sync.pendingCount > 0
|
||||||
|
? `${sync.pendingCount} Protokoll(e) noch nicht übertragen`
|
||||||
|
: 'Alles synchronisiert',
|
||||||
|
);
|
||||||
|
|
||||||
async function checkUpdate() {
|
async function checkUpdate() {
|
||||||
checking = true;
|
checking = true;
|
||||||
|
|
@ -22,11 +29,52 @@
|
||||||
if (upd) openUpdate(upd);
|
if (upd) openUpdate(upd);
|
||||||
else toast.show('App ist aktuell', 'success');
|
else toast.show('App ist aktuell', 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
await auth.logout();
|
||||||
|
sync.stop();
|
||||||
|
goto('/login/');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AppHeader title="Einstellungen" back />
|
<AppHeader title="Einstellungen" back />
|
||||||
|
|
||||||
<div class="flex flex-1 flex-col gap-4 overflow-y-auto p-4">
|
<div class="flex flex-1 flex-col gap-4 overflow-y-auto p-4">
|
||||||
|
<!-- App / Version / Update -->
|
||||||
|
<section class="rounded-lg bg-zinc-900 p-4">
|
||||||
|
<h2 class="mb-2 text-sm font-semibold text-zinc-300">App</h2>
|
||||||
|
<p class="text-sm">
|
||||||
|
Version <span class="font-mono text-zinc-400">{APP_VERSION}</span>
|
||||||
|
</p>
|
||||||
|
<p class="mt-0.5 text-xs text-zinc-500">
|
||||||
|
Der Zeitstempel zeigt, welcher Build installiert ist.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
class="mt-3 w-full rounded bg-zinc-800 px-3 py-2 text-sm active:bg-zinc-700 disabled:opacity-50"
|
||||||
|
onclick={checkUpdate}
|
||||||
|
disabled={checking}
|
||||||
|
>
|
||||||
|
{checking ? 'Prüfe …' : 'Auf Update prüfen'}
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Synchronisierung -->
|
||||||
|
<section class="rounded-lg bg-zinc-900 p-4">
|
||||||
|
<h2 class="mb-2 text-sm font-semibold text-zinc-300">Synchronisierung</h2>
|
||||||
|
<p class="text-sm text-zinc-300">{syncLabel}</p>
|
||||||
|
<p class="mt-0.5 break-all text-xs text-zinc-500">
|
||||||
|
Server: {getServerUrl() || '(Browser-Proxy)'}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
class="mt-3 w-full rounded bg-zinc-800 px-3 py-2 text-sm active:bg-zinc-700 disabled:opacity-50"
|
||||||
|
onclick={() => sync.syncManual()}
|
||||||
|
disabled={sync.status === 'syncing'}
|
||||||
|
>
|
||||||
|
Jetzt synchronisieren
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Konto -->
|
||||||
<section class="rounded-lg bg-zinc-900 p-4">
|
<section class="rounded-lg bg-zinc-900 p-4">
|
||||||
<h2 class="mb-2 text-sm font-semibold text-zinc-300">Konto</h2>
|
<h2 class="mb-2 text-sm font-semibold text-zinc-300">Konto</h2>
|
||||||
<p class="text-sm">{auth.user?.name ?? '—'}</p>
|
<p class="text-sm">{auth.user?.name ?? '—'}</p>
|
||||||
|
|
@ -36,30 +84,24 @@
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Diagnose -->
|
||||||
<section class="rounded-lg bg-zinc-900 p-4">
|
<section class="rounded-lg bg-zinc-900 p-4">
|
||||||
<h2 class="mb-2 text-sm font-semibold text-zinc-300">Server</h2>
|
<h2 class="mb-2 text-sm font-semibold text-zinc-300">Diagnose</h2>
|
||||||
<p class="break-all text-sm text-zinc-400">{getServerUrl() || '(Browser-Proxy)'}</p>
|
<p class="text-xs text-zinc-500">
|
||||||
<p class="mt-2 text-xs text-zinc-500">
|
Bei Problemen (Scan/Sync schlägt fehl): Log öffnen und teilen.
|
||||||
Offene Protokolle: {sync.pendingCount} · Status: {sync.status}
|
|
||||||
</p>
|
</p>
|
||||||
<button class="mt-2 rounded bg-zinc-800 px-3 py-1.5 text-sm" onclick={() => sync.syncNow()}>
|
<a
|
||||||
Jetzt synchronisieren
|
class="mt-3 block rounded bg-zinc-800 px-3 py-2 text-center text-sm active:bg-zinc-700"
|
||||||
</button>
|
href="/debug/"
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="rounded-lg bg-zinc-900 p-4">
|
|
||||||
<h2 class="mb-2 text-sm font-semibold text-zinc-300">App</h2>
|
|
||||||
<p class="text-sm text-zinc-400">Version {APP_VERSION}</p>
|
|
||||||
<button
|
|
||||||
class="mt-2 rounded bg-zinc-800 px-3 py-1.5 text-sm"
|
|
||||||
onclick={checkUpdate}
|
|
||||||
disabled={checking}
|
|
||||||
>
|
>
|
||||||
{checking ? 'Prüfe …' : 'Auf Update prüfen'}
|
Debug-Log öffnen
|
||||||
</button>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<button class="mt-2 rounded-lg bg-red-700 py-2.5 font-semibold text-white" onclick={logout}>
|
<button
|
||||||
|
class="mt-2 rounded-lg bg-red-700 py-2.5 font-semibold text-white active:bg-red-800"
|
||||||
|
onclick={logout}
|
||||||
|
>
|
||||||
Abmelden
|
Abmelden
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue