Mirror sibling to firsts: das *letzte* Mal, das du etwas getan hast — markiert oder rückwirkend erkannt. Plan: docs/plans/lasts-module.md. M1 Skelett — Dexie v51 lasts-Tabelle, Encryption-Registry, Per-Space- Welcome-Seed, Empty-State ListView. Kategorien aus firsts/types.ts nach \$lib/data/milestones/categories.ts extrahiert (Re-Exports halten firsts-API stabil). M2 CRUD + DetailView — StatusTabs (Vermutet/Bestätigt/Aufgehoben), Quick-Add mit Mode-Toggle, always-editable DetailView mit Lifecycle- Buttons (Bestätigen, Aufheben mit Inline-Note), 44 i18n-Keys × 5 Locales. M3 Inbox + Inferenz — Dexie v52 lastsCooldown (12-Monate-Cooldown, deterministische ID), Source-Registry-Pattern in inference/, places- Source mit Heuristik visitCount>=5 Span>=180d Silence>=365d. InboxView mit Akzeptieren/Verwerfen + manueller Scan. contacts/habits → M3.b sobald jeweilige Frequenz-Felder existieren. M4 AI-Tools — 5 Tools im AI_TOOL_CATALOG (create_last, confirm_last, reclaim_last, list_lasts, suggest_lasts), Webapp-Executor mit Vault- Locked-Handling. Server-Drift-Test 4/4, Schema-Test 6/6. M5 Reminders + Settings — Pivot zu In-App-DueBanner statt OS-Push (kein PWA-Push-System im Repo). Pure date-math (12 Vitest cases), Settings- Store mit 4 Toggles, DueBanner mit max-N rendering, Test-Banner-Knopf. M6 Visibility + Unlisted-Sharing — VisibilityPicker + SharedLinkControls in DetailView, buildLastBlob mit reflective-core whitelist (reclaimed Lasts gehärtet ausgeblockt), SharedLastView public-render, Share- Dispatcher kennt 'lasts'. M7 Meilensteine-Aggregator — Cross-modul firsts vereinigt mit lasts Timeline + Year-Recap. Pure aggregator (mergeMilestones, buildMilestonesRecap), 12 Vitest cases. /milestones und /milestones/recap/[year] Routes, Cross-Link in lasts/ListView. Validation: 0 errors / 0 warnings (svelte-check 7645 files), 24/24 tests, i18n-parity 39x5 aligned (+2 namespaces), i18n-keys baseline- equal, crypto 211 tables. LOCAL TIER PATCH: lasts ist 'guest' für Testing — vor Release auf 'beta' setzen (packages/shared-branding/src/mana-apps.ts). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
34 KiB
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 | confirmedconfirm_last(propose) — suspected → confirmed mit Reflexion (date, meaning, whatIKnewThen, whatIKnowNow, tenderness 1-5, wouldReclaim no/maybe/yes)reclaim_last(propose) — confirmed → reclaimed mit optionaler Notelist_lasts(auto) — gefiltert nach status + category, max 100suggest_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: <AiProposalInbox module="lasts" /> 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 <AiProposalInbox module="lasts" /> 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 mitdateheute vor X Jahren),findRecognitionAnniversaryLasts(any status mitrecognisedAtheute vor X Jahren). 12 Vitest-Cases, alle grün. - Settings-Store (
stores/settings.svelte.ts) viacreateAppSettingsStore('lasts-settings', …): 4 persistent localStorage-Flags —anniversaryReminders,recognitionReminders,inboxNotify,bannerMaxItems(Default 3). Modul-Pattern analogtodoSettings/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ürbannerMaxItems+ "Zurücksetzen" + "Test-Banner zeigen" (rendert 4 Sek Beispiel) + Footnote zur fehlenden OS-Push-Infrastruktur. - ListView-Wiring:
<DueBanner {lasts} {inboxCount} />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:
LocalLastundLasthaben jetztvisibility,visibilityChangedAt,visibilityChangedBy,unlistedToken,unlistedExpiresAt.toLastsetzt 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:
buildLastBlobindata/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.sveltekennt jetztdata.collection === 'lasts'. - DetailView-Wire:
<VisibilityPicker>+<SharedLinkControls>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/<token>, 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. PlusfilterByDirection,filterByYear,compareTimelineDesc. Reactive HookuseMilestonesTimeline()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
/milestonesund/milestones/recap/[year](nutzenRoutePagemitappId="milestones"— Registry hat keinen Eintrag, fallback rendert sauber mit Title-Override). - Cross-Link in
lasts/ListView.svelteTab-Bar: "Meilensteine"-Link führt zu/milestones. - i18n-Namespace
milestones/× 5 Locales mittimeline.*,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/ (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:
- Manuell — der User markiert bewusst (selten, oft an Wendepunkten: letzter Tag im Job, letztes Konzert mit X, letzte Nacht in der alten Wohnung).
- 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
lossesaus 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 → confirmedvs.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.lastsist 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.lastssind 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)
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):
// ─── 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:
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-Listecategory— Kategorie-Filterdate— Sort + Anniversary-ScansrecognisedAt— "vor X Jahren erkannt"-ReminderisPinned,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
contactsmitrelationship: 'family' | 'partner'ohne explizite Opt-In — Trauer-Trigger. - Keine Vorschläge für Refs in
period,dreams,losses,regret/forgive— zu intim. - Wenn
losseseinen Eintrag mit gleicherpersonIdhat, 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):
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:
- Anniversary-Reminder — "Heute vor X Jahren das letzte Mal …" (nur für
confirmedmitdate). - Recognition-Reminder — "Vor X Jahren als Last erkannt: …" (nutzt
recognisedAt). - 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:
- Extract Categories:
CATEGORY_LABELS,CATEGORY_COLORSausfirsts/types.tsraus →data/milestones/categories.ts.firsts/types.tsre-exportiert für Abwärtskompatibilität. - Extract MilestoneCard: aus
firsts/ListView.sveltedie Listen-Item-Markup-Logik extrahieren incomponents/milestones/MilestoneCard.svelte.ListView.svelterendert dann<MilestoneCard direction="first">. - Optional jetzt, sicher später: ReflectionFields, LifecycleToggle, CategoryPill, PeoplePlaceMediaPicker analog extrahieren. Nicht im kritischen Pfad — kann passieren, wenn
lastssie 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.tsextrahieren,firsts/types.tsre-exportiertcomponents/milestones/MilestoneCard.svelteextrahieren,firsts/ListView.svelteumstellenlasts/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.sveltemountet 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/deletequeries.ts: useAllLasts, useLastsByStatus, useLast(id)ListView.svelte: StatusTabs (Suspected | Confirmed | Reclaimed), MilestoneCard-basierte ListeDetailView.svelte+ Route/lasts/[id]/+page.svelte: Reflexionsfelder, Lifecycle-Buttons- Editor-Component (kann inline oder modal): nutzt geteilten
MilestoneEditormit 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 mitinferredFrom != null, Akzeptieren/VerwerfensuggestLasts()-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
lastsInferenceCooldownoder 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.tsmit 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
<AiProposalInbox module="lasts" />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.sveltemit drei opt-in-Toggles- Anniversary-Scan (Cron + match auf
datemit 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
lastsregistrieren (analogevents/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 ∪ lastsroutes/(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:
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:
{
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
lossesCross-Link: Soll ein Last → Loss eskaliert werden können (Button "Das war ein echter Verlust → in losses übernehmen")? Ja, aberlossesexistiert noch nicht als Modul. Hook-Point in DetailView vorbereiten, no-op bis losses gebaut ist.- 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). - 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.
recognisedAtvs.createdAt: In 99% der Fälle gleich. Brauchen wir beide? Ja, weil bei AI-inferred Records dercreatedAtder Scan-Zeitpunkt ist undrecognisedAtder "User-akzeptiert"-Zeitpunkt — relevant für Recognition-Reminder.- i18n: Direkt mit echten Keys bauen (nach Memory
project_i18n_hardening.mdist 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.