From 9ff2cfcdac8a2ce6466cb86a07e300426f5d0012 Mon Sep 17 00:00:00 2001 From: Till JS Date: Tue, 14 Apr 2026 13:48:44 +0200 Subject: [PATCH] feat(workbench): unify system pages as workbench apps + categorize picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 8 system pages as first-class workbench apps (Settings, Themes, Profile, Admin, API Keys, Help, Feedback, Subscription) so they can be opened as side-by-side panels next to other apps instead of requiring a full-page route switch. Existing routes remain as fullscreen fallback/deep-link targets. Group the AppPagePicker by 5 categories (Companion, Leben, Arbeit, Kreativ, System) with collapsible sections; System is collapsed by default. Search still works as a flat fuzzy match across all apps. Category assignment lives in a central map so registerApp() calls stay unchanged — unmapped apps fall back to System, which surfaces miscategorization at a glance. Remove profile-data and theme-picker duplication from Settings (both are separate workbench apps now): Settings defaults to 'Allgemein' and passes showTheme={false} to GlobalSettingsSection; SettingsSidebar accepts a categories override so the workbench version hides Profile. Fix Cannot-read-'subscribe'-of-undefined crash in mood/sleep/body/ stretch ListViews when opened in the workbench: replace getContext (which is only set by the route +layout.svelte) with direct query-hook calls, matching the goals/companion pattern. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../apps/web/src/lib/app-registry/apps.ts | 90 ++ .../web/src/lib/app-registry/categories.ts | 115 +++ .../settings/SettingsSidebar.svelte | 12 +- .../components/workbench/AppPagePicker.svelte | 252 +++++- .../web/src/lib/modules/admin/ListView.svelte | 264 ++++++ .../src/lib/modules/api-keys/ListView.svelte | 685 +++++++++++++++ .../web/src/lib/modules/body/ListView.svelte | 91 +- .../src/lib/modules/feedback/ListView.svelte | 20 + .../web/src/lib/modules/help/ListView.svelte | 39 + .../web/src/lib/modules/mood/ListView.svelte | 377 +++++---- .../src/lib/modules/profile/ListView.svelte | 180 ++++ .../src/lib/modules/settings/ListView.svelte | 100 +++ .../web/src/lib/modules/sleep/ListView.svelte | 40 +- .../src/lib/modules/stretch/ListView.svelte | 55 +- .../lib/modules/subscription/ListView.svelte | 792 ++++++++++++++++++ .../src/lib/modules/themes/ListView.svelte | 48 ++ 16 files changed, 2819 insertions(+), 341 deletions(-) create mode 100644 apps/mana/apps/web/src/lib/app-registry/categories.ts create mode 100644 apps/mana/apps/web/src/lib/modules/admin/ListView.svelte create mode 100644 apps/mana/apps/web/src/lib/modules/api-keys/ListView.svelte create mode 100644 apps/mana/apps/web/src/lib/modules/feedback/ListView.svelte create mode 100644 apps/mana/apps/web/src/lib/modules/help/ListView.svelte create mode 100644 apps/mana/apps/web/src/lib/modules/profile/ListView.svelte create mode 100644 apps/mana/apps/web/src/lib/modules/settings/ListView.svelte create mode 100644 apps/mana/apps/web/src/lib/modules/subscription/ListView.svelte create mode 100644 apps/mana/apps/web/src/lib/modules/themes/ListView.svelte diff --git a/apps/mana/apps/web/src/lib/app-registry/apps.ts b/apps/mana/apps/web/src/lib/app-registry/apps.ts index a0f401476..b3ad753fe 100644 --- a/apps/mana/apps/web/src/lib/app-registry/apps.ts +++ b/apps/mana/apps/web/src/lib/app-registry/apps.ts @@ -57,6 +57,14 @@ import { Robot, Target, Smiley, + Gear, + Palette, + UserCircle, + ShieldCheck, + Key, + Question, + ChatCircleDots, + CreditCard, } from '@mana/shared-icons'; // ── Apps with entity capabilities ─────────────────────────── @@ -977,3 +985,85 @@ registerApp({ list: { load: () => import('$lib/modules/goals/ListView.svelte') }, }, }); + +// ── System Pages ───────────────────────────────────── + +registerApp({ + id: 'settings', + name: 'Einstellungen', + color: '#6B7280', + icon: Gear, + views: { + list: { load: () => import('$lib/modules/settings/ListView.svelte') }, + }, +}); + +registerApp({ + id: 'themes', + name: 'Themes', + color: '#EC4899', + icon: Palette, + views: { + list: { load: () => import('$lib/modules/themes/ListView.svelte') }, + }, +}); + +registerApp({ + id: 'profile', + name: 'Profil', + color: '#6366F1', + icon: UserCircle, + views: { + list: { load: () => import('$lib/modules/profile/ListView.svelte') }, + }, +}); + +registerApp({ + id: 'admin', + name: 'Admin', + color: '#EF4444', + icon: ShieldCheck, + views: { + list: { load: () => import('$lib/modules/admin/ListView.svelte') }, + }, +}); + +registerApp({ + id: 'api-keys', + name: 'API Keys', + color: '#F59E0B', + icon: Key, + views: { + list: { load: () => import('$lib/modules/api-keys/ListView.svelte') }, + }, +}); + +registerApp({ + id: 'help', + name: 'Hilfe', + color: '#3B82F6', + icon: Question, + views: { + list: { load: () => import('$lib/modules/help/ListView.svelte') }, + }, +}); + +registerApp({ + id: 'feedback', + name: 'Feedback', + color: '#8B5CF6', + icon: ChatCircleDots, + views: { + list: { load: () => import('$lib/modules/feedback/ListView.svelte') }, + }, +}); + +registerApp({ + id: 'subscription', + name: 'Abonnement', + color: '#10B981', + icon: CreditCard, + views: { + list: { load: () => import('$lib/modules/subscription/ListView.svelte') }, + }, +}); diff --git a/apps/mana/apps/web/src/lib/app-registry/categories.ts b/apps/mana/apps/web/src/lib/app-registry/categories.ts new file mode 100644 index 000000000..8ce9711bb --- /dev/null +++ b/apps/mana/apps/web/src/lib/app-registry/categories.ts @@ -0,0 +1,115 @@ +/** + * App Categories — Groups apps in the workbench AppPagePicker so users + * can find pages by intent rather than scanning an alphabetical list. + * + * Five categories (Vorschlag C): + * - companion: Companion Brain pages (myday, eventstream, companion, goals) + * - life: Personal / wellness / everyday-life tracking + * - work: Productivity & planning + * - creative: Creative, learning, generation + * - system: Settings, admin, help, billing — everything meta + * + * Category assignment lives in APP_CATEGORY_MAP (keyed by appId) so + * registerApp() calls stay unchanged. Anything not in the map falls + * back to 'system'. + */ + +import type { Component } from 'svelte'; +import { Robot, Heart, Briefcase, Sparkle, Gear } from '@mana/shared-icons'; + +export type AppCategory = 'companion' | 'life' | 'work' | 'creative' | 'system'; + +export interface CategoryMeta { + id: AppCategory; + label: string; + icon: Component; + order: number; +} + +export const APP_CATEGORIES: CategoryMeta[] = [ + { id: 'companion', label: 'Companion', icon: Robot, order: 0 }, + { id: 'life', label: 'Leben', icon: Heart, order: 1 }, + { id: 'work', label: 'Arbeit', icon: Briefcase, order: 2 }, + { id: 'creative', label: 'Kreativ', icon: Sparkle, order: 3 }, + { id: 'system', label: 'System', icon: Gear, order: 4 }, +]; + +/** + * appId → AppCategory. Apps not listed here default to 'system'. + */ +export const APP_CATEGORY_MAP: Record = { + // Companion Brain + myday: 'companion', + eventstream: 'companion', + companion: 'companion', + goals: 'companion', + + // Leben — personal, wellness, everyday + habits: 'life', + body: 'life', + sleep: 'life', + mood: 'life', + stretch: 'life', + cycles: 'life', + dreams: 'life', + drink: 'life', + meditate: 'life', + journal: 'life', + nutriphi: 'life', + recipes: 'life', + plants: 'life', + finance: 'life', + contacts: 'life', + places: 'life', + citycorners: 'life', + news: 'life', + inventory: 'life', + storage: 'life', + who: 'life', + firsts: 'life', + memoro: 'life', + questions: 'life', + + // Arbeit — productivity, planning, communication + todo: 'work', + calendar: 'work', + notes: 'work', + times: 'work', + events: 'work', + mail: 'work', + chat: 'work', + context: 'work', + automations: 'work', + calc: 'work', + + // Kreativ — generation, learning, media + music: 'creative', + picture: 'creative', + photos: 'creative', + presi: 'creative', + moodlit: 'creative', + cards: 'creative', + skilltree: 'creative', + guides: 'creative', + zitare: 'creative', + uload: 'creative', + playground: 'creative', + + // System — settings, admin, meta + settings: 'system', + themes: 'system', + profile: 'system', + admin: 'system', + 'api-keys': 'system', + help: 'system', + feedback: 'system', + subscription: 'system', +}; + +export function getAppCategory(appId: string): AppCategory { + return APP_CATEGORY_MAP[appId] ?? 'system'; +} + +export function getCategoryMeta(id: AppCategory): CategoryMeta { + return APP_CATEGORIES.find((c) => c.id === id) ?? APP_CATEGORIES[APP_CATEGORIES.length - 1]; +} diff --git a/apps/mana/apps/web/src/lib/components/settings/SettingsSidebar.svelte b/apps/mana/apps/web/src/lib/components/settings/SettingsSidebar.svelte index 53dd680e7..952846de2 100644 --- a/apps/mana/apps/web/src/lib/components/settings/SettingsSidebar.svelte +++ b/apps/mana/apps/web/src/lib/components/settings/SettingsSidebar.svelte @@ -5,15 +5,23 @@ --> - - {#snippet subheader()} -
- - -
- {/snippet} - {#snippet item(app)} - {@const Icon = app.icon} - - {/snippet} -
+ {/snippet} + {#snippet item(row)} + {#if row.kind === 'header'} + {@const CatIcon = row.category.icon} + {@const isCollapsed = collapsed[row.category.id]} + + {:else} + {@const Icon = row.app.icon} + + {/if} + {/snippet} + + diff --git a/apps/mana/apps/web/src/lib/modules/api-keys/ListView.svelte b/apps/mana/apps/web/src/lib/modules/api-keys/ListView.svelte new file mode 100644 index 000000000..f9b989730 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/api-keys/ListView.svelte @@ -0,0 +1,685 @@ + + + +
+
+ +
+ + {#if loading} +
+
+
+ {:else} + {#if error} +
{error}
+ {/if} + + +
+
+ + Active Keys + {activeKeys.length} +
+ + {#if activeKeys.length === 0} +
+ +

No API keys yet

+
+ {:else} +
+ {#each activeKeys as key (key.id)} +
+
+
+ {key.name} + {key.scopes.join(', ')} + {key.rateLimitRequests}/min +
+
+ {key.keyPrefix} + Created: {formatDate(key.createdAt)} +
+
+ +
+ {/each} +
+ {/if} +
+ + + {#if revokedKeys.length > 0} +
+
+ + Revoked + {revokedKeys.length} +
+
+ {#each revokedKeys as key (key.id)} +
+
+ {key.name} +
+ {key.keyPrefix} + Revoked: {formatDate(key.revokedAt)} +
+
+
+ {/each} +
+
+ {/if} + + +
+
+ + How to Use +
+
+

Speech-to-Text (STT)

+
curl -X POST https://gpu-stt.mana.how/transcribe \
+  -H "X-API-Key: sk_live_..." \
+  -F "audio=@audio.mp3"
+
+
+

Text-to-Speech (TTS)

+
curl -X POST https://tts-api.mana.how/synthesize/kokoro \
+  -H "X-API-Key: sk_live_..." \
+  -H "Content-Type: application/json" \
+  -d '{{ text: 'Hello', voice: 'af_heart' }}' \
+  --output speech.wav
+
+
+ {/if} +
+ + +{#if showCreateModal} + +{/if} + + diff --git a/apps/mana/apps/web/src/lib/modules/body/ListView.svelte b/apps/mana/apps/web/src/lib/modules/body/ListView.svelte index 48797ce78..74adab44c 100644 --- a/apps/mana/apps/web/src/lib/modules/body/ListView.svelte +++ b/apps/mana/apps/web/src/lib/modules/body/ListView.svelte @@ -6,20 +6,20 @@ daily energy/sleep/soreness/mood card; recent workouts. --> + + + + diff --git a/apps/mana/apps/web/src/lib/modules/help/ListView.svelte b/apps/mana/apps/web/src/lib/modules/help/ListView.svelte new file mode 100644 index 000000000..a27a3b8bd --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/help/ListView.svelte @@ -0,0 +1,39 @@ + + + +
+ +
+ + diff --git a/apps/mana/apps/web/src/lib/modules/mood/ListView.svelte b/apps/mana/apps/web/src/lib/modules/mood/ListView.svelte index 5b510ead3..9d51074d2 100644 --- a/apps/mana/apps/web/src/lib/modules/mood/ListView.svelte +++ b/apps/mana/apps/web/src/lib/modules/mood/ListView.svelte @@ -3,10 +3,9 @@ Today's check-ins, week trend, emotion distribution, patterns, insights. --> + +
+ {#if loading} +
+
+
+ {:else} + + {/if} +
+ + (showEditModal = false)} + onSuccess={handleProfileUpdate} +/> + + (showPasswordModal = false)} + onSuccess={handlePasswordChange} +/> + + (showDeleteModal = false)} + onSuccess={handleAccountDeleted} +/> + +{#if toastMessage} +
{toastMessage}
+{/if} + + diff --git a/apps/mana/apps/web/src/lib/modules/settings/ListView.svelte b/apps/mana/apps/web/src/lib/modules/settings/ListView.svelte new file mode 100644 index 000000000..a631369a9 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/settings/ListView.svelte @@ -0,0 +1,100 @@ + + + +
+ (activeCategory = id)} + onJump={jumpTo} + categories={workbenchCategories} + /> + +
+ {#if activeCategory === 'general'} + + + + {:else if activeCategory === 'ai'} + + {:else if activeCategory === 'security'} + + {:else if activeCategory === 'credits'} + + {:else if activeCategory === 'data'} + + {/if} +
+ +

v{APP_VERSION}

+
+ + diff --git a/apps/mana/apps/web/src/lib/modules/sleep/ListView.svelte b/apps/mana/apps/web/src/lib/modules/sleep/ListView.svelte index 972de7d8c..0e89daec7 100644 --- a/apps/mana/apps/web/src/lib/modules/sleep/ListView.svelte +++ b/apps/mana/apps/web/src/lib/modules/sleep/ListView.svelte @@ -3,10 +3,11 @@ Last night summary, week bars, sleep goal, debt, stats, hygiene. --> + +
+ {#if loading} +
+ {:else if error} +
+

{error}

+ +
+ {:else} + + {#if currentSubscription?.subscription} + {@const sub = currentSubscription.subscription} + {@const plan = currentSubscription.plan} +
+
+
+
+ {plan?.name || 'Aktueller Plan'} + + {getStatusLabel(sub.status)} + +
+ + {plan?.monthlyCredits.toLocaleString('de-DE')} Mana / Monat + +
+ +
+
+
+ Zeitraum + {sub.billingInterval === 'year' ? 'Jährlich' : 'Monatlich'} +
+
+ Periode + {formatDate(sub.currentPeriodStart)} – {formatDate(sub.currentPeriodEnd)} +
+
+ {#if sub.cancelAtPeriodEnd} + Endet am + {formatDate(sub.currentPeriodEnd)} + + {:else} + Verlängert am + {formatDate(sub.currentPeriodEnd)} + + {/if} +
+
+
+ {:else} +
+
+ Free Plan + Aktuell +
+ 150 Mana / Monat +
+ {/if} + + +
+ + +
+ + {#if activeTab === 'plans'} + +
+ + +
+ + +
+ {#each plans as plan} + {@const isCurrent = currentSubscription?.plan?.id === plan.id} + {@const price = + billingInterval === 'year' ? plan.priceYearlyEuroCents : plan.priceMonthlyEuroCents} +
+ {#if isCurrent}Dein Plan{/if} + {plan.name} + {#if plan.description} + {plan.description} + {/if} +
+ + {plan.isDefault ? 'Kostenlos' : formatPrice(price)} + + {#if !plan.isDefault} + / {billingInterval === 'year' ? 'Jahr' : 'Monat'} + {#if billingInterval === 'year'} + {formatMonthlyEquivalent(plan.priceYearlyEuroCents)} / Monat + {/if} + {/if} +
+ + {plan.monthlyCredits.toLocaleString('de-DE')} Mana / Monat + + {#if plan.features?.length} +
    + {#each plan.features as feature} +
  • {feature}
  • + {/each} +
+ {/if} + +
+ {/each} +
+ {:else} + +
+ {#if invoices.length === 0} +

Noch keine Rechnungen vorhanden.

+ {:else} + {#each invoices as inv} +
+
+ {inv.number || '-'} + {formatDate(inv.createdAt)} +
+
+ {formatPrice(inv.amountPaidEuroCents)} + + {inv.status === 'paid' ? 'Bezahlt' : inv.status} + + {#if inv.invoicePdfUrl} + PDF + {/if} +
+
+ {/each} + {/if} +
+ {/if} + {/if} +
+ +{#if toastMessage} +
{toastMessage}
+{/if} + + diff --git a/apps/mana/apps/web/src/lib/modules/themes/ListView.svelte b/apps/mana/apps/web/src/lib/modules/themes/ListView.svelte new file mode 100644 index 000000000..ce383b354 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/themes/ListView.svelte @@ -0,0 +1,48 @@ + + + +
+ theme.setVariant(v)} + showModeSelector={true} + currentMode={theme.mode} + onModeChange={(m) => theme.setMode(m)} + showBackButton={false} + transparent={wallpaperStore.hasWallpaper} + > +
+

Hintergrund

+ +
+
+
+ +