diff --git a/apps/mana/apps/web/src/lib/data/events/catalog.ts b/apps/mana/apps/web/src/lib/data/events/catalog.ts index 40a6f8498..8a66fc34a 100644 --- a/apps/mana/apps/web/src/lib/data/events/catalog.ts +++ b/apps/mana/apps/web/src/lib/data/events/catalog.ts @@ -442,6 +442,87 @@ export interface SocialEventDeletedPayload { export type SocialEventsEventType = 'SocialEventCreated' | 'SocialEventDeleted'; +// ── Cycles ────────────────────────────────────────── + +export interface CycleDayLoggedPayload { + logId: string; + date: string; + flow?: string; +} +export type CyclesEventType = 'CycleDayLogged'; + +// ── Firsts ────────────────────────────────────────── + +export interface FirstCreatedPayload { + firstId: string; + title: string; + isLived: boolean; +} +export type FirstsEventType = 'FirstCreated'; + +// ── Guides ────────────────────────────────────────── + +export interface GuideCreatedPayload { + guideId: string; + title: string; +} +export type GuidesEventType = 'GuideCreated'; + +// ── Inventory ─────────────────────────────────────── + +export interface InventoryItemCreatedPayload { + itemId: string; + name: string; + category?: string; +} +export type InventoryEventType = 'InventoryItemCreated'; + +// ── Photos ────────────────────────────────────────── + +export interface PhotoDeletedPayload { + photoId: string; +} +export type PhotosEventType = 'PhotoDeleted'; + +// ── Plants ────────────────────────────────────────── + +export interface PlantCreatedPayload { + plantId: string; + name: string; + species?: string; +} +export interface PlantDeletedPayload { + plantId: string; +} +export type PlantsEventType = 'PlantCreated' | 'PlantDeleted'; + +// ── News ──────────────────────────────────────────── + +export interface ArticleSavedPayload { + articleId: string; + title: string; +} +export type NewsEventType = 'ArticleSaved'; + +// ── Recipes ───────────────────────────────────────── + +export interface RecipeCreatedPayload { + recipeId: string; + title: string; +} +export interface RecipeDeletedPayload { + recipeId: string; +} +export type RecipesEventType = 'RecipeCreated' | 'RecipeDeleted'; + +// ── Questions ─────────────────────────────────────── + +export interface QuestionAskedPayload { + questionId: string; + question: string; +} +export type QuestionsEventType = 'QuestionAsked'; + // ── Body ──────────────────────────────────────────── export interface WorkoutStartedPayload { @@ -525,6 +606,15 @@ export type ManaEventType = | ChatEventType | MemoroEventType | SkilltreeEventType + | CyclesEventType + | FirstsEventType + | GuidesEventType + | InventoryEventType + | PhotosEventType + | PlantsEventType + | NewsEventType + | RecipesEventType + | QuestionsEventType | SocialEventsEventType | BodyEventType | SystemEventType; @@ -606,6 +696,26 @@ export type ManaEvent = // Skilltree | DomainEvent<'SkillXpAdded', SkillXpAddedPayload> | DomainEvent<'SkillCreated', SkillCreatedPayload> + // Cycles + | DomainEvent<'CycleDayLogged', CycleDayLoggedPayload> + // Firsts + | DomainEvent<'FirstCreated', FirstCreatedPayload> + // Guides + | DomainEvent<'GuideCreated', GuideCreatedPayload> + // Inventory + | DomainEvent<'InventoryItemCreated', InventoryItemCreatedPayload> + // Photos + | DomainEvent<'PhotoDeleted', PhotoDeletedPayload> + // Plants + | DomainEvent<'PlantCreated', PlantCreatedPayload> + | DomainEvent<'PlantDeleted', PlantDeletedPayload> + // News + | DomainEvent<'ArticleSaved', ArticleSavedPayload> + // Recipes + | DomainEvent<'RecipeCreated', RecipeCreatedPayload> + | DomainEvent<'RecipeDeleted', RecipeDeletedPayload> + // Questions + | DomainEvent<'QuestionAsked', QuestionAskedPayload> // Social Events | DomainEvent<'SocialEventCreated', SocialEventCreatedPayload> | DomainEvent<'SocialEventDeleted', SocialEventDeletedPayload> diff --git a/apps/mana/apps/web/src/lib/data/tools/init.ts b/apps/mana/apps/web/src/lib/data/tools/init.ts index 1f55683bf..eb27c5760 100644 --- a/apps/mana/apps/web/src/lib/data/tools/init.ts +++ b/apps/mana/apps/web/src/lib/data/tools/init.ts @@ -24,6 +24,14 @@ import { storageTools } from '$lib/modules/storage/tools'; import { chatTools } from '$lib/modules/chat/tools'; import { memoroTools } from '$lib/modules/memoro/tools'; import { skilltreeTools } from '$lib/modules/skilltree/tools'; +import { cyclesTools } from '$lib/modules/cycles/tools'; +import { firstsTools } from '$lib/modules/firsts/tools'; +import { guidesTools } from '$lib/modules/guides/tools'; +import { inventoryTools } from '$lib/modules/inventory/tools'; +import { plantsTools } from '$lib/modules/plants/tools'; +import { newsTools } from '$lib/modules/news/tools'; +import { recipesTools } from '$lib/modules/recipes/tools'; +import { questionsTools } from '$lib/modules/questions/tools'; let initialized = false; @@ -49,5 +57,13 @@ export function initTools(): void { registerTools(chatTools); registerTools(memoroTools); registerTools(skilltreeTools); + registerTools(cyclesTools); + registerTools(firstsTools); + registerTools(guidesTools); + registerTools(inventoryTools); + registerTools(plantsTools); + registerTools(newsTools); + registerTools(recipesTools); + registerTools(questionsTools); initialized = true; } diff --git a/apps/mana/apps/web/src/lib/modules/cycles/stores/cycles.svelte.ts b/apps/mana/apps/web/src/lib/modules/cycles/stores/cycles.svelte.ts index dc844daf5..ad59ddca5 100644 --- a/apps/mana/apps/web/src/lib/modules/cycles/stores/cycles.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/cycles/stores/cycles.svelte.ts @@ -6,6 +6,7 @@ import { cycleTable } from '../collections'; import { toCycle } from '../queries'; import { daysBetween } from '../utils/phase'; import { encryptRecord } from '$lib/data/crypto'; +import { emitDomainEvent } from '$lib/data/events'; import { createBlock, updateBlock, deleteBlock } from '$lib/data/time-blocks/service'; import type { LocalCycle } from '../types'; @@ -68,6 +69,11 @@ export const cyclesStore = { const plaintextSnapshot = toCycle(newLocal); await encryptRecord('cycles', newLocal); await cycleTable.add(newLocal); + emitDomainEvent('CycleDayLogged', 'cycles', 'cycleDayLogs', newLocal.id, { + logId: newLocal.id, + date: newLocal.startDate, + flow: null, + }); return plaintextSnapshot; }, diff --git a/apps/mana/apps/web/src/lib/modules/cycles/tools.ts b/apps/mana/apps/web/src/lib/modules/cycles/tools.ts new file mode 100644 index 000000000..a672c4047 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/cycles/tools.ts @@ -0,0 +1,22 @@ +import type { ModuleTool } from '$lib/data/tools/types'; +export const cyclesTools: ModuleTool[] = [ + { + name: 'log_cycle_day', + module: 'cycles', + description: 'Loggt einen Zyklus-Tag (Menstruationszyklus)', + parameters: [ + { + name: 'flow', + type: 'string', + description: 'Staerke', + required: false, + enum: ['light', 'medium', 'heavy', 'spotting', 'none'], + }, + ], + async execute(params) { + const { cyclesStore } = await import('./stores/cycles.svelte'); + const entry = await cyclesStore.createCycle({}); + return { success: true, data: entry, message: 'Zyklus-Tag geloggt' }; + }, + }, +]; diff --git a/apps/mana/apps/web/src/lib/modules/firsts/stores/firsts.svelte.ts b/apps/mana/apps/web/src/lib/modules/firsts/stores/firsts.svelte.ts index eed653c31..e1c115016 100644 --- a/apps/mana/apps/web/src/lib/modules/firsts/stores/firsts.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/firsts/stores/firsts.svelte.ts @@ -1,6 +1,7 @@ import { firstTable } from '../collections'; import { toFirst } from '../queries'; import { encryptRecord } from '$lib/data/crypto'; +import { emitDomainEvent } from '$lib/data/events'; import type { First, FirstCategory, @@ -49,6 +50,11 @@ export const firstsStore = { const plaintextSnapshot = toFirst(newLocal); await encryptRecord('firsts', newLocal); await firstTable.add(newLocal); + emitDomainEvent('FirstCreated', 'firsts', 'firsts', newLocal.id, { + firstId: newLocal.id, + title: data.title ?? '', + isLived: false, + }); return plaintextSnapshot; }, diff --git a/apps/mana/apps/web/src/lib/modules/firsts/tools.ts b/apps/mana/apps/web/src/lib/modules/firsts/tools.ts new file mode 100644 index 000000000..7299a7a52 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/firsts/tools.ts @@ -0,0 +1,14 @@ +import type { ModuleTool } from '$lib/data/tools/types'; +export const firstsTools: ModuleTool[] = [ + { + name: 'create_first', + module: 'firsts', + description: 'Erstellt ein "Erstes Mal" (Bucket-List-Traum oder erlebtes Ersterlebnis)', + parameters: [{ name: 'title', type: 'string', description: 'Was', required: true }], + async execute(params) { + const { firstsStore } = await import('./stores/firsts.svelte'); + const first = await firstsStore.createDream({ title: params.title as string }); + return { success: true, data: first, message: `Erstes Mal: "${params.title}"` }; + }, + }, +]; diff --git a/apps/mana/apps/web/src/lib/modules/guides/stores/guides.svelte.ts b/apps/mana/apps/web/src/lib/modules/guides/stores/guides.svelte.ts index 977c57670..c41bfcf7a 100644 --- a/apps/mana/apps/web/src/lib/modules/guides/stores/guides.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/guides/stores/guides.svelte.ts @@ -9,6 +9,7 @@ import { guideTable, sectionTable, stepTable, runTable } from '../collections'; import { toGuide, toSection, toStep, toRun } from '../queries'; import { encryptRecord, decryptRecord } from '$lib/data/crypto'; +import { emitDomainEvent } from '$lib/data/events'; import { createBlock, updateBlock, deleteBlock } from '$lib/data/time-blocks/service'; import type { LocalGuide, @@ -48,6 +49,10 @@ export const guidesStore = { const snapshot = toGuide({ ...newLocal }); await encryptRecord('guides', newLocal); await guideTable.add(newLocal); + emitDomainEvent('GuideCreated', 'guides', 'guides', newLocal.id, { + guideId: newLocal.id, + title: dto.title ?? '', + }); return snapshot; }, diff --git a/apps/mana/apps/web/src/lib/modules/guides/tools.ts b/apps/mana/apps/web/src/lib/modules/guides/tools.ts new file mode 100644 index 000000000..7fcd5986a --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/guides/tools.ts @@ -0,0 +1,16 @@ +import type { ModuleTool } from '$lib/data/tools/types'; +export const guidesTools: ModuleTool[] = [ + { + name: 'create_guide', + module: 'guides', + description: 'Erstellt eine neue Anleitung / Guide', + parameters: [ + { name: 'title', type: 'string', description: 'Titel der Anleitung', required: true }, + ], + async execute(params) { + const { guidesStore } = await import('./stores/guides.svelte'); + const guide = await guidesStore.createGuide({ title: params.title as string }); + return { success: true, data: guide, message: `Guide "${params.title}" erstellt` }; + }, + }, +]; diff --git a/apps/mana/apps/web/src/lib/modules/inventory/stores/items.svelte.ts b/apps/mana/apps/web/src/lib/modules/inventory/stores/items.svelte.ts index 0ede4f578..cbfa1f745 100644 --- a/apps/mana/apps/web/src/lib/modules/inventory/stores/items.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/inventory/stores/items.svelte.ts @@ -11,6 +11,7 @@ import type { LocalItem } from '../types'; import type { ItemStatus } from '../queries'; import { InventoryEvents } from '@mana/shared-utils/analytics'; import { encryptRecord } from '$lib/data/crypto'; +import { emitDomainEvent } from '$lib/data/events'; export const itemsStore = { async create(data: { @@ -49,6 +50,11 @@ export const itemsStore = { const plaintextSnapshot = toItem(newLocal); await encryptRecord('invItems', newLocal); await invItemTable.add(newLocal); + emitDomainEvent('InventoryItemCreated', 'inventory', 'inventoryItems', newLocal.id, { + itemId: newLocal.id, + name: data.name ?? '', + category: data.categoryId, + }); InventoryEvents.itemCreated(); return plaintextSnapshot; }, diff --git a/apps/mana/apps/web/src/lib/modules/inventory/tools.ts b/apps/mana/apps/web/src/lib/modules/inventory/tools.ts new file mode 100644 index 000000000..0d3d85b85 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/inventory/tools.ts @@ -0,0 +1,20 @@ +import type { ModuleTool } from '$lib/data/tools/types'; +export const inventoryTools: ModuleTool[] = [ + { + name: 'create_inventory_item', + module: 'inventory', + description: 'Erfasst einen Gegenstand im Inventar', + parameters: [ + { name: 'name', type: 'string', description: 'Name', required: true }, + { name: 'collectionId', type: 'string', description: 'Sammlungs-ID', required: true }, + ], + async execute(params) { + const { itemsStore } = await import('./stores/items.svelte'); + const item = await itemsStore.create({ + name: params.name as string, + collectionId: params.collectionId as string, + }); + return { success: true, data: item, message: `"${params.name}" im Inventar erfasst` }; + }, + }, +]; diff --git a/apps/mana/apps/web/src/lib/modules/news/stores/articles.svelte.ts b/apps/mana/apps/web/src/lib/modules/news/stores/articles.svelte.ts index 914bf5e18..fdfba8ee5 100644 --- a/apps/mana/apps/web/src/lib/modules/news/stores/articles.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/news/stores/articles.svelte.ts @@ -13,6 +13,7 @@ */ import { encryptRecord } from '$lib/data/crypto'; +import { emitDomainEvent } from '$lib/data/events'; import { articleTable } from '../collections'; import { extractFromUrl } from '../api'; import { toArticle } from '../queries'; @@ -50,6 +51,10 @@ export const articlesStore = { const snapshot = toArticle(newLocal); await encryptRecord('newsArticles', newLocal); await articleTable.add(newLocal); + emitDomainEvent('ArticleSaved', 'news', 'newsArticles', newLocal.id, { + articleId: newLocal.id, + title: input.title ?? '', + }); return snapshot; }, diff --git a/apps/mana/apps/web/src/lib/modules/news/tools.ts b/apps/mana/apps/web/src/lib/modules/news/tools.ts new file mode 100644 index 000000000..64a1a067c --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/news/tools.ts @@ -0,0 +1,4 @@ +import type { ModuleTool } from '$lib/data/tools/types'; +// News tools are limited — saveFromCurated requires a full LocalCachedArticle +// which is complex for LLM tool calling. Read-only for now. +export const newsTools: ModuleTool[] = []; diff --git a/apps/mana/apps/web/src/lib/modules/photos/stores/photos.svelte.ts b/apps/mana/apps/web/src/lib/modules/photos/stores/photos.svelte.ts index 18cd91ac5..002a7d297 100644 --- a/apps/mana/apps/web/src/lib/modules/photos/stores/photos.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/photos/stores/photos.svelte.ts @@ -7,6 +7,7 @@ import { PhotosEvents } from '@mana/shared-utils/analytics'; import { db } from '$lib/data/database'; +import { emitDomainEvent } from '$lib/data/events'; import type { LocalFavorite, Photo, PhotoFilters, PhotoStats } from '../types'; const MEDIA_URL = () => @@ -181,6 +182,7 @@ export const photoStore = { photos = photos.filter((p) => p.id !== mediaId); if (selectedPhoto?.id === mediaId) selectedPhoto = null; + emitDomainEvent('PhotoDeleted', 'photos', 'photos', mediaId, { photoId: mediaId }); PhotosEvents.photoDeleted(); return true; } catch (e) { diff --git a/apps/mana/apps/web/src/lib/modules/plants/mutations.ts b/apps/mana/apps/web/src/lib/modules/plants/mutations.ts index 451caeb13..685c5b792 100644 --- a/apps/mana/apps/web/src/lib/modules/plants/mutations.ts +++ b/apps/mana/apps/web/src/lib/modules/plants/mutations.ts @@ -9,6 +9,7 @@ import { db } from '$lib/data/database'; import { toPlant, toWateringSchedule } from './queries'; import { PlantsEvents } from '@mana/shared-utils/analytics'; import { encryptRecord, decryptRecord } from '$lib/data/crypto'; +import { emitDomainEvent } from '$lib/data/events'; import { createBlock } from '$lib/data/time-blocks/service'; import { uploadPlantPhoto, identifyPlant, type IdentifyResult } from './api'; import type { @@ -45,6 +46,11 @@ export const plantMutations = { const plaintextSnapshot = toPlant(newLocal); await encryptRecord('plants', newLocal); await db.table('plants').add(newLocal); + emitDomainEvent('PlantCreated', 'plants', 'plants', newLocal.id, { + plantId: newLocal.id, + name: dto.name, + species: dto.scientificName, + }); PlantsEvents.plantCreated(); return plaintextSnapshot; }, @@ -77,6 +83,7 @@ export const plantMutations = { deletedAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }); + emitDomainEvent('PlantDeleted', 'plants', 'plants', id, { plantId: id }); PlantsEvents.plantDeleted(); }, diff --git a/apps/mana/apps/web/src/lib/modules/plants/tools.ts b/apps/mana/apps/web/src/lib/modules/plants/tools.ts new file mode 100644 index 000000000..5bc4a8d78 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/plants/tools.ts @@ -0,0 +1,14 @@ +import type { ModuleTool } from '$lib/data/tools/types'; +export const plantsTools: ModuleTool[] = [ + { + name: 'create_plant', + module: 'plants', + description: 'Erfasst eine neue Pflanze', + parameters: [{ name: 'name', type: 'string', description: 'Name der Pflanze', required: true }], + async execute(params) { + const { plantMutations } = await import('./mutations'); + const plant = await plantMutations.create({ name: params.name as string }); + return { success: true, data: plant, message: `Pflanze "${params.name}" erstellt` }; + }, + }, +]; diff --git a/apps/mana/apps/web/src/lib/modules/questions/stores/answers.svelte.ts b/apps/mana/apps/web/src/lib/modules/questions/stores/answers.svelte.ts index 6ec91fd19..e037eb216 100644 --- a/apps/mana/apps/web/src/lib/modules/questions/stores/answers.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/questions/stores/answers.svelte.ts @@ -21,6 +21,7 @@ import { db } from '$lib/data/database'; import { encryptRecord, decryptRecord } from '$lib/data/crypto'; +import { emitDomainEvent } from '$lib/data/events'; import { researchApi, type ResearchEvent, type ResearchSource } from '$lib/api/research'; import type { LocalAnswer, LocalQuestion } from '../types'; @@ -47,6 +48,10 @@ async function createManual(input: CreateManualAnswerInput): Promise { }; await encryptRecord('answers', row); await db.table('answers').add(row); + emitDomainEvent('QuestionAsked', 'questions', 'questions', id, { + questionId: id, + question: input.content ?? '', + }); return id; } diff --git a/apps/mana/apps/web/src/lib/modules/questions/tools.ts b/apps/mana/apps/web/src/lib/modules/questions/tools.ts new file mode 100644 index 000000000..39a8f87f0 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/questions/tools.ts @@ -0,0 +1,3 @@ +import type { ModuleTool } from '$lib/data/tools/types'; +// Questions module requires a questionId for answers — no simple create tool yet. +export const questionsTools: ModuleTool[] = []; diff --git a/apps/mana/apps/web/src/lib/modules/recipes/stores/recipes.svelte.ts b/apps/mana/apps/web/src/lib/modules/recipes/stores/recipes.svelte.ts index 2f5a8b276..907de7f94 100644 --- a/apps/mana/apps/web/src/lib/modules/recipes/stores/recipes.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/recipes/stores/recipes.svelte.ts @@ -5,6 +5,7 @@ */ import { encryptRecord } from '$lib/data/crypto'; +import { emitDomainEvent } from '$lib/data/events'; import { recipeTable } from '../collections'; import { toRecipe } from '../queries'; import type { LocalRecipe, Difficulty, Ingredient } from '../types'; @@ -40,6 +41,10 @@ export const recipesStore = { const snapshot = toRecipe({ ...newLocal }); await encryptRecord('recipes', newLocal); await recipeTable.add(newLocal); + emitDomainEvent('RecipeCreated', 'recipes', 'recipes', newLocal.id, { + recipeId: newLocal.id, + title: input.title ?? '', + }); return snapshot; }, @@ -76,6 +81,7 @@ export const recipesStore = { deletedAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }); + emitDomainEvent('RecipeDeleted', 'recipes', 'recipes', id, { recipeId: id }); }, async toggleFavorite(id: string) { diff --git a/apps/mana/apps/web/src/lib/modules/recipes/tools.ts b/apps/mana/apps/web/src/lib/modules/recipes/tools.ts new file mode 100644 index 000000000..527134c5e --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/recipes/tools.ts @@ -0,0 +1,20 @@ +import type { ModuleTool } from '$lib/data/tools/types'; +export const recipesTools: ModuleTool[] = [ + { + name: 'create_recipe', + module: 'recipes', + description: 'Erstellt ein neues Rezept', + parameters: [ + { name: 'title', type: 'string', description: 'Name des Rezepts', required: true }, + { name: 'description', type: 'string', description: 'Beschreibung', required: false }, + ], + async execute(params) { + const { recipesStore } = await import('./stores/recipes.svelte'); + const recipe = await recipesStore.createRecipe({ + title: params.title as string, + description: params.description as string | undefined, + }); + return { success: true, data: recipe, message: `Rezept "${params.title}" erstellt` }; + }, + }, +];