revert(apps): settings pages back to routes (not workbench cards)

User feedback: per-module settings/preferences as separate workbench
cards bloats the scene-picker with rarely-used configuration surfaces.
Cards are for daily workflows; one-time config belongs in routes that
open from the parent module's ⚙ button.

- Inline the ListView content back into each /settings route
- Delete lib/modules/{broadcast-settings,invoices-settings,uload-settings,news-preferences}/
- Remove the four registerApp entries

Kept: spaces card (operative member management, daily use).
Deferred: admin-* cards will fuse into a single admin card with tabs
in a follow-up commit, since merging 4 power-user surfaces into tabs
is a different shape than deleting settings cards.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-22 14:53:15 +02:00
parent 8647bfd100
commit 3e65637fcb
9 changed files with 493 additions and 578 deletions

View file

@ -1318,50 +1318,6 @@ registerApp({
},
});
// ── Module-Settings Cards ────────────────────────────
// Per-module settings/preferences as workbench cards so they can be
// dropped into any scene without a subroute.
registerApp({
id: 'broadcast-settings',
name: 'Broadcast · Settings',
color: '#6366f1',
icon: Gear,
views: {
list: { load: () => import('$lib/modules/broadcast-settings/ListView.svelte') },
},
});
registerApp({
id: 'invoices-settings',
name: 'Invoices · Settings',
color: '#059669',
icon: Gear,
views: {
list: { load: () => import('$lib/modules/invoices-settings/ListView.svelte') },
},
});
registerApp({
id: 'uload-settings',
name: 'uLoad · Settings',
color: '#0EA5E9',
icon: Gear,
views: {
list: { load: () => import('$lib/modules/uload-settings/ListView.svelte') },
},
});
registerApp({
id: 'news-preferences',
name: 'News · Preferences',
color: '#10B981',
icon: Gear,
views: {
list: { load: () => import('$lib/modules/news-preferences/ListView.svelte') },
},
});
registerApp({
id: 'quiz',
name: 'Quiz',

View file

@ -1,40 +0,0 @@
<!--
Broadcast → Settings — workbench card.
Wraps the existing SettingsForm so both the standalone route
(/broadcasts/settings) and the workbench card render the same UI.
-->
<script lang="ts">
import SettingsForm from '$lib/modules/broadcast/components/SettingsForm.svelte';
</script>
<div class="pane">
<header class="bar">
<div class="title">
<strong>Broadcast-Einstellungen</strong>
<span class="sub">Sender-Defaults, Impressum und Footer</span>
</div>
</header>
<SettingsForm />
</div>
<style>
.pane {
max-width: 720px;
margin: 0 auto;
padding: 1rem;
color: hsl(var(--color-foreground));
}
.bar {
margin-bottom: 1rem;
}
.bar .title strong {
font-size: 1rem;
font-weight: 600;
}
.bar .sub {
margin-left: 0.5rem;
font-size: 0.8125rem;
color: hsl(var(--color-muted-foreground));
}
</style>

View file

@ -1,40 +0,0 @@
<!--
Invoices → Settings — workbench card.
Wraps SenderProfileForm so the standalone route
(/invoices/settings) and the workbench card share the same UI.
-->
<script lang="ts">
import SenderProfileForm from '$lib/modules/invoices/components/SenderProfileForm.svelte';
</script>
<div class="pane">
<header class="bar">
<div class="title">
<strong>Rechnungs-Einstellungen</strong>
<span class="sub">Absender, Nummernkreis und Standards</span>
</div>
</header>
<SenderProfileForm />
</div>
<style>
.pane {
max-width: 720px;
margin: 0 auto;
padding: 1rem;
color: hsl(var(--color-foreground));
}
.bar {
margin-bottom: 1rem;
}
.bar .title strong {
font-size: 1rem;
font-weight: 600;
}
.bar .sub {
margin-left: 0.5rem;
font-size: 0.8125rem;
color: hsl(var(--color-muted-foreground));
}
</style>

View file

@ -1,231 +0,0 @@
<!--
News → Preferences — workbench card.
Topics + languages selection, source-management link, reset learned
weights, rerun onboarding.
-->
<script lang="ts">
import { goto } from '$app/navigation';
import { usePreferences } from '$lib/modules/news/queries';
import { preferencesStore } from '$lib/modules/news/stores/preferences.svelte';
import { ALL_TOPICS, type Topic, type Language } from '$lib/modules/news/types';
import { TOPIC_LABELS } from '$lib/modules/news/sources-meta';
const prefs$ = usePreferences();
const prefs = $derived(prefs$.value);
let topicWeightCount = $derived(Object.keys(prefs.topicWeights).length);
let sourceWeightCount = $derived(Object.keys(prefs.sourceWeights).length);
async function toggleTopic(t: Topic) {
const next = prefs.selectedTopics.includes(t)
? prefs.selectedTopics.filter((x) => x !== t)
: [...prefs.selectedTopics, t];
await preferencesStore.setTopics(next);
}
async function toggleLang(l: Language) {
const next = prefs.preferredLanguages.includes(l)
? prefs.preferredLanguages.filter((x) => x !== l)
: [...prefs.preferredLanguages, l];
await preferencesStore.setLanguages(next);
}
async function resetWeights() {
if (!confirm('Alle gelernten Gewichtungen zurücksetzen?')) return;
await preferencesStore.resetWeights();
}
async function rerunOnboarding() {
await preferencesStore.applyWeightDiff({});
const { preferencesTable } = await import('$lib/modules/news/collections');
const { encryptRecord } = await import('$lib/data/crypto');
const { PREFERENCES_ID } = await import('$lib/modules/news/types');
const diff = { onboardingCompleted: false, updatedAt: new Date().toISOString() };
await encryptRecord('newsPreferences', diff);
await preferencesTable.update(PREFERENCES_ID, diff);
goto('/news');
}
</script>
<div class="pane">
<header class="bar">
<div class="title">
<strong>News-Einstellungen</strong>
<span class="sub">Themen · Sprachen · Gewichtungen</span>
</div>
</header>
<section class="card">
<h2>Themen</h2>
<p class="hint">Welche Themen sollen im Feed auftauchen?</p>
<div class="grid">
{#each ALL_TOPICS as topic}
<button
type="button"
class="pill"
class:selected={prefs.selectedTopics.includes(topic)}
onclick={() => toggleTopic(topic)}
>
<span class="emoji">{TOPIC_LABELS[topic].emoji}</span>
<span>{TOPIC_LABELS[topic].de}</span>
</button>
{/each}
</div>
</section>
<section class="card">
<h2>Sprachen</h2>
<div class="row">
<button
type="button"
class="pill"
class:selected={prefs.preferredLanguages.includes('de')}
onclick={() => toggleLang('de')}
>
🇩🇪 Deutsch
</button>
<button
type="button"
class="pill"
class:selected={prefs.preferredLanguages.includes('en')}
onclick={() => toggleLang('en')}
>
🇬🇧 English
</button>
</div>
</section>
<section class="card">
<h2>Quellen</h2>
<p class="hint">
Du blockst aktuell <strong>{prefs.blockedSources.length}</strong> Quellen.
</p>
<a class="btn-link" href="/news/sources">Quellen verwalten →</a>
</section>
<section class="card">
<h2>Gelernte Gewichtungen</h2>
<p class="hint">
Über Reaktionen lernt der Feed deine Vorlieben:
{topicWeightCount} Themen-Gewichte, {sourceWeightCount} Quellen-Gewichte.
</p>
<button type="button" class="btn-secondary" onclick={resetWeights}>Zurücksetzen</button>
</section>
<section class="card">
<h2>Onboarding</h2>
<p class="hint">Themen, Sprachen und Quellen neu wählen.</p>
<button type="button" class="btn-secondary" onclick={rerunOnboarding}>
Onboarding neu starten
</button>
</section>
</div>
<style>
.pane {
max-width: 720px;
margin: 0 auto;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
color: hsl(var(--color-foreground));
}
.bar .title strong {
font-size: 1rem;
font-weight: 600;
}
.bar .sub {
margin-left: 0.5rem;
font-size: 0.8125rem;
color: hsl(var(--color-muted-foreground));
}
.card {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1.125rem;
background: hsl(var(--color-card));
border: 1px solid hsl(var(--color-border));
border-radius: 12px;
}
.card h2 {
font-size: 0.9375rem;
font-weight: 600;
margin: 0;
}
.hint {
font-size: 0.875rem;
color: hsl(var(--color-muted-foreground));
margin: 0;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 0.5rem;
}
.row {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.pill {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
border-radius: 10px;
background: hsl(var(--color-background, var(--color-card)));
border: 2px solid hsl(var(--color-border));
color: hsl(var(--color-foreground));
font: inherit;
font-size: 0.875rem;
cursor: pointer;
transition:
background 120ms ease,
border-color 120ms ease;
}
.pill.selected {
border-color: hsl(var(--color-primary, 230 80% 55%));
background: hsl(var(--color-primary, 230 80% 55%) / 0.12);
}
.emoji {
font-size: 1rem;
}
.btn-secondary {
align-self: flex-start;
padding: 0.5rem 0.875rem;
border-radius: 8px;
background: hsl(var(--color-background, var(--color-card)));
border: 1px solid hsl(var(--color-border));
color: hsl(var(--color-foreground));
font: inherit;
font-size: 0.8125rem;
cursor: pointer;
}
.btn-secondary:hover {
background: hsl(var(--color-muted) / 0.5);
}
.btn-link {
align-self: flex-start;
color: hsl(var(--color-primary, 230 80% 55%));
text-decoration: none;
font-size: 0.875rem;
font-weight: 500;
}
.btn-link:hover {
text-decoration: underline;
}
</style>

View file

@ -1,203 +0,0 @@
<!--
uLoad → Settings — workbench card.
Data overview (counts), JSON export, and the "clear all local data"
danger zone. Uses the uLoad module's collections directly because this
is a local-data admin surface, not a feature.
-->
<script lang="ts">
import { Trash, DownloadSimple } from '@mana/shared-icons';
import { linkTable, uloadTagTable, uloadFolderTable, linkTagTable } from '$lib/modules/uload';
import { decryptRecords } from '$lib/data/crypto';
import { useAllLinks, useAllTags, useAllFolders } from '$lib/modules/uload';
import { toast } from 'svelte-sonner';
const links = useAllLinks();
const tags = useAllTags();
const folders = useAllFolders();
async function clearAllData() {
if (!confirm('Alle lokalen uLoad-Daten löschen? Dies kann nicht rückgängig gemacht werden.'))
return;
await linkTable.clear();
await uloadTagTable.clear();
await uloadFolderTable.clear();
await linkTagTable.clear();
toast.success('Alle uLoad-Daten gelöscht');
}
async function exportData() {
const rawLinks = await linkTable.toArray();
const allLinks = await decryptRecords('links', rawLinks);
const allTags = await uloadTagTable.toArray();
const allFolders = await uloadFolderTable.toArray();
const allLinkTags = await linkTagTable.toArray();
const data = {
exportedAt: new Date().toISOString(),
links: allLinks,
tags: allTags,
folders: allFolders,
linkTags: allLinkTags,
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `uload-export-${new Date().toISOString().slice(0, 10)}.json`;
a.click();
URL.revokeObjectURL(url);
toast.success('Export heruntergeladen');
}
</script>
<div class="pane">
<header class="bar">
<div class="title">
<strong>uLoad-Einstellungen</strong>
<span class="sub">Datenübersicht · Export · Gefahrenzone</span>
</div>
</header>
<section class="panel">
<h2>Daten</h2>
<div class="stats">
<div class="stat">
<p class="stat-value">{links.value?.length ?? 0}</p>
<p class="stat-label">Links</p>
</div>
<div class="stat">
<p class="stat-value">{tags.value?.length ?? 0}</p>
<p class="stat-label">Tags</p>
</div>
<div class="stat">
<p class="stat-value">{folders.value?.length ?? 0}</p>
<p class="stat-label">Ordner</p>
</div>
</div>
</section>
<section class="panel">
<h2>Daten exportieren</h2>
<p class="hint">Alle Links, Tags und Ordner als JSON-Datei herunterladen.</p>
<button type="button" class="btn" onclick={exportData}>
<DownloadSimple size={16} />
JSON exportieren
</button>
</section>
<section class="panel danger">
<h2>Gefahrenzone</h2>
<p class="hint">
Löscht alle lokalen uLoad-Daten (Links, Tags, Ordner). Synchronisierte Daten auf dem Server
bleiben erhalten.
</p>
<button type="button" class="btn danger" onclick={clearAllData}>
<Trash size={16} />
Alle Daten löschen
</button>
</section>
</div>
<style>
.pane {
max-width: 720px;
margin: 0 auto;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
color: hsl(var(--color-foreground));
}
.bar .title strong {
font-size: 1rem;
font-weight: 600;
}
.bar .sub {
margin-left: 0.5rem;
font-size: 0.8125rem;
color: hsl(var(--color-muted-foreground));
}
.panel {
background: hsl(var(--color-card));
border: 1px solid hsl(var(--color-border));
border-radius: 12px;
padding: 1.125rem;
}
.panel.danger {
border-color: hsl(0 70% 55% / 0.3);
background: hsl(0 70% 55% / 0.04);
}
.panel h2 {
font-size: 0.9375rem;
font-weight: 600;
margin: 0 0 0.5rem;
}
.panel.danger h2 {
color: hsl(0 70% 55%);
}
.hint {
font-size: 0.875rem;
color: hsl(var(--color-muted-foreground));
margin: 0 0 0.875rem;
}
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
text-align: center;
gap: 1rem;
}
.stat-value {
font-size: 1.5rem;
font-weight: 600;
margin: 0;
}
.stat-label {
font-size: 0.75rem;
color: hsl(var(--color-muted-foreground));
margin: 0.25rem 0 0;
}
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.875rem;
border: 1px solid hsl(var(--color-border));
border-radius: 8px;
background: hsl(var(--color-background, var(--color-card)));
color: hsl(var(--color-foreground));
font: inherit;
font-size: 0.875rem;
cursor: pointer;
transition:
background 120ms ease,
color 120ms ease;
}
.btn:hover {
background: hsl(var(--color-muted) / 0.5);
}
.btn.danger {
background: hsl(0 70% 55%);
color: white;
border-color: hsl(0 70% 45%);
}
.btn.danger:hover {
background: hsl(0 70% 50%);
}
</style>

View file

@ -1,12 +1,42 @@
<!--
/broadcasts/settings — sender defaults, impressum, footer for new campaigns.
Reached via the ⚙ button in the broadcast module; not a workbench card.
-->
<script lang="ts">
/**
* /broadcasts/settings — renders the workbench-card ListView.
*/
import ListView from '$lib/modules/broadcast-settings/ListView.svelte';
import SettingsForm from '$lib/modules/broadcast/components/SettingsForm.svelte';
</script>
<svelte:head>
<title>Broadcast-Einstellungen — Mana</title>
</svelte:head>
<ListView />
<div class="pane">
<header class="bar">
<div class="title">
<strong>Broadcast-Einstellungen</strong>
<span class="sub">Sender-Defaults, Impressum und Footer</span>
</div>
</header>
<SettingsForm />
</div>
<style>
.pane {
max-width: 720px;
margin: 0 auto;
padding: 1rem;
color: hsl(var(--color-foreground));
}
.bar {
margin-bottom: 1rem;
}
.bar .title strong {
font-size: 1rem;
font-weight: 600;
}
.bar .sub {
margin-left: 0.5rem;
font-size: 0.8125rem;
color: hsl(var(--color-muted-foreground));
}
</style>

View file

@ -1,12 +1,42 @@
<!--
/invoices/settings — sender profile, number range, invoice defaults.
Reached via the ⚙ button in the invoices module; not a workbench card.
-->
<script lang="ts">
/**
* /invoices/settings — renders the workbench-card ListView.
*/
import ListView from '$lib/modules/invoices-settings/ListView.svelte';
import SenderProfileForm from '$lib/modules/invoices/components/SenderProfileForm.svelte';
</script>
<svelte:head>
<title>Rechnungs-Einstellungen — Mana</title>
</svelte:head>
<ListView />
<div class="pane">
<header class="bar">
<div class="title">
<strong>Rechnungs-Einstellungen</strong>
<span class="sub">Absender, Nummernkreis und Standards</span>
</div>
</header>
<SenderProfileForm />
</div>
<style>
.pane {
max-width: 720px;
margin: 0 auto;
padding: 1rem;
color: hsl(var(--color-foreground));
}
.bar {
margin-bottom: 1rem;
}
.bar .title strong {
font-size: 1rem;
font-weight: 600;
}
.bar .sub {
margin-left: 0.5rem;
font-size: 0.8125rem;
color: hsl(var(--color-muted-foreground));
}
</style>

View file

@ -1,12 +1,233 @@
<!--
/news/preferences — topics, languages, reset weights, onboarding rerun.
Reached via the ⚙ button in the news module; not a workbench card.
-->
<script lang="ts">
/**
* /news/preferences — renders the workbench-card ListView.
*/
import ListView from '$lib/modules/news-preferences/ListView.svelte';
import { goto } from '$app/navigation';
import { usePreferences } from '$lib/modules/news/queries';
import { preferencesStore } from '$lib/modules/news/stores/preferences.svelte';
import { ALL_TOPICS, type Topic, type Language } from '$lib/modules/news/types';
import { TOPIC_LABELS } from '$lib/modules/news/sources-meta';
const prefs$ = usePreferences();
const prefs = $derived(prefs$.value);
let topicWeightCount = $derived(Object.keys(prefs.topicWeights).length);
let sourceWeightCount = $derived(Object.keys(prefs.sourceWeights).length);
async function toggleTopic(t: Topic) {
const next = prefs.selectedTopics.includes(t)
? prefs.selectedTopics.filter((x) => x !== t)
: [...prefs.selectedTopics, t];
await preferencesStore.setTopics(next);
}
async function toggleLang(l: Language) {
const next = prefs.preferredLanguages.includes(l)
? prefs.preferredLanguages.filter((x) => x !== l)
: [...prefs.preferredLanguages, l];
await preferencesStore.setLanguages(next);
}
async function resetWeights() {
if (!confirm('Alle gelernten Gewichtungen zurücksetzen?')) return;
await preferencesStore.resetWeights();
}
async function rerunOnboarding() {
await preferencesStore.applyWeightDiff({});
const { preferencesTable } = await import('$lib/modules/news/collections');
const { encryptRecord } = await import('$lib/data/crypto');
const { PREFERENCES_ID } = await import('$lib/modules/news/types');
const diff = { onboardingCompleted: false, updatedAt: new Date().toISOString() };
await encryptRecord('newsPreferences', diff);
await preferencesTable.update(PREFERENCES_ID, diff);
goto('/news');
}
</script>
<svelte:head>
<title>News-Einstellungen — Mana</title>
</svelte:head>
<ListView />
<div class="pane">
<header class="bar">
<div class="title">
<strong>News-Einstellungen</strong>
<span class="sub">Themen · Sprachen · Gewichtungen</span>
</div>
</header>
<section class="card">
<h2>Themen</h2>
<p class="hint">Welche Themen sollen im Feed auftauchen?</p>
<div class="grid">
{#each ALL_TOPICS as topic}
<button
type="button"
class="pill"
class:selected={prefs.selectedTopics.includes(topic)}
onclick={() => toggleTopic(topic)}
>
<span class="emoji">{TOPIC_LABELS[topic].emoji}</span>
<span>{TOPIC_LABELS[topic].de}</span>
</button>
{/each}
</div>
</section>
<section class="card">
<h2>Sprachen</h2>
<div class="row">
<button
type="button"
class="pill"
class:selected={prefs.preferredLanguages.includes('de')}
onclick={() => toggleLang('de')}
>
🇩🇪 Deutsch
</button>
<button
type="button"
class="pill"
class:selected={prefs.preferredLanguages.includes('en')}
onclick={() => toggleLang('en')}
>
🇬🇧 English
</button>
</div>
</section>
<section class="card">
<h2>Quellen</h2>
<p class="hint">
Du blockst aktuell <strong>{prefs.blockedSources.length}</strong> Quellen.
</p>
<a class="btn-link" href="/news/sources">Quellen verwalten →</a>
</section>
<section class="card">
<h2>Gelernte Gewichtungen</h2>
<p class="hint">
Über Reaktionen lernt der Feed deine Vorlieben:
{topicWeightCount} Themen-Gewichte, {sourceWeightCount} Quellen-Gewichte.
</p>
<button type="button" class="btn-secondary" onclick={resetWeights}>Zurücksetzen</button>
</section>
<section class="card">
<h2>Onboarding</h2>
<p class="hint">Themen, Sprachen und Quellen neu wählen.</p>
<button type="button" class="btn-secondary" onclick={rerunOnboarding}>
Onboarding neu starten
</button>
</section>
</div>
<style>
.pane {
max-width: 720px;
margin: 0 auto;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
color: hsl(var(--color-foreground));
}
.bar .title strong {
font-size: 1rem;
font-weight: 600;
}
.bar .sub {
margin-left: 0.5rem;
font-size: 0.8125rem;
color: hsl(var(--color-muted-foreground));
}
.card {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1.125rem;
background: hsl(var(--color-card));
border: 1px solid hsl(var(--color-border));
border-radius: 12px;
}
.card h2 {
font-size: 0.9375rem;
font-weight: 600;
margin: 0;
}
.hint {
font-size: 0.875rem;
color: hsl(var(--color-muted-foreground));
margin: 0;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 0.5rem;
}
.row {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.pill {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
border-radius: 10px;
background: hsl(var(--color-background, var(--color-card)));
border: 2px solid hsl(var(--color-border));
color: hsl(var(--color-foreground));
font: inherit;
font-size: 0.875rem;
cursor: pointer;
transition:
background 120ms ease,
border-color 120ms ease;
}
.pill.selected {
border-color: hsl(var(--color-primary, 230 80% 55%));
background: hsl(var(--color-primary, 230 80% 55%) / 0.12);
}
.emoji {
font-size: 1rem;
}
.btn-secondary {
align-self: flex-start;
padding: 0.5rem 0.875rem;
border-radius: 8px;
background: hsl(var(--color-background, var(--color-card)));
border: 1px solid hsl(var(--color-border));
color: hsl(var(--color-foreground));
font: inherit;
font-size: 0.8125rem;
cursor: pointer;
}
.btn-secondary:hover {
background: hsl(var(--color-muted) / 0.5);
}
.btn-link {
align-self: flex-start;
color: hsl(var(--color-primary, 230 80% 55%));
text-decoration: none;
font-size: 0.875rem;
font-weight: 500;
}
.btn-link:hover {
text-decoration: underline;
}
</style>

View file

@ -1,12 +1,204 @@
<!--
/uload/settings — uLoad data overview, JSON export, clear-local-data.
Reached via the ⚙ button in the uLoad module; not a workbench card.
-->
<script lang="ts">
/**
* /uload/settings — renders the workbench-card ListView.
*/
import ListView from '$lib/modules/uload-settings/ListView.svelte';
import { Trash, DownloadSimple } from '@mana/shared-icons';
import { linkTable, uloadTagTable, uloadFolderTable, linkTagTable } from '$lib/modules/uload';
import { decryptRecords } from '$lib/data/crypto';
import { useAllLinks, useAllTags, useAllFolders } from '$lib/modules/uload';
import { toast } from 'svelte-sonner';
const links = useAllLinks();
const tags = useAllTags();
const folders = useAllFolders();
async function clearAllData() {
if (!confirm('Alle lokalen uLoad-Daten löschen? Dies kann nicht rückgängig gemacht werden.'))
return;
await linkTable.clear();
await uloadTagTable.clear();
await uloadFolderTable.clear();
await linkTagTable.clear();
toast.success('Alle uLoad-Daten gelöscht');
}
async function exportData() {
const rawLinks = await linkTable.toArray();
const allLinks = await decryptRecords('links', rawLinks);
const allTags = await uloadTagTable.toArray();
const allFolders = await uloadFolderTable.toArray();
const allLinkTags = await linkTagTable.toArray();
const data = {
exportedAt: new Date().toISOString(),
links: allLinks,
tags: allTags,
folders: allFolders,
linkTags: allLinkTags,
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `uload-export-${new Date().toISOString().slice(0, 10)}.json`;
a.click();
URL.revokeObjectURL(url);
toast.success('Export heruntergeladen');
}
</script>
<svelte:head>
<title>uLoad-Einstellungen — Mana</title>
</svelte:head>
<ListView />
<div class="pane">
<header class="bar">
<div class="title">
<strong>uLoad-Einstellungen</strong>
<span class="sub">Datenübersicht · Export · Gefahrenzone</span>
</div>
</header>
<section class="panel">
<h2>Daten</h2>
<div class="stats">
<div class="stat">
<p class="stat-value">{links.value?.length ?? 0}</p>
<p class="stat-label">Links</p>
</div>
<div class="stat">
<p class="stat-value">{tags.value?.length ?? 0}</p>
<p class="stat-label">Tags</p>
</div>
<div class="stat">
<p class="stat-value">{folders.value?.length ?? 0}</p>
<p class="stat-label">Ordner</p>
</div>
</div>
</section>
<section class="panel">
<h2>Daten exportieren</h2>
<p class="hint">Alle Links, Tags und Ordner als JSON-Datei herunterladen.</p>
<button type="button" class="btn" onclick={exportData}>
<DownloadSimple size={16} />
JSON exportieren
</button>
</section>
<section class="panel danger">
<h2>Gefahrenzone</h2>
<p class="hint">
Löscht alle lokalen uLoad-Daten (Links, Tags, Ordner). Synchronisierte Daten auf dem Server
bleiben erhalten.
</p>
<button type="button" class="btn danger" onclick={clearAllData}>
<Trash size={16} />
Alle Daten löschen
</button>
</section>
</div>
<style>
.pane {
max-width: 720px;
margin: 0 auto;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
color: hsl(var(--color-foreground));
}
.bar .title strong {
font-size: 1rem;
font-weight: 600;
}
.bar .sub {
margin-left: 0.5rem;
font-size: 0.8125rem;
color: hsl(var(--color-muted-foreground));
}
.panel {
background: hsl(var(--color-card));
border: 1px solid hsl(var(--color-border));
border-radius: 12px;
padding: 1.125rem;
}
.panel.danger {
border-color: hsl(0 70% 55% / 0.3);
background: hsl(0 70% 55% / 0.04);
}
.panel h2 {
font-size: 0.9375rem;
font-weight: 600;
margin: 0 0 0.5rem;
}
.panel.danger h2 {
color: hsl(0 70% 55%);
}
.hint {
font-size: 0.875rem;
color: hsl(var(--color-muted-foreground));
margin: 0 0 0.875rem;
}
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
text-align: center;
gap: 1rem;
}
.stat-value {
font-size: 1.5rem;
font-weight: 600;
margin: 0;
}
.stat-label {
font-size: 0.75rem;
color: hsl(var(--color-muted-foreground));
margin: 0.25rem 0 0;
}
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.875rem;
border: 1px solid hsl(var(--color-border));
border-radius: 8px;
background: hsl(var(--color-background, var(--color-card)));
color: hsl(var(--color-foreground));
font: inherit;
font-size: 0.875rem;
cursor: pointer;
transition:
background 120ms ease,
color 120ms ease;
}
.btn:hover {
background: hsl(var(--color-muted) / 0.5);
}
.btn.danger {
background: hsl(0 70% 55%);
color: white;
border-color: hsl(0 70% 45%);
}
.btn.danger:hover {
background: hsl(0 70% 50%);
}
</style>