feat(brain): add 4 Companion Brain workbench pages

Registers Mein Tag, Event Stream, Companion Chat, and Ziele as
workbench apps so they can be added to scenes alongside existing
modules like Todo, Calendar, etc.

New workbench pages:
- Mein Tag (myday): DaySnapshot overview — tasks, events, water
  progress, nutrition, streaks at a glance
- Events (eventstream): live domain event feed with icons, labels,
  and timestamps — shows the system "pulse" in real-time
- Companion (companion): embedded chat interface that auto-creates
  a conversation on first use
- Ziele (goals): goal cards with progress bars, template picker
  for quick goal creation, pause/resume/delete

Each page registered in both app-registry (workbench views) and
shared-branding (app metadata, icons, descriptions, tier=guest).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-13 22:33:53 +02:00
parent e1884cfbd1
commit 4211ce68da
7 changed files with 820 additions and 0 deletions

View file

@ -52,6 +52,10 @@ import {
PersonSimpleTaiChi,
Envelope,
Flower,
SunDim,
Pulse,
Robot,
Target,
} from '@mana/shared-icons';
// ── Apps with entity capabilities ───────────────────────────
@ -920,3 +924,45 @@ registerApp({
list: { load: () => import('$lib/modules/sleep/ListView.svelte') },
},
});
// ── Companion Brain Pages ─────────────────────────────
registerApp({
id: 'myday',
name: 'Mein Tag',
color: '#F59E0B',
icon: SunDim,
views: {
list: { load: () => import('$lib/modules/myday/ListView.svelte') },
},
});
registerApp({
id: 'eventstream',
name: 'Events',
color: '#6366F1',
icon: Pulse,
views: {
list: { load: () => import('$lib/modules/eventstream/ListView.svelte') },
},
});
registerApp({
id: 'companion',
name: 'Companion',
color: '#8B5CF6',
icon: Robot,
views: {
list: { load: () => import('$lib/modules/companion/ListView.svelte') },
},
});
registerApp({
id: 'goals',
name: 'Ziele',
color: '#10B981',
icon: Target,
views: {
list: { load: () => import('$lib/modules/goals/ListView.svelte') },
},
});

View file

