From 0f38a567fefc0097c39afa5ff705e7587082150e Mon Sep 17 00:00:00 2001 From: Till JS Date: Fri, 3 Apr 2026 20:44:20 +0200 Subject: [PATCH] feat(manacore/web): add automations module with UI for trigger rules New workbench module for creating and managing cross-module automations. Users can configure rules like "when calendar event with title containing 'Basketball' is created, log habit 'Basketball'". UI includes: source picker (app/collection/op), condition builder (field/op/value), action picker with dynamic params (e.g. habit select dropdown), toggle enable/disable, delete. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../apps/web/src/lib/app-registry/apps.ts | 9 + .../lib/modules/automations/ListView.svelte | 549 ++++++++++++++++++ .../lib/modules/automations/collections.ts | 8 + .../web/src/lib/modules/automations/index.ts | 4 + .../automations/stores/automations.svelte.ts | 72 +++ .../web/src/lib/modules/automations/types.ts | 121 ++++ .../apps/web/src/lib/splitscreen/registry.ts | 1 + 7 files changed, 764 insertions(+) create mode 100644 apps/manacore/apps/web/src/lib/modules/automations/ListView.svelte create mode 100644 apps/manacore/apps/web/src/lib/modules/automations/collections.ts create mode 100644 apps/manacore/apps/web/src/lib/modules/automations/index.ts create mode 100644 apps/manacore/apps/web/src/lib/modules/automations/stores/automations.svelte.ts create mode 100644 apps/manacore/apps/web/src/lib/modules/automations/types.ts diff --git a/apps/manacore/apps/web/src/lib/app-registry/apps.ts b/apps/manacore/apps/web/src/lib/app-registry/apps.ts index dfcbb6ab5..a697d7ecf 100644 --- a/apps/manacore/apps/web/src/lib/app-registry/apps.ts +++ b/apps/manacore/apps/web/src/lib/app-registry/apps.ts @@ -456,6 +456,15 @@ registerApp({ }, }); +registerApp({ + id: 'automations', + name: 'Automations', + color: '#8B5CF6', + views: { + list: { load: () => import('$lib/modules/automations/ListView.svelte') }, + }, +}); + registerApp({ id: 'playground', name: 'Playground', diff --git a/apps/manacore/apps/web/src/lib/modules/automations/ListView.svelte b/apps/manacore/apps/web/src/lib/modules/automations/ListView.svelte new file mode 100644 index 000000000..1445abca1 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/automations/ListView.svelte @@ -0,0 +1,549 @@ + + + +
+ +
+ Automations + {#if !showCreate} + + {/if} +
+ + + {#if showCreate} +
+ + + +
+ WENN + + + {#if selectedSource} +
+ +
+ {/if} +
+ + + {#if selectedSource} +
+ BEDINGUNG (optional) +
+ + {#if newConditionField} + + + {/if} +
+
+ {/if} + + +
+ DANN + + + {#if selectedAction} + {#each selectedAction.params as param} +
+ {param.label} + {#if param.key === 'habitId'} + + {:else} + { + newParams = { ...newParams, [param.key]: (e.target as HTMLInputElement).value }; + }} + /> + {/if} +
+ {/each} + {/if} +
+ + +
+ + +
+
+ {/if} + + +
+ {#each automations as auto (auto.id)} +
+ +
+ {auto.name} + + {sourceLabel(auto)} + + {conditionLabel(auto)} + + {actionLabel(auto)} + +
+ +
+ {/each} +
+ + {#if automations.length === 0 && !showCreate} +
+

Keine Automations angelegt.

+

Erstelle Regeln die Module miteinander verbinden.

+ +
+ {/if} +
+ + diff --git a/apps/manacore/apps/web/src/lib/modules/automations/collections.ts b/apps/manacore/apps/web/src/lib/modules/automations/collections.ts new file mode 100644 index 000000000..6075c91c8 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/automations/collections.ts @@ -0,0 +1,8 @@ +/** + * Automations module — collection accessors. + */ + +import { db } from '$lib/data/database'; +import type { LocalAutomation } from './types'; + +export const automationTable = db.table('automations'); diff --git a/apps/manacore/apps/web/src/lib/modules/automations/index.ts b/apps/manacore/apps/web/src/lib/modules/automations/index.ts new file mode 100644 index 000000000..b5d048127 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/automations/index.ts @@ -0,0 +1,4 @@ +export { automationsStore } from './stores/automations.svelte'; +export { automationTable } from './collections'; +export type { LocalAutomation, SourceOption, ActionOption } from './types'; +export { SOURCE_OPTIONS, ACTION_OPTIONS, CONDITION_OPS } from './types'; diff --git a/apps/manacore/apps/web/src/lib/modules/automations/stores/automations.svelte.ts b/apps/manacore/apps/web/src/lib/modules/automations/stores/automations.svelte.ts new file mode 100644 index 000000000..1ff3fa9ee --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/automations/stores/automations.svelte.ts @@ -0,0 +1,72 @@ +/** + * Automations Store — Mutation-Only + * + * CRUD for automation rules. After each mutation, reloads triggers. + */ + +import { automationTable } from '../collections'; +import { loadAutomations } from '$lib/triggers'; +import type { LocalAutomation } from '../types'; +import type { ConditionOp } from '$lib/triggers/conditions'; + +export const automationsStore = { + async create(data: { + name: string; + sourceApp: string; + sourceCollection: string; + sourceOp: 'insert' | 'update'; + conditionField?: string; + conditionOp?: ConditionOp; + conditionValue?: string; + targetApp: string; + targetAction: string; + targetParams?: Record; + }) { + const now = new Date().toISOString(); + const auto: LocalAutomation = { + id: crypto.randomUUID(), + name: data.name, + enabled: true, + sourceApp: data.sourceApp, + sourceCollection: data.sourceCollection, + sourceOp: data.sourceOp, + conditionField: data.conditionField, + conditionOp: data.conditionOp, + conditionValue: data.conditionValue, + targetApp: data.targetApp, + targetAction: data.targetAction, + targetParams: data.targetParams, + createdAt: now, + updatedAt: now, + }; + await automationTable.add(auto); + await loadAutomations(); + return auto; + }, + + async update(id: string, data: Partial) { + await automationTable.update(id, { + ...data, + updatedAt: new Date().toISOString(), + }); + await loadAutomations(); + }, + + async toggle(id: string) { + const auto = await automationTable.get(id); + if (!auto) return; + await automationTable.update(id, { + enabled: !auto.enabled, + updatedAt: new Date().toISOString(), + }); + await loadAutomations(); + }, + + async remove(id: string) { + await automationTable.update(id, { + deletedAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + await loadAutomations(); + }, +}; diff --git a/apps/manacore/apps/web/src/lib/modules/automations/types.ts b/apps/manacore/apps/web/src/lib/modules/automations/types.ts new file mode 100644 index 000000000..e9e8a65f1 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/automations/types.ts @@ -0,0 +1,121 @@ +/** + * Automations module types. + */ + +import type { BaseRecord } from '@manacore/local-store'; +import type { ConditionOp } from '$lib/triggers/conditions'; + +export interface LocalAutomation extends BaseRecord { + name: string; + enabled: boolean; + sourceApp: string; + sourceCollection: string; + sourceOp: 'insert' | 'update'; + conditionField?: string; + conditionOp?: ConditionOp; + conditionValue?: string; + targetApp: string; + targetAction: string; + targetParams?: Record; +} + +// ─── UI Metadata ───────────────────────────────────────── + +export interface SourceOption { + app: string; + appLabel: string; + collection: string; + collectionLabel: string; + fields: string[]; +} + +export interface ActionOption { + app: string; + appLabel: string; + action: string; + actionLabel: string; + params: { + key: string; + label: string; + type: 'text' | 'select'; + options?: { value: string; label: string }[]; + }[]; +} + +/** Available source apps/collections for trigger conditions. */ +export const SOURCE_OPTIONS: SourceOption[] = [ + { + app: 'calendar', + appLabel: 'Kalender', + collection: 'events', + collectionLabel: 'Events', + fields: ['title', 'description', 'location'], + }, + { + app: 'todo', + appLabel: 'Todo', + collection: 'tasks', + collectionLabel: 'Aufgaben', + fields: ['title', 'description'], + }, + { + app: 'contacts', + appLabel: 'Kontakte', + collection: 'contacts', + collectionLabel: 'Kontakte', + fields: ['firstName', 'lastName', 'company'], + }, + { + app: 'notes', + appLabel: 'Notes', + collection: 'notes', + collectionLabel: 'Notizen', + fields: ['title', 'content'], + }, + { + app: 'habits', + appLabel: 'Habits', + collection: 'habitLogs', + collectionLabel: 'Habit-Logs', + fields: ['habitId'], + }, + { + app: 'places', + appLabel: 'Places', + collection: 'places', + collectionLabel: 'Orte', + fields: ['name', 'address', 'category'], + }, +]; + +/** Available target actions. Params with type='select' get populated dynamically. */ +export const ACTION_OPTIONS: ActionOption[] = [ + { + app: 'habits', + appLabel: 'Habits', + action: 'logHabit', + actionLabel: 'Habit loggen', + params: [{ key: 'habitId', label: 'Habit', type: 'select' }], + }, + { + app: 'todo', + appLabel: 'Todo', + action: 'createTask', + actionLabel: 'Aufgabe erstellen', + params: [{ key: 'title', label: 'Titel', type: 'text' }], + }, + { + app: 'notes', + appLabel: 'Notes', + action: 'createNote', + actionLabel: 'Notiz erstellen', + params: [{ key: 'title', label: 'Titel', type: 'text' }], + }, +]; + +export const CONDITION_OPS: { value: ConditionOp; label: string }[] = [ + { value: 'contains', label: 'enthält' }, + { value: 'equals', label: 'ist gleich' }, + { value: 'startsWith', label: 'beginnt mit' }, + { value: 'matches', label: 'Regex' }, +]; diff --git a/apps/manacore/apps/web/src/lib/splitscreen/registry.ts b/apps/manacore/apps/web/src/lib/splitscreen/registry.ts index db56ea4b7..32270b192 100644 --- a/apps/manacore/apps/web/src/lib/splitscreen/registry.ts +++ b/apps/manacore/apps/web/src/lib/splitscreen/registry.ts @@ -31,6 +31,7 @@ const SPLIT_APP_ID_LIST = [ 'moodlit', 'memoro', 'places', + 'automations', 'playground', ] as const;