Ka-Note/ka-note/client/src/routes/settings/+page.svelte

258 lines
9.1 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
import { scopeSettings, currentScope, type ScopeSettings } from '$lib/stores/scopeContext';
import { authFetch } from '$lib/auth/apiClient.js';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import { onMount } from 'svelte';
currentScope.set(null);
const API_BASE = import.meta.env.VITE_API_URL ?? '';
let settings = $state<ScopeSettings>({ ...$scopeSettings });
// --- API Keys ---
interface ApiKey {
id: string;
label: string;
createdAt: string | number;
lastUsedAt: string | number | null;
}
let apiKeysList = $state<ApiKey[]>([]);
let apiKeysLoading = $state(false);
let apiKeysError = $state<string | null>(null);
let newKeyLabel = $state('');
let newKeyCreating = $state(false);
let newKeyValue = $state<string | null>(null);
let confirmDelete = $state<ApiKey | null>(null);
function fmtDate(val: string | number | null | undefined): string {
if (!val) return '';
return new Date(typeof val === 'number' ? val : val).toLocaleDateString('de-DE');
}
async function loadApiKeys() {
apiKeysLoading = true;
apiKeysError = null;
try {
const res = await authFetch(`${API_BASE}/api/api-keys`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
apiKeysList = await res.json() as ApiKey[];
} catch (e: unknown) {
apiKeysError = e instanceof Error ? e.message : 'Fehler';
} finally {
apiKeysLoading = false;
}
}
async function createApiKey() {
if (!newKeyLabel.trim()) return;
newKeyCreating = true;
apiKeysError = null;
try {
const res = await authFetch(`${API_BASE}/api/api-keys`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ label: newKeyLabel.trim() }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json() as ApiKey & { key: string };
newKeyValue = data.key;
newKeyLabel = '';
await loadApiKeys();
} catch (e: unknown) {
apiKeysError = e instanceof Error ? e.message : 'Fehler';
} finally {
newKeyCreating = false;
}
}
async function deleteApiKey(key: ApiKey) {
try {
const res = await authFetch(`${API_BASE}/api/api-keys/${key.id}`, { method: 'DELETE' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
await loadApiKeys();
} catch (e: unknown) {
apiKeysError = e instanceof Error ? e.message : 'Fehler';
} finally {
confirmDelete = null;
}
}
onMount(loadApiKeys);
function save() {
scopeSettings.set({ ...settings });
}
function reset() {
scopeSettings.set({
businessColor: '#4a9eff',
privateColor: '#e8a944',
unknownColor: '#555566',
businessIcon: '🏭',
privateIcon: '🏠',
maxTitleLength: 22,
});
settings = { ...$scopeSettings };
}
const iconOptions = ['🏭', '🏢', '💼', '📊', '🔵', '⚙️', '🏠', '🏡', '🔒', '🟡', '❤️', '🌿'];
</script>
<div class="space-y-8 max-w-md">
<h1 class="border-b-2 pb-2.5 text-2xl font-bold" style="border-color: var(--scope-color)">Einstellungen</h1>
<section class="space-y-4">
<h2 class="text-sm font-semibold uppercase text-muted">Kontext-Farben & Icons</h2>
<p class="text-xs text-muted">Titelleiste und Sidebar-Logo färben sich je nach aktivem Kontext (Firma / Privat / unbekannt).</p>
<!-- Business -->
<div class="rounded border border-border bg-card-bg p-4 space-y-3">
<div class="flex items-center gap-2 font-semibold" style="color: {settings.businessColor}">
{settings.businessIcon} Firma
</div>
<div class="flex items-center gap-4">
<label class="text-xs text-muted w-16">Farbe</label>
<input type="color" bind:value={settings.businessColor} oninput={save}
class="h-8 w-16 cursor-pointer rounded border border-border bg-transparent" />
<span class="text-xs text-muted font-mono">{settings.businessColor}</span>
</div>
<div class="flex flex-wrap items-center gap-2">
<span class="text-xs text-muted w-16">Icon</span>
{#each iconOptions as icon}
<button
class="text-xl rounded px-1 py-0.5 transition-all {settings.businessIcon === icon ? 'bg-accent/30 ring-1 ring-accent' : 'hover:bg-white/10'}"
onclick={() => { settings.businessIcon = icon; save(); }}
>{icon}</button>
{/each}
</div>
</div>
<!-- Private -->
<div class="rounded border border-border bg-card-bg p-4 space-y-3">
<div class="flex items-center gap-2 font-semibold" style="color: {settings.privateColor}">
{settings.privateIcon} Privat
</div>
<div class="flex items-center gap-4">
<label class="text-xs text-muted w-16">Farbe</label>
<input type="color" bind:value={settings.privateColor} oninput={save}
class="h-8 w-16 cursor-pointer rounded border border-border bg-transparent" />
<span class="text-xs text-muted font-mono">{settings.privateColor}</span>
</div>
<div class="flex flex-wrap items-center gap-2">
<span class="text-xs text-muted w-16">Icon</span>
{#each iconOptions as icon}
<button
class="text-xl rounded px-1 py-0.5 transition-all {settings.privateIcon === icon ? 'bg-accent/30 ring-1 ring-accent' : 'hover:bg-white/10'}"
onclick={() => { settings.privateIcon = icon; save(); }}
>{icon}</button>
{/each}
</div>
</div>
<!-- Unknown -->
<div class="rounded border border-border bg-card-bg p-4 space-y-3">
<div class="flex items-center gap-2 font-semibold" style="color: {settings.unknownColor}">
Unbekannt / gemischt
</div>
<div class="flex items-center gap-4">
<label class="text-xs text-muted w-16">Farbe</label>
<input type="color" bind:value={settings.unknownColor} oninput={save}
class="h-8 w-16 cursor-pointer rounded border border-border bg-transparent" />
<span class="text-xs text-muted font-mono">{settings.unknownColor}</span>
</div>
</div>
<button class="text-xs text-muted hover:text-white underline" onclick={reset}>
Auf Standard zurücksetzen
</button>
</section>
<!-- Title truncation -->
<section class="space-y-4">
<h2 class="text-sm font-semibold uppercase text-muted">Titel</h2>
<p class="text-xs text-muted">Titel, die diese Zeichenlänge überschreiten, werden automatisch gekürzt. Der vollständige Text wird als Notiz gespeichert.</p>
<div class="rounded border border-border bg-card-bg p-4 flex items-center gap-4">
<label class="text-xs text-muted w-40" for="maxTitleLength">Max. Titellänge</label>
<input
id="maxTitleLength"
type="number"
min="10"
max="100"
bind:value={settings.maxTitleLength}
oninput={save}
class="w-20 rounded border border-border bg-bg px-2 py-1 text-sm text-center focus:outline-none focus:ring-1 focus:ring-accent"
/>
<span class="text-xs text-muted">Zeichen</span>
</div>
</section>
<!-- API Keys -->
<section class="space-y-4">
<h2 class="text-sm font-semibold uppercase text-muted">API Keys</h2>
<p class="text-xs text-muted">Für Automationen und Maschinen-Zugänge (z.B. iOS Kurzbefehle). Der Key wird nur einmal angezeigt.</p>
{#if apiKeysError}
<p class="text-xs text-red-400">{apiKeysError}</p>
{/if}
{#if newKeyValue}
<div class="rounded border border-green-600 bg-green-950/40 p-3 space-y-2">
<p class="text-xs text-green-400 font-semibold">Neuer Key — jetzt kopieren, wird nicht mehr angezeigt:</p>
<code class="block break-all text-xs font-mono text-green-300 select-all">{newKeyValue}</code>
<button class="text-xs text-muted hover:text-white underline" onclick={() => newKeyValue = null}>Schliessen</button>
</div>
{/if}
<!-- existing keys -->
{#if apiKeysLoading}
<p class="text-xs text-muted">Laden...</p>
{:else if apiKeysList.length === 0}
<p class="text-xs text-muted">Keine API Keys vorhanden.</p>
{:else}
<ul class="space-y-2">
{#each apiKeysList as key (key.id)}
<li class="flex items-center justify-between rounded border border-border bg-card-bg px-3 py-2">
<div>
<span class="text-sm font-medium">{key.label}</span>
<span class="ml-3 text-xs text-muted">erstellt {fmtDate(key.createdAt)}</span>
{#if key.lastUsedAt}
<span class="ml-2 text-xs text-muted">zuletzt {fmtDate(key.lastUsedAt)}</span>
{/if}
</div>
<button
class="text-xs text-red-400 hover:text-red-300 ml-4 shrink-0"
onclick={() => confirmDelete = key}
>Löschen</button>
</li>
{/each}
</ul>
{/if}
<!-- create new -->
<div class="flex gap-2">
<input
type="text"
placeholder="Bezeichnung (z.B. iOS Kurzbefehle)"
bind:value={newKeyLabel}
class="flex-1 rounded border border-border bg-card-bg px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-accent"
onkeydown={(e) => e.key === 'Enter' && createApiKey()}
/>
<button
class="rounded bg-accent px-3 py-1.5 text-sm font-medium disabled:opacity-50"
disabled={!newKeyLabel.trim() || newKeyCreating}
onclick={createApiKey}
>{newKeyCreating ? '...' : 'Erstellen'}</button>
</div>
</section>
</div>
{#if confirmDelete}
<ConfirmDialog
message="API Key &quot;{confirmDelete.label}&quot; wirklich löschen? Alle Anwendungen, die diesen Key verwenden, verlieren den Zugriff."
onconfirm={() => deleteApiKey(confirmDelete!)}
oncancel={() => confirmDelete = null}
/>
{/if}