# Lasts — Module Plan
## Status (2026-04-26)
**M1 Skelett: DONE** — `lasts/`-Modul registriert, Dexie v51, Encryption-Registry, Per-Space-Welcome-Seed, Route `/lasts` mountet mit Empty-State, Refactor `firsts/types.ts` extrahiert Categories nach `data/milestones/categories.ts` ohne API-Bruch.
**M2 CRUD + DetailView: DONE** — ListView mit StatusTabs (Alle | Vermutet | Bestätigt | Aufgehoben), Quick-Add (Suspected/Confirmed-Toggle, Enter erstellt + öffnet Detail), Context-Menu, Search ab > 5 Einträgen. DetailView (`views/DetailView.svelte` + Route `/lasts/entry/[id]`) mit always-editable Feldern, Autosave on blur/change, Lifecycle-Buttons (Bestätigen, Aufheben mit Inline-Note), Delete + Auto-Back. 44 i18n-Keys × 5 Locales.
**M3 Inbox + Inferenz: DONE** — Dexie v52 mit `lastsCooldown`-Tabelle (deterministische ID `${refTable}:${refId}`, 12-Monate-Cooldown). Inferenz-Engine (`inference/scan.ts` + `inference/sources/places.ts`) als Source-Registry-Pattern. Erste Quelle: Places — Heuristik `visitCount ≥ 5 ∧ Span ≥ 180d ∧ Silence ≥ 365d`. Orchestrator dedupliziert gegen existierende Lasts + Cooldown-Liste. `suggestLasts()`-Store-Methode triggert Scan + schreibt Survivors als `suspected` mit `inferredFrom`. InboxView (`views/InboxView.svelte` + Route `/lasts/inbox`) mit "Jetzt scannen"-Button + Akzeptieren (löscht inferredFrom → bleibt suspected im Hauptfeed) / Verwerfen (delete + cooldown). ListView trägt Inbox-Link + Live-Count rechts in der Tab-Bar.
**Deferred zu M3.b**: `contacts`-Source braucht `lastInteractionAt`-Feld auf Contact-Records (existiert nicht); `habits`-Source braucht direkten Timestamp im HabitLog (aktuell via `timeBlockId`-Join). Beide nachziehen sobald jeweilige Felder existieren oder via separater Aggregation. Tabu-Liste (kein Auto-Suggest für `relationship: family|partner` in contacts, no refs zu period/dreams/losses/regret) wird erst beim Hinzufügen der jeweiligen Source aktiv — Hooks sind im Orchestrator vorbereitet (kann pro Source-Scanner früh ausgefiltert werden, bevor Kandidat zurückkehrt).
**M4 AI-Tools: DONE** — 5 Tools im `AI_TOOL_CATALOG` (`@mana/shared-ai/src/tools/schemas.ts`):
- `create_last` (propose) — neuer Last suspected | confirmed
- `confirm_last` (propose) — suspected → confirmed mit Reflexion (date, meaning, whatIKnewThen, whatIKnowNow, tenderness 1-5, wouldReclaim no/maybe/yes)
- `reclaim_last` (propose) — confirmed → reclaimed mit optionaler Note
- `list_lasts` (auto) — gefiltert nach status + category, max 100
- `suggest_lasts` (auto) — triggert Inferenz-Engine, schreibt Survivors als suspected mit inferredFrom in Inbox
Webapp-Implementierungen in `lasts/tools.ts` (Vault-locked-Handling für `list_lasts`, Validierung von Enums + Range-Checks). Registriert in `data/tools/init.ts`. Server-side Planner-Drift-Test (`services/mana-ai/src/planner/tools.test.ts`) bestätigt Konsistenz: 4/4 grün. Shared-AI Schema-Tests: 6/6 grün.
**Nicht in M4**: `` Wiring in ListView entfällt — die Komponente existiert nicht im Repo (root `apps/mana/CLAUDE.md` beschreibt sie als "wired in /todo, /calendar, /places, /drink, /food, /news, /notes" aber `find apps/mana -iname "*proposal*"` liefert null). Aspirational-Doc-Drift, nicht meine M4-Lieferung. Sobald die Komponente existiert, ist `` ein Einzeiler in `lasts/ListView.svelte` zwischen Tab-Bar und Quick-Add.
**M5 Reminders + Settings: DONE** (Pivot zu In-App-Banner statt OS-Push) — kein PWA-Push-System existiert im Repo (`mana-notify` ist server-side für Email/Web-Push, kein Service-Worker-Push-Subscription, keine `Notification.requestPermission()` Aufrufe in der webapp). Pragmatischer Pfad analog zum **augur `DueBanner`-Pattern**: in-app surfacing der heutigen Lasts beim Öffnen von `/lasts`, opt-in-toggelbar in den Settings.
Lieferung:
- **Pure Date-Math** (`lib/reminders.ts`): `isSameDayOfYear`, `yearsBetween`, `findAnniversaryLasts` (confirmed lasts mit `date` heute vor X Jahren), `findRecognitionAnniversaryLasts` (any status mit `recognisedAt` heute vor X Jahren). 12 Vitest-Cases, alle grün.
- **Settings-Store** (`stores/settings.svelte.ts`) via `createAppSettingsStore('lasts-settings', …)`: 4 persistent localStorage-Flags — `anniversaryReminders`, `recognitionReminders`, `inboxNotify`, `bannerMaxItems` (Default 3). Modul-Pattern analog `todoSettings`/`broadcastSettings`.
- **DueBanner-Component** (`components/DueBanner.svelte`): rendert max-N Zeilen — Anniversaries → Recognition-Anniversaries → Inbox-Notify in dieser Priorität, deduplicated wenn Anniversary + Recognition denselben Last treffen. Klick → `/lasts/entry/[id]` oder `/lasts/inbox`.
- **SettingsView + Route** (`views/SettingsView.svelte` + `/lasts/settings`): 3 Toggles + Slider für `bannerMaxItems` + "Zurücksetzen" + "Test-Banner zeigen" (rendert 4 Sek Beispiel) + Footnote zur fehlenden OS-Push-Infrastruktur.
- **ListView-Wiring**: `` ganz oben, `⚙`-Settings-Link in der Tab-Bar.
- **i18n**: 22 neue Keys × 5 Locales (banner.* + settings.*).
**Deferred zu M5.b — echtes OS-Push** sobald PWA-Push-Infra existiert: Service-Worker-Subscription via `Notification.requestPermission()` + Push-Subscription-Endpoint, `mana-notify`-Backend-Cron für Anniversary-Scans (statt client-side beim App-Öffnen), Hard-Cap 2 Pushs/Monat als Server-Throttle. Die Date-Math-Helper (`findAnniversaryLasts` + `findRecognitionAnniversaryLasts`) sind bereits push-tauglich purer Code ohne Svelte-Runen-Bindings — können der Server-Cron ohne Refactor wiederverwendet werden.
**Vor-Push-Validatoren**: `validate:i18n-parity` rot wegen pre-existing untracked WIP `apps/mana/apps/web/src/lib/i18n/locales/settings/{de,en,es,fr}.json` — `it.json` fehlt; nicht in git history, mtime = 20:42 (nicht meine Lieferung; vermutlich parallele Session oder Hook). Mein `lasts/`-Namespace hat alle 5 Locales aligned.
**M6 Visibility + Unlisted-Sharing: DONE** — Modul auf das Repo-weite `@mana/shared-privacy`-System aufgesattelt, analog augur/library/places/events.
Lieferung:
- **Type-Erweiterung**: `LocalLast` und `Last` haben jetzt `visibility`, `visibilityChangedAt`, `visibilityChangedBy`, `unlistedToken`, `unlistedExpiresAt`. `toLast` setzt Default `'private'` (intim, anders als firsts).
- **Encryption-Registry**: visibility/Token-Felder bleiben plaintext (Server-Routing-Felder, keine User-typed Inhalte). Crypto-Audit weiter sauber bei 211 Tables.
- **Per-Space-Welcome-Seed**: explizit `visibility: 'private'`.
- **Resolver**: `buildLastBlob` in `data/unlisted/resolvers.ts` — Whitelist nur "reflective core" (title, status, category, date, meaning, whatIKnewThen, whatIKnowNow, tenderness, wouldReclaim). `note`, `inferredFrom`, person/place/media-Refs, recognisedAt, reclaimedNote bleiben PRIVAT. **Hard-Block: reclaimed Lasts werden nicht serialisiert** — die zurückgekommen-Emotion ist verletzlicher als der Last selbst.
- **Store-Methoden** (`stores/items.svelte.ts`): `setVisibility(id, level)` mit publish/revoke unlisted-Snapshot via `@mana/shared-privacy/unlisted-client`, `regenerateUnlistedToken(id)` für Token-Rotation, `setUnlistedExpiry(id, date)` für TTL-Update. Reclaim-Lasts → unlisted wird im Store geblockt mit klarer Fehlermeldung.
- **SharedLastView** (`SharedLastView.svelte`): public-render-Komponente, kontemplativer Ton, weisse Karte mit Kategorie-Akzent links, "Damals / Heute"-Reflexion zweispaltig, optional Tenderness-Stars + WouldReclaim. "via Mana Lasts" Footer, kein Marketing.
- **Share-Dispatcher**: `routes/share/[token]/+page.svelte` kennt jetzt `data.collection === 'lasts'`.
- **DetailView-Wire**: `` + `` Block oberhalb der Lifecycle-Action-Bar — nur sichtbar für non-reclaimed Lasts.
- **i18n**: `lasts.detail.visibilityLabel` × 5 Locales.
**M6 Done-Definition**: ✓ Last kann auf `unlisted` gesetzt werden, Share-Link funktioniert öffentlich ohne Login (Snapshot-Server-Resolution via mana-api `/api/v1/unlisted/public/`, dann SSR-Render via `SharedLastView`).
**Vor-Push-Validatoren** weiter: 2 svelte-check-Errors in `SettingsSidebar.svelte` — gleiche Orphan-WIP-Quelle wie `settings/it.json` (nicht meine Lieferung). Mein Code: 0/0/0 in allen reminders.test (12/12), i18n-keys baseline-equal, crypto 211 ✓.
**M7 Timeline-Aggregator + Year-Recap: DONE** — Cross-modulares "Meilensteine"-Surface, das firsts ∪ lasts als ein chronologisches Feed rendert.
Lieferung:
- **Pure Aggregator** `data/milestones/timeline-query.ts`: `mergeMilestones(firsts, lasts)` mit Discriminator-Direction (`'first'` | `'last'`), Pinned-First-Sort, Date-desc-fallback-zu-createdAt. Plus `filterByDirection`, `filterByYear`, `compareTimelineDesc`. Reactive Hook `useMilestonesTimeline()` lädt beide Tabellen parallel + dekodiert client-seitig.
- **Recap-Aggregator** `data/milestones/year-recap.ts`: `buildMilestonesRecap(entries, year)` → `{ year, total, firsts, lasts, byCategory: per-Cat × per-Direction, topFirsts (5), topLasts (5), activeMonths: 'YYYY-MM'[] }`. Bewusst nur Counts, keine Hit-Rate/Brier-Style-Metriken (lasts/firsts haben kein "verifizierbares" Element).
- **Tests** `timeline-query.test.ts`: 12/12 passed (mergeMilestones, filterByDirection, filterByYear, buildMilestonesRecap mit allen Feldern, compareTimelineDesc).
- **TimelineView** (`lib/components/milestones/TimelineView.svelte`): Tab-Bar (Alle | Firsts | Lasts), Karten mit Direction-Chip + Kategorie-Pille, Klick → jeweilige Modul-Detail-Route. Recap-Link top-right zum aktuellen Jahr.
- **YearRecapView** (`lib/components/milestones/YearRecapView.svelte`): Hero-Stats (Total | Firsts | Lasts mit direction-coloring), Kategorie-Breakdown mit Per-Direction-Counts, Top-5-Listen pro Direction (klickbar), Active-Months-Strip.
- **Routes** `/milestones` und `/milestones/recap/[year]` (nutzen `RoutePage` mit `appId="milestones"` — Registry hat keinen Eintrag, fallback rendert sauber mit Title-Override).
- **Cross-Link** in `lasts/ListView.svelte` Tab-Bar: "Meilensteine"-Link führt zu `/milestones`.
- **i18n-Namespace** `milestones/` × 5 Locales mit `timeline.*`, `tabs.*`, `recap.*` Keys (i18n-parity nun 39 namespaces × 5 locales aligned).
**M7 Done-Definition**: ✓ Timeline-View zeigt firsts und lasts interleaved, sortierbar (date desc, pinned-first), filterbar (direction tabs, year-filter über Recap-Route).
**Nicht in M7** (Polish): Eintrag in `packages/shared-branding/src/mana-apps.ts` als "milestones"-App mit Icon/Color für den App-Launcher. Ohne Eintrag funktioniert die Route via direkte URL-Navigation oder den Cross-Link von `/lasts`. Kann später ergänzt werden — kostet einen App-Icon-SVG und einen mana-apps.ts-Block.
---
**M1-M7 SHIPPED** — Modul `lasts` ist feature-komplett gemäß Plan. Validation: 0/0/0 in svelte-check (7645 files), 24/24 tests grün (12 reminders + 12 timeline), i18n-parity 39×5 aligned (+2 namespaces: lasts, milestones), i18n-keys baseline-equal, crypto 211 tables. Browser-Test offen (`pnpm run mana:dev` → `/lasts`, `/lasts/inbox`, `/lasts/settings`, `/lasts/entry/[id]`, `/milestones`, `/milestones/recap/2026`).
Vorbild: das bereits existierende Modul [`firsts/`](../../apps/mana/apps/web/src/lib/modules/firsts/) (Bucket-List + Reflexion mit `dream → lived` Lifecycle, 11 Kategorien, Foto/Audio/Place/People). `lasts` ist das spiegelbildliche Modul: das *letzte* Mal, dass du etwas getan/gefühlt/gesehen hast — meistens erst rückwirkend erkennbar.
---
## Ziel
Ein Modul `lasts`, in dem der Nutzer **Letzte Male** erfasst und reflektiert. Kernfrage: *"Wann habe ich das eigentlich zum letzten Mal getan/gefühlt — und wusste ich's damals?"*
Zwei Eingabewege:
1. **Manuell** — der User markiert bewusst (selten, oft an Wendepunkten: letzter Tag im Job, letztes Konzert mit X, letzte Nacht in der alten Wohnung).
2. **Inferred** — die AI scannt regelmässig die anderen Module (places, contacts, food, habits, routes, music) auf Frequenz-Muster und schlägt Last-Kandidaten in einer Inbox vor: *"Du warst seit 18 Monaten nicht mehr in [Café X] — vorher 3×/Woche. Last?"*
Nicht im Scope:
- Trauer-Workflow für Verluste/Tod (eigenes Modul `losses` aus Module-Ideas).
- Bucket-List / Vorfreude — bleibt bei `firsts`.
- Streak-Tracking — bleibt bei `habits`.
## Abgrenzung
- **Nicht `firsts`**: Tonalität ist anders (Kontemplation vs. Vorfreude), Lifecycle ist anders (`suspected → confirmed` vs. `dream → lived`), Push-Quoten sind anders. Eigenes Modul, eigene Tabelle. Geteilt wird nur der Code drumherum (Komponenten, Kategorien, Picker).
- **Nicht `losses`**: dort gehört der Trauer-Workflow für markierte Verluste hin. `lasts` ist breiter und oft *zärtlich* statt schmerzhaft. Ein Last kann zu einem Loss eskaliert werden (Cross-Link), aber ein Loss erzeugt keinen Last.
- **Nicht `eras`**: Eras aggregieren ganze Lebensabschnitte. `lasts` sind die Endpunkte einzelner Dinge — Eras können auf Lasts referenzieren ("Burnout-Jahr endete mit folgenden Lasts: …").
- **Nicht `journal`**: ein Journal-Eintrag ist datiert auf den Schreibtag; ein Last ist datiert auf das (vermutete) Ereignis. Reflexion lebt im Last-Datensatz selbst, nicht als verlinkter Journal-Eintrag.
## Architektur-Entscheidung: zwei Tabellen, geteilte Komponenten
**Eigene Dexie-Tabelle `lasts`** — nicht `milestones` mit Diskriminator. Begründung in der vorgelagerten Diskussion (Modul-Boundary, `_pendingChanges`-Tagging, Encryption-Registry pro Tabelle, eigene Visibility-Defaults, eigene Migrations).
**Geteilt** wird stattdessen alles ausserhalb der Tabelle:
- Kategorien (11 Stück, identisch mit `firsts`) → `lib/data/milestones/categories.ts`
- Lifecycle-Helpers, Validators → `lib/data/milestones/lifecycle.ts`
- Timeline-Aggregator-Query (firsts + lasts gemerged) → `lib/data/milestones/timeline-query.ts`
- UI-Komponenten (Card, Editor, ReflectionFields, LifecycleToggle, CategoryPill) → `lib/components/milestones/`
Das macht ein zukünftiges drittes Geschwister-Modul (`peaks`, `pivots`, `cycles`) trivial — nur neue Tabelle + Lifecycle-Strings, alles andere ist da.
## Lifecycle-Mapping
| `firsts` | `lasts` |
|---|---|
| `dream` (geplant, will ich erleben) | `suspected` (vermutet, vom User oder AI markiert) |
| `lived` (gemacht, mit Reflexion) | `confirmed` (bestätigt, mit Reflexion) |
| — (kein Rückwärts-Pfad) | `reclaimed` (war doch nicht das letzte Mal — ist wieder passiert) |
`reclaimed` ist semantisch wichtig: das Modul soll mit dem Leben atmen. Wenn du wieder mit der Person sprichst oder doch wieder ins Café gehst, klickst du "Aufgehoben" — der Eintrag bleibt in der History (mit Notiz "Aufgehoben am …"), erscheint aber nicht mehr im Hauptfeed. Reclaimed-Items sind ihre eigene kleine emotional bedeutsame Untersicht.
## Felder-Mapping (`lasts` ↔ `firsts`)
| `firsts` Feld | `lasts` Feld | Bemerkung |
|---|---|---|
| `title` | `title` | identisch |
| `status: 'dream'\|'lived'` | `status: 'suspected'\|'confirmed'\|'reclaimed'` | Discriminator |
| `category` | `category` | gleiches Enum |
| `motivation` | `meaning` | "Was hat es dir bedeutet?" statt "Warum willst du das?" |
| `priority: 1\|2\|3` | `confidence: 'probably'\|'likely'\|'certain'` | Wie sicher ist es das letzte Mal? |
| `date` | `date` | Vermutetes/bestätigtes Datum (oft approximativ) |
| `note` | `note` | identisch |
| `expectation` | `whatIKnewThen` | "Was wusstest du damals nicht?" |
| `reality` | `whatIKnowNow` | "Was weisst du jetzt?" |
| `rating: 1-5` | `tenderness: 1-5` | Nicht "gut/schlecht" — wie sehr berührt es dich heute |
| `wouldRepeat: yes\|no\|definitely` | `wouldReclaim: no\|maybe\|yes` | Würdest du es zurückholen, wenn du könntest? |
| `personIds[]` | `personIds[]` | identisch |
| `placeId` | `placeId` | identisch |
| `mediaIds[]` | `mediaIds[]` | identisch |
| `audioNoteId` | `audioNoteId` | identisch |
| `sharedWith` | `sharedWith` | identisch |
| `isPinned`, `isArchived` | `isPinned`, `isArchived` | identisch |
| — | `recognisedAt` | Wann wurde es als Last erkannt (oft Jahre nach `date`) — wichtig für "vor X Jahren erkannt"-Reminder |
| — | `inferredFrom` | Optionales Provenance-Object: `{ tool: 'suggest_lasts', refTable: 'places', refId: '...', frequencyHint: '3x/week → 0 in 18mo' }` für AI-Vorschläge |
## Modul-Struktur
```
apps/mana/apps/web/src/lib/modules/lasts/
├── types.ts # LocalLast, Last, LastStatus, LastConfidence, WouldReclaim, Tenderness
├── collections.ts # lastTable + LASTS_GUEST_SEED (1 confirmed + 1 suspected Beispiel)
├── queries.ts # useAllLasts, useLastsByStatus, useLastsByCategory, useLast(id), useLastsInbox (suspected only), useLastsStats
├── stores/
│ └── items.svelte.ts # createLast, updateLast, confirmLast, reclaimLast, suggestLasts (Inferenz-Loop), pin/archive/delete
├── tools.ts # AI-Tools: create_last (propose), confirm_last (propose), reclaim_last (propose), list_lasts (auto), suggest_lasts (auto)
├── inference/
│ └── scan.ts # Cross-Modul-Reader: places/contacts/food/habits/routes für Frequenz-Drops
├── ListView.svelte # Modul-Root (komponiert StatusTabs + Liste, leitet zu InboxView)
├── InboxView.svelte # Suspected-Vorschläge: Akzeptieren / Verwerfen
├── DetailView.svelte # Einzelansicht inkl. Reflexion + Reclaim-Button
├── module.config.ts # { appId: 'lasts', tables: [{ name: 'lasts' }] }
└── index.ts # Re-Exports
```
```
apps/mana/apps/web/src/lib/data/milestones/ # NEU — geteilt firsts ↔ lasts
├── categories.ts # MilestoneCategory, CATEGORY_LABELS, CATEGORY_COLORS (extrahiert aus firsts/types.ts)
├── lifecycle.ts # Status-Transition-Helpers, Validators
├── shared-types.ts # Person/Place/Media-Ref-Shapes (re-exports BaseRecord)
└── timeline-query.ts # useMilestonesTimeline() — Union-Query firsts ∪ lasts, sortiert nach date
```
```
apps/mana/apps/web/src/lib/components/milestones/ # NEU — geteilte UI
├── MilestoneCard.svelte # generisch, props: direction, status, category, title, date, isPinned
├── MilestoneEditor.svelte # Formular-Body — slot-basiert für direction-spezifische Reflexions-Felder
├── ReflectionFields.svelte # zwei Textareas, Labels via props
├── LifecycleToggle.svelte # generisch, status-options via props
├── CategoryPill.svelte # Farb-Pill aus CATEGORY_COLORS
└── PeoplePlaceMediaPicker.svelte # bündelt die drei Picker (existieren schon einzeln)
```
```
apps/mana/apps/web/src/routes/(app)/
├── lasts/+page.svelte # NEU — Modul-Root
├── lasts/[id]/+page.svelte # NEU — Detail-Route
├── lasts/inbox/+page.svelte # NEU — Suspected-Inbox (separate Route weil eigenes mentales Modell)
└── milestones/+page.svelte # OPTIONAL M7 — Timeline-Aggregator firsts + lasts
```
## Daten-Schema
### `LocalLast` (Dexie)
```typescript
import type { BaseRecord } from '@mana/local-store';
import type { MilestoneCategory } from '$lib/data/milestones/categories';
export type LastStatus = 'suspected' | 'confirmed' | 'reclaimed';
export type LastConfidence = 'probably' | 'likely' | 'certain';
export type WouldReclaim = 'no' | 'maybe' | 'yes';
export interface InferredFrom {
tool: string; // z.B. 'suggest_lasts'
refTable: string; // 'places' | 'contacts' | 'food' | 'habits' | 'routes' | …
refId: string;
frequencyHint?: string; // human-readable: '3x/week → 0 in 18mo'
scannedAt: string; // ISO
}
export interface LocalLast extends BaseRecord {
title: string;
status: LastStatus;
category: MilestoneCategory;
// Recognition phase
confidence: LastConfidence | null; // wie sicher
inferredFrom: InferredFrom | null; // null = manuell
// Confirmed phase (Reflexion)
date: string | null; // ISO date — vermutet oder bestätigt
meaning: string | null; // "was hat es bedeutet"
note: string | null;
whatIKnewThen: string | null;
whatIKnowNow: string | null;
tenderness: number | null; // 1-5
wouldReclaim: WouldReclaim | null;
// Reclaimed phase
reclaimedAt: string | null; // ISO — falls aufgehoben
reclaimedNote: string | null; // optional Begründung
// Social
personIds: string[];
sharedWith: string | null;
// Rich media
mediaIds: string[];
audioNoteId: string | null;
placeId: string | null;
// Meta
recognisedAt: string; // wann wurde der Last erkannt (≠ createdAt nicht garantiert, aber meist gleich)
isPinned: boolean;
isArchived: boolean;
}
```
### Domain-Typ `Last` — gleiche Form ohne BaseRecord-Internals (analog `firsts/types.ts`).
### Encryption-Registry
In `apps/mana/apps/web/src/lib/data/crypto/registry.ts` (analog zu `firsts`-Block):
```typescript
// ─── Lasts ───────────────────────────────────────────────
// User-typed text fields are encrypted. Status, category, confidence, dates,
// tenderness, wouldReclaim, personIds, mediaIds, placeId, inferredFrom stay
// plaintext for indexing/filtering and so the inference scanner can read
// provenance without master-key access.
lasts: {
enabled: true,
fields: ['title', 'meaning', 'note', 'whatIKnewThen', 'whatIKnowNow', 'reclaimedNote', 'sharedWith'],
},
```
### Dexie-Migration
Neue Version `db.version(51)` in `apps/mana/apps/web/src/lib/data/database.ts`:
```typescript
db.version(51).stores({
// … alle existierenden Tabellen 1:1 übernehmen aus v50 …
lasts: 'id, spaceId, userId, status, category, date, recognisedAt, isPinned, isArchived',
});
```
Index-Strategie:
- `status` — schnelle Filter für Inbox vs. Confirmed-Liste
- `category` — Kategorie-Filter
- `date` — Sort + Anniversary-Scans
- `recognisedAt` — "vor X Jahren erkannt"-Reminder
- `isPinned`, `isArchived` — Standard-Listing-Filter
Kein Soft/Hard-Split nötig — neue Tabelle, keine bestehenden Daten zu migrieren.
## Inferenz-Engine (`inference/scan.ts`)
Heuristik pro Quell-Modul:
| Quelle | Signal |
|---|---|
| `places` | Visit-Frequenz drop: war `≥ N visits / month` über `≥ M months`, jetzt `0 visits` seit `≥ K months` |
| `contacts` | Last-contact-date in `contacts.lastInteractionAt` (falls vorhanden) — wenn `> threshold` Monate und vorher häufig |
| `food` | Gericht in `meals` regelmässig, jetzt nicht mehr |
| `habits` | Habit pausiert oder seit X nicht mehr geloggt |
| `routes`/`hikes` | Route mit Wiederholungs-Counter, jetzt 0 |
| `music` (falls Listening-Logs existieren) | Künstler-Drop |
| `notes`/`writing`/`quotes` | Tag/Theme-Frequenz-Drop |
Default-Schwellen konservativ (Inbox-Lärm ist tödlich für die emotionale Wirkung):
- minimale Vorgeschichte: ≥ 5 Vorkommen über ≥ 6 Monate
- minimale Stille: ≥ 12 Monate ohne Vorkommen
- max. Vorschläge pro Scan: 3
- Cooldown: keine Wiedervorschläge derselben `(refTable, refId)` für 12 Monate nach Verwerfen
Cron: einmal pro Monat, z.B. am 1. um 9:00 Lokalzeit. Ausführung im AI-Mission-Runner als Mission `lasts.monthly-scan` (oder als simpler client-seitiger Cron-Job — tendiere zu Mission, weil dadurch Audit-Log + Pause-Switch gratis). Modul-Owner: einer der bestehenden Agents oder ein dedizierter "Gefährte"-Agent.
**Hard rules** (in der Heuristik verdrahtet, nicht User-konfigurierbar):
- Keine Vorschläge für `contacts` mit `relationship: 'family' | 'partner'` ohne explizite Opt-In — Trauer-Trigger.
- Keine Vorschläge für Refs in `period`, `dreams`, `losses`, `regret/forgive` — zu intim.
- Wenn `losses` einen Eintrag mit gleicher `personId` hat, suspend alle Inferenz für diese Person komplett.
## AI-Tool-Coverage
Im `AI_TOOL_CATALOG` in `@mana/shared-ai/src/tools/schemas.ts`:
| Modul | Propose | Auto |
|---|---|---|
| **lasts** | `create_last`, `confirm_last`, `reclaim_last` | `list_lasts`, `suggest_lasts` |
Schemas (skizziert):
```typescript
create_last: {
policyHint: 'standard',
input: { title, category, status?, date?, confidence?, meaning?, note?, personIds?, placeId? }
}
confirm_last: {
policyHint: 'standard',
input: { id, date?, whatIKnewThen?, whatIKnowNow?, tenderness?, wouldReclaim? }
}
reclaim_last: {
policyHint: 'standard',
input: { id, reclaimedAt, reclaimedNote? }
}
list_lasts: {
policyHint: 'read',
input: { status?, category?, sinceDate? }
}
suggest_lasts: {
policyHint: 'read', // liefert Vorschläge, schreibt nicht
input: { sources?: string[], minMonthsSilent?, limit? }
}
```
`suggest_lasts` schreibt selbst nichts — der Planner kann das Resultat in eine `create_last`-Proposal umwandeln, die der User approved.
## Push-Notifications (M5)
Drei opt-in-Klassen, getrennt umschaltbar in `/lasts/settings`:
1. **Anniversary-Reminder** — "Heute vor X Jahren das letzte Mal …" (nur für `confirmed` mit `date`).
2. **Recognition-Reminder** — "Vor X Jahren als Last erkannt: …" (nutzt `recognisedAt`).
3. **Inbox-Notify** — "3 neue Last-Vorschläge in der Inbox" (max. 1×/Monat nach dem Scan).
Hard-Cap insgesamt: 2 Push pro Monat. Snooze-pro-Item.
Implementierung über das bestehende Notification-System (zu prüfen: existiert das schon zentral, oder ad-hoc pro Modul?). Falls noch nicht vorhanden: M5 als separater Sub-Plan, M1-M4 funktionieren ohne.
## Visibility / Sharing
Default-Visibility: `private`. Anders als `firsts` (oft teilbar — du erzählst gerne von deinem ersten Mal Bungee-Jumping) sind Lasts intim.
Embed-Resolver für Visibility-System (analog `events`/`library` aus `project_visibility_system.md`-Memory): einzelne `lasts` können `unlisted` werden für `/share/[token]`-Routen, mit QR + Expiry. Sinnvolle Public-Aggregate kommen erst in M7+:
- "Lasts of 2026" Year-Recap (anonymisiert/kuratiert)
- Embed auf personal-site: poetische Sammlung kuratierter Lasts
## Refactor `firsts/` (Vorbereitung M1)
Damit `lasts` die geteilten Pieces wirklich nutzen kann, muss `firsts/` minimal umgebaut werden:
1. **Extract Categories**: `CATEGORY_LABELS`, `CATEGORY_COLORS` aus `firsts/types.ts` raus → `data/milestones/categories.ts`. `firsts/types.ts` re-exportiert für Abwärtskompatibilität.
2. **Extract MilestoneCard**: aus `firsts/ListView.svelte` die Listen-Item-Markup-Logik extrahieren in `components/milestones/MilestoneCard.svelte`. `ListView.svelte` rendert dann ``.
3. **Optional jetzt, sicher später**: ReflectionFields, LifecycleToggle, CategoryPill, PeoplePlaceMediaPicker analog extrahieren. Nicht im kritischen Pfad — kann passieren, wenn `lasts` sie real braucht.
Klassischer **soft-first**-Migrationsstil (siehe Memory `feedback_soft_before_hard_migrations.md`): zuerst extrahieren mit Re-Export-Aliassen, dann später Imports umstellen, dann Aliasse löschen. Aber kein Schema-Change — nur Code-Move, deshalb risikoarm.
## Milestones
### M1 — Refactor + Skelett (~ 1 Tag)
- `data/milestones/categories.ts` extrahieren, `firsts/types.ts` re-exportiert
- `components/milestones/MilestoneCard.svelte` extrahieren, `firsts/ListView.svelte` umstellen
- `lasts/` Modul-Skelett: `module.config.ts`, `types.ts`, `collections.ts`, `index.ts`
- Dexie v51 mit `lasts`-Tabelle
- Encryption-Registry-Eintrag
- Guest-Seed: 1 confirmed Beispiel ("Letzter Tag im alten Job"), 1 suspected ("Vermutlich letztes Mal …")
- Route `/lasts/+page.svelte` mountet leer mit "Noch keine Lasts"
**Done-Definition**: `lasts`-Modul lädt, leere Liste rendert, Dexie-Inspector zeigt Tabelle, `validate:all` grün.
### M2 — CRUD + ListView + DetailView (~ 1 Tag)
- `stores/items.svelte.ts`: createLast, updateLast, confirmLast, reclaimLast, pin/archive/delete
- `queries.ts`: useAllLasts, useLastsByStatus, useLast(id)
- `ListView.svelte`: StatusTabs (Suspected | Confirmed | Reclaimed), MilestoneCard-basierte Liste
- `DetailView.svelte` + Route `/lasts/[id]/+page.svelte`: Reflexionsfelder, Lifecycle-Buttons
- Editor-Component (kann inline oder modal): nutzt geteilten `MilestoneEditor` mit lasts-spezifischen Reflexions-Labels
**Done-Definition**: User kann Last manuell anlegen, von suspected nach confirmed bewegen, reflektieren, reclaimen.
### M3 — Inbox + Inference (~ 1 Tag)
- `inference/scan.ts`: Place-Drop + Contact-Drop + Habit-Drop Heuristiken (erste drei reichen für M3)
- `InboxView.svelte` + Route `/lasts/inbox/+page.svelte`: Liste der suspected mit `inferredFrom != null`, Akzeptieren/Verwerfen
- `suggestLasts()`-Methode im Store (nicht der AI-Tool — direct call), die einen Scan triggern und Suspected-Records anlegen kann
- "Scan jetzt"-Button für Dev/Manual-Trigger
- Cooldown-Liste: Tabelle `lastsInferenceCooldown` oder Feld in einer Settings-Tabelle für die "verworfen, nicht wieder vorschlagen"-Logik
**Done-Definition**: Manueller Scan-Trigger erzeugt sinnvolle Vorschläge basierend auf realen places/contacts/habits-Daten; Verwerfen unterdrückt Wiedervorschlag.
### M4 — AI-Tools (~ 0.5 Tage)
- `tools.ts` mit den fünf Tool-Definitionen
- Eintrag in `AI_TOOL_CATALOG` (`@mana/shared-ai/src/tools/schemas.ts`)
- Server-side Planner-Drift-Check läuft automatisch grün
- `` in ListView eingebaut
- Tool-Implementierungen in `data/ai/tools/` analog zu existierenden Modulen
**Done-Definition**: AI-Mission "Schau mal, ob es Lasts gibt" generiert Proposals, User kann approven, Eintrag landet als `suspected` mit `inferredFrom`.
### M5 — Push-Notifications + Settings (~ 0.5 Tage, abhängig vom Notification-System)
- `/lasts/settings/+page.svelte` mit drei opt-in-Toggles
- Anniversary-Scan (Cron + match auf `date` mit Jahres-Differenz)
- Recognition-Scan (Cron + match auf `recognisedAt`)
- Inbox-Notify-Hook nach `suggest_lasts`
- Hard-Cap-Logik
**Done-Definition**: Toggles persistent, eine Push-Test-Funktion sendet Beispiel.
### M6 — Visibility + Unlisted-Sharing (~ 0.5 Tage)
- Default-Visibility auf `private`
- VisibilityPicker in DetailView
- Embed-Resolver für `lasts` registrieren (analog `events`/`library`)
- `/share/[token]`-Route lädt einzelnes Last in lesbarem Format
**Done-Definition**: Last kann auf `unlisted` gesetzt werden, Share-Link funktioniert öffentlich ohne Login.
### M7 — Optional: Timeline-Aggregator + Year-Recap
- `data/milestones/timeline-query.ts`: Union-Query firsts ∪ lasts
- `routes/(app)/milestones/+page.svelte`: chronologische Timeline beider, Filter nach Direction
- Year-Recap-Page mit Top-N Lasts + Firsts des Jahres (analog Augur-Year-Recap aus Memory)
**Done-Definition**: Timeline-View zeigt firsts und lasts interleaved, sortierbar, filterbar.
## Per-Space-Seeds
`lasts` ist per-space (analog allen post-spaces-foundation Modulen). Ein Per-Space-Seeder registriert sich in `apps/mana/apps/web/src/lib/data/seeds/lasts.ts`:
```typescript
registerSpaceSeed('lasts-welcome', async (spaceId) => {
const id = `seed-welcome-${spaceId}`;
if (await db.table('lasts').get(id)) return;
await db.table('lasts').add({
id, spaceId,
title: 'Willkommen bei Lasts',
status: 'confirmed',
category: 'other',
confidence: 'certain',
inferredFrom: null,
date: new Date().toISOString().slice(0, 10),
meaning: 'Hier fängst du an, deine "letzten Male" festzuhalten.',
note: null,
whatIKnewThen: null,
whatIKnowNow: null,
tenderness: 3,
wouldReclaim: null,
reclaimedAt: null,
reclaimedNote: null,
personIds: [], sharedWith: null,
mediaIds: [], audioNoteId: null, placeId: null,
recognisedAt: new Date().toISOString(),
isPinned: false, isArchived: false,
});
});
```
Side-effect-Import in `data/seeds/index.ts`.
## App-Registry
In `packages/shared-branding/src/mana-apps.ts`:
```typescript
{
id: 'lasts',
name: 'Lasts',
description: 'Letzte Male — bewusst markiert oder rückwirkend erkannt',
category: 'reflection', // gleiche Kategorie wie firsts
requiredTier: 'guest', // LOCAL TIER PATCH bis Release, dann auf prod-tier setzen
icon: '…',
// …
}
```
## Tier-Strategie
Während Entwicklung: `'guest'` mit `// LOCAL TIER PATCH` Marker (Memory `project_tier_patch_resolved.md`). Vor Release auf prod-tier hochziehen — Vorschlag: `beta` analog zu firsts.
## Offene Fragen
1. **`losses` Cross-Link**: Soll ein Last → Loss eskaliert werden können (Button "Das war ein echter Verlust → in losses übernehmen")? Ja, aber `losses` existiert noch nicht als Modul. Hook-Point in DetailView vorbereiten, no-op bis losses gebaut ist.
2. **Audio-First-Eingabe**: Mic → STT → AI-strukturiert → Last-Draft? Würde gut zu Rubberduck/Scribe-Pattern passen. Erstmal nicht in M1-M6, aber DetailView so designen, dass `audioNoteId`-Eingabe leicht später ergänzbar ist (Feld existiert ja schon im Schema).
3. **Persona-Begleitung**: Soll ein dedizierter "Gefährte"-Agent (sanft, kontemplativ) für die Lasts-Begleitung gespawnt werden, oder reicht der Default-Mana-Agent? Vorschlag: in M5 prüfen, fürs Erste Default-Agent.
4. **`recognisedAt` vs. `createdAt`**: In 99% der Fälle gleich. Brauchen wir beide? Ja, weil bei AI-inferred Records der `createdAt` der Scan-Zeitpunkt ist und `recognisedAt` der "User-akzeptiert"-Zeitpunkt — relevant für Recognition-Reminder.
5. **i18n**: Direkt mit echten Keys bauen (nach Memory `project_i18n_hardening.md` ist hardcoded German verboten; validator wird sonst rot). Namespace: `lasts.*`.
## Kosten-Schätzung
| Milestone | Aufwand |
|---|---|
| M1 Refactor + Skelett | 1 Tag |
| M2 CRUD + Views | 1 Tag |
| M3 Inbox + Inference | 1 Tag |
| M4 AI-Tools | 0.5 Tage |
| M5 Push + Settings | 0.5 Tage |
| M6 Visibility + Sharing | 0.5 Tage |
| M7 Timeline (optional) | 0.5 Tage |
| **Total M1-M6** | **4 Tage** |
| **Total inkl. M7** | **4.5 Tage** |
Die ursprüngliche Schätzung "1-2 Tage für M1, +1 Tag Inferenz, +1 Tag Push" war zu knapp — vor allem M3 (Inferenz mit konservativen Schwellen + Cooldown-Mechanik) und der Refactor-Vorlauf in M1 brauchen jeweils ihren vollen Tag.