@ -0,0 +1,55 @@
<!--
Companion Chat — Workbench-embedded version.
Auto-creates a conversation if none exists.
-->
<script lang="ts">
import { onMount } from 'svelte';
import CompanionChat from './components/CompanionChat.svelte';
import { chatStore } from './stores/chat.svelte';
import { useConversations } from './queries';
import type { LocalConversation } from './types';
const conversations = useConversations();
let activeConversation = $state<LocalConversation | null>(null);
$effect(() => {
if (!activeConversation && conversations.value.length > 0) {
activeConversation = conversations.value[0];
}
});
onMount(async () => {
// Auto-create if no conversation exists
if (conversations.value.length === 0) {
const conv = await chatStore.createConversation('Workbench Chat');
activeConversation = conv;
}
});
</script>
<div class="companion-embed">
{#if activeConversation}
{#key activeConversation.id}
<CompanionChat conversation={activeConversation} />
{/key}
{:else}
<div class="loading">Lade Companion...</div>
{/if}
</div>
<style>
.companion-embed {
display: flex;
flex-direction: column;
height: 100%;
}
.loading {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: hsl(var(--color-muted-foreground));
font-size: 0.875rem;
}
</style>

View file

@ -0,0 +1,174 @@
<!--
Event Stream — Live feed of domain events from all modules.
Shows what's happening across the system in real-time.
-->
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { eventBus } from '$lib/data/events/event-bus';
import { queryEvents } from '$lib/data/events/event-store';
import type { DomainEvent } from '$lib/data/events/types';
import {
CheckCircle,
CalendarBlank,
Drop,
ForkKnife,
MapPin,
Lightning,
} from '@mana/shared-icons';
let events = $state<DomainEvent[]>([]);
let unsubscribe: (() => void) | null = null;
const EVENT_ICONS: Record<string, { icon: typeof CheckCircle; color: string }> = {
TaskCreated: { icon: CheckCircle, color: '#10B981' },
TaskCompleted: { icon: CheckCircle, color: '#6366F1' },
TaskDeleted: { icon: CheckCircle, color: '#EF4444' },
CalendarEventCreated: { icon: CalendarBlank, color: '#F59E0B' },
CalendarEventDeleted: { icon: CalendarBlank, color: '#EF4444' },
DrinkLogged: { icon: Drop, color: '#3B82F6' },
DrinkEntryDeleted: { icon: Drop, color: '#EF4444' },
MealLogged: { icon: ForkKnife, color: '#F97316' },
MealFromPhotoLogged: { icon: ForkKnife, color: '#F97316' },
PlaceVisited: { icon: MapPin, color: '#A855F7' },
PlaceCreated: { icon: MapPin, color: '#10B981' },
};
const EVENT_LABELS: Record<string, (p: Record<string, unknown>) => string> = {
TaskCreated: (p) => `Task erstellt: "${p.title}"`,
TaskCompleted: (p) => `Task erledigt: "${p.title}"`,
TaskUncompleted: (p) => `Task wiedergeoeffnet: "${p.title}"`,
TaskDeleted: (p) => `Task geloescht: "${p.title}"`,
SubtasksUpdated: (p) => `Subtasks: ${p.completed}/${p.total}`,
CalendarEventCreated: (p) => `Termin: "${p.title}"`,
CalendarEventUpdated: () => 'Termin aktualisiert',
CalendarEventDeleted: (p) => `Termin geloescht: "${p.title}"`,
DrinkLogged: (p) => `${p.quantityMl}ml ${p.name ?? p.drinkType}`,
DrinkEntryDeleted: (p) => `Drink geloescht (${p.quantityMl}ml)`,
DrinkEntryUndone: () => 'Letzter Drink rueckgaengig',
MealLogged: (p) => `${p.mealType}: "${p.description}"`,
MealFromPhotoLogged: (p) => `Foto-Mahlzeit (${p.mealType})`,
MealDeleted: (p) => `Mahlzeit geloescht (${p.mealType})`,
PlaceCreated: (p) => `Neuer Ort: "${p.name}"`,
PlaceVisited: (p) => `Besuch: "${p.name}" (${p.visitCount}x)`,
PlaceDeleted: (p) => `Ort geloescht: "${p.name}"`,
LocationLogged: () => 'Standort erfasst',
TrackingStarted: () => 'Tracking gestartet',
TrackingStopped: () => 'Tracking gestoppt',
GoalReached: (p) => `Ziel erreicht: "${p.title}"`,
};
function formatTime(iso: string): string {
try {
return new Date(iso).toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
} catch {
return iso.slice(11, 19);
}
}
function getLabel(event: DomainEvent): string {
const fn = EVENT_LABELS[event.type];
if (fn) return fn(event.payload as Record<string, unknown>);
return event.type;
}
onMount(async () => {
// Load recent events
const recent = await queryEvents({ limit: 50 });
events = recent;
// Subscribe to live events
unsubscribe = eventBus.onAny((event) => {
events = [event, ...events].slice(0, 100);
});
});
onDestroy(() => {
unsubscribe?.();
});
</script>
<div class="stream">
{#if events.length === 0}
<div class="empty">
Noch keine Events. Erstelle Daten in Todo, Kalender, Drink oder Nutriphi.
</div>
{:else}
{#each events as event (event.meta.id)}
{@const iconDef = EVENT_ICONS[event.type] ?? { icon: Lightning, color: '#6B7280' }}
{@const IconComp = iconDef.icon}
<div class="event-row">
<div class="event-icon" style:color={iconDef.color}>
<IconComp size={14} weight="fill" />
</div>
<div class="event-content">
<span class="event-label">{getLabel(event)}</span>
<span class="event-meta">{event.meta.appId} · {formatTime(event.meta.timestamp)}</span>
</div>
</div>
{/each}
{/if}
</div>
<style>
.stream {
display: flex;
flex-direction: column;
padding: 0.5rem;
}
.empty {
font-size: 0.8125rem;
color: hsl(var(--color-muted-foreground));
text-align: center;
padding: 2rem 1rem;
}
.event-row {
display: flex;
align-items: flex-start;
gap: 0.5rem;
padding: 0.375rem 0.5rem;
border-radius: 0.375rem;
transition: background 0.1s;
}
.event-row:hover {
background: hsl(var(--color-muted) / 0.1);
}
.event-icon {
flex-shrink: 0;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: hsl(var(--color-muted) / 0.15);
margin-top: 1px;
}
.event-content {
display: flex;
flex-direction: column;
gap: 0.0625rem;
min-width: 0;
}
.event-label {
font-size: 0.8125rem;
color: hsl(var(--color-foreground));
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.event-meta {
font-size: 0.6875rem;
color: hsl(var(--color-muted-foreground));
}
</style>

View file

@ -0,0 +1,243 @@
<!--
Goals — Goal cards with progress bars and template picker.
-->
<script lang="ts">
import { Target, Plus, Play, Pause, Trash } from '@mana/shared-icons';
import { goalStore, useAllGoals, GOAL_TEMPLATES } from '$lib/companion/goals';
import type { LocalGoal } from '$lib/companion/goals/types';
const goals = useAllGoals();
let showTemplates = $state(false);
function progressPercent(goal: LocalGoal): number {
if (goal.target.value === 0) return 0;
return Math.min(Math.round((goal.currentValue / goal.target.value) * 100), 100);
}
function periodLabel(period: string): string {
return period === 'day' ? 'Heute' : period === 'week' ? 'Diese Woche' : 'Diesen Monat';
}
async function createFromTemplate(templateId: string) {
const tpl = GOAL_TEMPLATES.find((t) => t.id === templateId);
if (tpl) await goalStore.createFromTemplate(tpl);
showTemplates = false;
}
</script>
<div class="goals-page">
<div class="header">
<button class="add-btn" onclick={() => (showTemplates = !showTemplates)}>
<Plus size={14} weight="bold" /> Ziel
</button>
</div>
{#if showTemplates}
<div class="templates">
{#each GOAL_TEMPLATES as tpl}
<button class="tpl-card" onclick={() => createFromTemplate(tpl.id)}>
<span class="tpl-title">{tpl.title}</span>
<span class="tpl-desc">{tpl.description}</span>
</button>
{/each}
</div>
{/if}
<div class="goal-list">
{#each goals.value.filter((g) => g.status === 'active') as goal (goal.id)}
{@const pct = progressPercent(goal)}
<div class="goal-card">
<div class="goal-header">
<Target size={16} weight="bold" />
<span class="goal-title">{goal.title}</span>
<button class="goal-action" onclick={() => goalStore.pause(goal.id)} title="Pausieren">
<Pause size={12} />
</button>
</div>
<div class="goal-progress">
<div class="progress-bar">
<div class="progress-fill" class:complete={pct >= 100} style:width="{pct}%"></div>
</div>
<span class="progress-text">
{goal.currentValue} / {goal.target.value}
<span class="period">({periodLabel(goal.target.period)})</span>
</span>
</div>
</div>
{/each}
{#each goals.value.filter((g) => g.status === 'paused') as goal (goal.id)}
<div class="goal-card paused">
<div class="goal-header">
<span class="goal-title">{goal.title}</span>
<button class="goal-action" onclick={() => goalStore.resume(goal.id)} title="Fortsetzen">
<Play size={12} weight="fill" />
</button>
<button
class="goal-action danger"
onclick={() => goalStore.delete(goal.id)}
title="Loeschen"
>
<Trash size={12} />
</button>
</div>
<span class="paused-label">Pausiert</span>
</div>
{/each}
{#if goals.value.length === 0 && !showTemplates}
<div class="empty">Keine Ziele aktiv. Tippe + um ein Ziel zu setzen.</div>
{/if}
</div>
</div>
<style>
.goals-page {
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.header {
display: flex;
justify-content: flex-end;
}
.add-btn {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.375rem 0.75rem;
border-radius: 9999px;
border: none;
background: hsl(var(--color-primary) / 0.1);
color: hsl(var(--color-primary));
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
}
.add-btn:hover {
background: hsl(var(--color-primary) / 0.2);
}
.templates {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.tpl-card {
display: flex;
flex-direction: column;
gap: 0.125rem;
padding: 0.5rem 0.75rem;
border: 1px solid hsl(var(--color-border));
border-radius: 0.5rem;
background: hsl(var(--color-card));
cursor: pointer;
text-align: left;
transition: all 0.15s;
}
.tpl-card:hover {
border-color: hsl(var(--color-primary) / 0.5);
}
.tpl-title {
font-size: 0.8125rem;
font-weight: 500;
color: hsl(var(--color-foreground));
}
.tpl-desc {
font-size: 0.6875rem;
color: hsl(var(--color-muted-foreground));
}
.goal-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.goal-card {
padding: 0.625rem 0.75rem;
border: 1px solid hsl(var(--color-border));
border-radius: 0.625rem;
background: hsl(var(--color-card));
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.goal-card.paused {
opacity: 0.6;
}
.goal-header {
display: flex;
align-items: center;
gap: 0.375rem;
}
.goal-title {
font-size: 0.8125rem;
font-weight: 500;
color: hsl(var(--color-foreground));
flex: 1;
}
.goal-action {
width: 24px;
height: 24px;
border-radius: 50%;
border: none;
background: transparent;
color: hsl(var(--color-muted-foreground));
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.goal-action:hover {
background: hsl(var(--color-surface-hover));
color: hsl(var(--color-foreground));
}
.goal-action.danger:hover {
color: hsl(var(--color-error));
}
.goal-progress {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.progress-bar {
height: 6px;
background: hsl(var(--color-muted) / 0.2);
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: hsl(var(--color-primary));
border-radius: 3px;
transition: width 0.3s ease;
}
.progress-fill.complete {
background: hsl(142 71% 45%);
}
.progress-text {
font-size: 0.6875rem;
color: hsl(var(--color-muted-foreground));
}
.period {
font-weight: 400;
}
.paused-label {
font-size: 0.6875rem;
color: hsl(var(--color-muted-foreground));
font-style: italic;
}
.empty {
font-size: 0.8125rem;
color: hsl(var(--color-muted-foreground));
text-align: center;
padding: 1.5rem;
}
</style>

View file

@ -0,0 +1,230 @@
<!--
Mein Tag — DaySnapshot overview as a workbench page.
Shows tasks, events, drinks, nutrition, places at a glance.
-->
<script lang="ts">
import { useDaySnapshot } from '$lib/data/projections/day-snapshot';
import { useStreaks } from '$lib/data/projections/streaks';
import { CheckCircle, CalendarBlank, Drop, ForkKnife, MapPin, Fire } from '@mana/shared-icons';
const day = useDaySnapshot();
const streaks = useStreaks();
</script>
<div class="myday">
<!-- Tasks -->
<div class="section">
<div class="section-header">
<CheckCircle size={18} weight="bold" />
<span>Tasks</span>
<span class="badge"
>{day.value.tasks.completed}/{day.value.tasks.total + day.value.tasks.completed}</span
>
</div>
{#if day.value.tasks.overdue > 0}
<div class="alert">{day.value.tasks.overdue} ueberfaellig</div>
{/if}
{#each day.value.tasks.dueToday.slice(0, 5) as t}
<div class="item">{t.title}</div>
{/each}
{#if day.value.tasks.dueToday.length === 0 && day.value.tasks.total === 0}
<div class="empty">Keine Tasks heute</div>
{/if}
</div>
<!-- Events -->
<div class="section">
<div class="section-header">
<CalendarBlank size={18} weight="bold" />
<span>Termine</span>
<span class="badge">{day.value.events.total}</span>
</div>
{#each day.value.events.upcoming.slice(0, 4) as e}
<div class="item">
<span class="time">{e.startTime.slice(11, 16)}</span>
{e.title}
</div>
{/each}
{#if day.value.events.total === 0}
<div class="empty">Keine Termine</div>
{/if}
</div>
<!-- Drinks -->
<div class="section">
<div class="section-header">
<Drop size={18} weight="bold" />
<span>Wasser</span>
<span class="badge">{day.value.drinks.water.percent}%</span>
</div>
<div class="progress-bar">
<div
class="progress-fill water"
style:width="{Math.min(day.value.drinks.water.percent, 100)}%"
></div>
</div>
<div class="stat">{day.value.drinks.water.ml} / {day.value.drinks.water.goal} ml</div>
{#if day.value.drinks.coffee.count > 0}
<div class="stat-secondary">{day.value.drinks.coffee.count}x Kaffee</div>
{/if}
</div>
<!-- Nutrition -->
<div class="section">
<div class="section-header">
<ForkKnife size={18} weight="bold" />
<span>Ernaehrung</span>
<span class="badge">{day.value.nutrition.meals} Mahlz.</span>
</div>
<div class="progress-bar">
<div
class="progress-fill cal"
style:width="{Math.min(day.value.nutrition.calories.percent, 100)}%"
></div>
</div>
<div class="stat">
{day.value.nutrition.calories.actual} / {day.value.nutrition.calories.goal} kcal
</div>
</div>
<!-- Streaks -->
{#if streaks.value.length > 0}
<div class="section">
<div class="section-header">
<Fire size={18} weight="bold" />
<span>Streaks</span>
</div>
{#each streaks.value as s}
<div class="streak-row">
<span class="streak-label">{s.label}</span>
<span
class="streak-count"
class:active={s.status === 'active'}
class:risk={s.status === 'at_risk'}
class:broken={s.status === 'broken'}
>
{s.currentStreak}d
</span>
</div>
{/each}
</div>
{/if}
</div>
<style>
.myday {
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.section {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.section-header {
display: flex;
align-items: center;
gap: 0.375rem;
font-weight: 600;
font-size: 0.8125rem;
color: hsl(var(--color-foreground));
}
.badge {
margin-left: auto;
font-size: 0.75rem;
font-weight: 500;
color: hsl(var(--color-muted-foreground));
}
.item {
font-size: 0.8125rem;
color: hsl(var(--color-foreground));
padding-left: 1.5rem;
}
.time {
font-size: 0.75rem;
color: hsl(var(--color-primary));
font-weight: 500;
margin-right: 0.375rem;
}
.alert {
font-size: 0.75rem;
color: hsl(var(--color-error, 0 84% 60%));
font-weight: 500;
padding-left: 1.5rem;
}
.empty {
font-size: 0.75rem;
color: hsl(var(--color-muted-foreground));
padding-left: 1.5rem;
font-style: italic;
}
.progress-bar {
height: 4px;
background: hsl(var(--color-muted) / 0.3);
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 2px;
transition: width 0.3s ease;
}
.progress-fill.water {
background: hsl(200 80% 55%);
}
.progress-fill.cal {
background: hsl(var(--color-primary));
}
.stat {
font-size: 0.75rem;
color: hsl(var(--color-foreground));
}
.stat-secondary {
font-size: 0.6875rem;
color: hsl(var(--color-muted-foreground));
}
.streak-row {
display: flex;
align-items: center;
justify-content: space-between;
padding-left: 1.5rem;
}
.streak-label {
font-size: 0.8125rem;
color: hsl(var(--color-foreground));
}
.streak-count {
font-size: 0.75rem;
font-weight: 600;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
}
.streak-count.active {
background: hsl(142 71% 45% / 0.15);
color: hsl(142 71% 45%);
}
.streak-count.risk {
background: hsl(45 93% 47% / 0.15);
color: hsl(45 93% 47%);
}
.streak-count.broken {
background: hsl(var(--color-muted) / 0.2);
color: hsl(var(--color-muted-foreground));
}
</style>

View file

@ -191,6 +191,19 @@ export const APP_ICONS = {
// Indigo→purple gradient for the nighttime/rest theme.
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="sl" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#6366f1"/><stop offset="100%" style="stop-color:#7c3aed"/></linearGradient></defs><rect width="100" height="100" rx="22" fill="url(#sl)"/><path d="M62 24c-18 2-32 17-32 35 0 19 16 35 35 35 12 0 22-6 28-14-4 2-9 3-14 3-19 0-35-16-35-35 0-10 4-18 10-24z" fill="white" fill-opacity="0.9"/><circle cx="68" cy="28" r="2.5" fill="white" fill-opacity="0.7"/><circle cx="78" cy="38" r="1.5" fill="white" fill-opacity="0.5"/><circle cx="58" cy="18" r="1.5" fill="white" fill-opacity="0.5"/><circle cx="82" cy="24" r="2" fill="white" fill-opacity="0.6"/></svg>`
),
// ── Companion Brain ─────────────────────────────────
myday: svgToDataUrl(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="md" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#F59E0B"/><stop offset="100%" style="stop-color:#F97316"/></linearGradient></defs><rect width="100" height="100" rx="22" fill="url(#md)"/><circle cx="50" cy="44" r="16" fill="white" fill-opacity="0.9"/><line x1="50" y1="20" x2="50" y2="26" stroke="white" stroke-width="3" stroke-linecap="round" opacity="0.7"/><line x1="50" y1="62" x2="50" y2="68" stroke="white" stroke-width="3" stroke-linecap="round" opacity="0.7"/><line x1="26" y1="44" x2="32" y2="44" stroke="white" stroke-width="3" stroke-linecap="round" opacity="0.7"/><line x1="68" y1="44" x2="74" y2="44" stroke="white" stroke-width="3" stroke-linecap="round" opacity="0.7"/><rect x="24" y="74" width="52" height="4" rx="2" fill="white" fill-opacity="0.5"/><rect x="30" y="82" width="40" height="3" rx="1.5" fill="white" fill-opacity="0.3"/></svg>`
),
eventstream: svgToDataUrl(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="es" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#6366F1"/><stop offset="100%" style="stop-color:#8B5CF6"/></linearGradient></defs><rect width="100" height="100" rx="22" fill="url(#es)"/><polyline points="20,55 35,40 50,50 65,30 80,45" fill="none" stroke="white" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.9"/><circle cx="35" cy="40" r="3" fill="white"/><circle cx="50" cy="50" r="3" fill="white"/><circle cx="65" cy="30" r="3" fill="white"/><circle cx="80" cy="45" r="3" fill="white"/><rect x="24" y="66" width="52" height="3" rx="1.5" fill="white" fill-opacity="0.4"/><rect x="24" y="74" width="36" height="3" rx="1.5" fill="white" fill-opacity="0.3"/></svg>`
),
companion: svgToDataUrl(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="cp" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#8B5CF6"/><stop offset="100%" style="stop-color:#A78BFA"/></linearGradient></defs><rect width="100" height="100" rx="22" fill="url(#cp)"/><rect x="25" y="28" width="50" height="36" rx="8" fill="white" fill-opacity="0.9"/><circle cx="40" cy="44" r="4" fill="#8B5CF6"/><circle cx="60" cy="44" r="4" fill="#8B5CF6"/><rect x="40" y="52" width="20" height="3" rx="1.5" fill="#8B5CF6" opacity="0.5"/><polygon points="35,64 45,64 38,76" fill="white" fill-opacity="0.9"/></svg>`
),
goals: svgToDataUrl(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="gl" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#10B981"/><stop offset="100%" style="stop-color:#059669"/></linearGradient></defs><rect width="100" height="100" rx="22" fill="url(#gl)"/><circle cx="50" cy="46" r="24" fill="none" stroke="white" stroke-width="3.5" opacity="0.4"/><circle cx="50" cy="46" r="16" fill="none" stroke="white" stroke-width="3" opacity="0.6"/><circle cx="50" cy="46" r="8" fill="none" stroke="white" stroke-width="2.5" opacity="0.8"/><circle cx="50" cy="46" r="3" fill="white"/><rect x="28" y="78" width="44" height="4" rx="2" fill="white" fill-opacity="0.4"/></svg>`
),
} as const;
export type AppIconId = keyof typeof APP_ICONS;

View file

@ -858,6 +858,65 @@ export const MANA_APPS: ManaApp[] = [
status: 'development',
requiredTier: 'guest',
},
// ── Companion Brain ─────────────────────────────────
{
id: 'myday',
name: 'Mein Tag',
description: { de: 'Tagesueberblick', en: 'Daily Overview' },
longDescription: {
de: 'Alle wichtigen Daten auf einen Blick: Tasks, Termine, Wasser, Ernaehrung, Streaks.',
en: 'All key data at a glance: tasks, events, water, nutrition, streaks.',
},
icon: APP_ICONS.myday ?? '☀️',
color: '#F59E0B',
comingSoon: false,
status: 'development',
requiredTier: 'guest',
},
{
id: 'eventstream',
name: 'Events',
description: { de: 'Live Event-Stream', en: 'Live Event Stream' },
longDescription: {
de: 'Echtzeit-Feed aller Aktionen ueber alle Module: Tasks, Drinks, Termine, Mahlzeiten.',
en: 'Real-time feed of all actions across modules: tasks, drinks, events, meals.',
},
icon: APP_ICONS.eventstream ?? '⚡',
color: '#6366F1',
comingSoon: false,
status: 'development',
requiredTier: 'guest',
},
{
id: 'companion',
name: 'Companion',
description: { de: 'AI Assistent', en: 'AI Assistant' },
longDescription: {
de: 'Dein persoenlicher AI-Begleiter. Fragt nach deinem Tag, erstellt Tasks, loggt Getraenke — alles per Chat.',
en: 'Your personal AI companion. Ask about your day, create tasks, log drinks — all via chat.',
},
icon: APP_ICONS.companion ?? '🤖',
color: '#8B5CF6',
comingSoon: false,
status: 'development',
requiredTier: 'guest',
},
{
id: 'goals',
name: 'Ziele',
description: { de: 'Ziele & Fortschritt', en: 'Goals & Progress' },
longDescription: {
de: 'Setze moduluebergreifende Ziele (Wasser, Tasks, Kalorien) und tracke deinen Fortschritt automatisch.',
en: 'Set cross-module goals (water, tasks, calories) and track progress automatically.',
},
icon: APP_ICONS.goals ?? '🎯',
color: '#10B981',
comingSoon: false,
status: 'development',
requiredTier: 'guest',
},
];
/**