7.7 KiB
Feature: Aufgaben (Tasks)
Zweck
Strukturierte Aufgaben, die direkt aus Markdown-Text entstehen — kein separates Task-Formular nötig. Tasks sind echte Datenobjekte (DB-basiert, sync-fähig) und können Personen zugewiesen werden.
Syntax im Markdown-Editor
| Syntax | Bedeutung |
|---|---|
[] Aufgabe erledigen |
Unverknüpfte Task-Markierung (Chip, noch kein Task-Objekt) |
[T:abc123] |
Verknüpfte Aufgabe (ID referenziert Task-Objekt in DB) |
[]muss am Zeilenanfang stehen (führende Leerzeichen erlaubt)- Inline-Tasks (z.B. mitten in einem Satz) werden nicht unterstützt
Shortcut
Ctrl+9 im Markdown-Editor:
- Wenn Zeile beginnt mit
[]→[]wird entfernt - Wenn Zeile beginnt mit
[T:...]→ kein Toggle (Task existiert in DB) - Sonst →
[]wird am Zeilenanfang eingefügt
Workflow
[] Aufgabe tippen→ wird als grauer Chip[ ]gerendert- Chip anklicken → TaskCreateModal öffnet sich mit vorausgefüllten Feldern:
- Titel (aus der Zeile extrahiert, ohne
[], Mentions, Pfeile) - Assignee (aus
-> ABBRoder@NAMEin der Zeile) - Fälligkeitsdatum (optional)
- Titel (aus der Zeile extrahiert, ohne
- Speichern → Text wird zu
[T:{id}] Aufgabe tippengeändert - Chip
[T:...]anklicken → Popup: Erledigt/Offen togglen
Felder
| Feld | Typ | Beschreibung |
|---|---|---|
title |
string | Titel der Aufgabe |
contextId |
string | Zugehöriger Kontext (von Topic des Quell-Eintrags) |
historyEntryId |
string | null | Quell-HistoryEntry (null bei externen Tasks) |
assignee |
string | null | Vollständiger Personenname (nie Abkürzung) |
status |
open | done |
Status |
completedAt |
string | null | ISO-Datum des Abschlusses |
dueDate |
string | null | Fälligkeitsdatum (YYYY-MM-DD) |
source |
local | email | api |
Herkunft |
externalId |
string | null | ID im Quellsystem (für externe Quellen) |
externalUrl |
string | null | Deep-Link ins Quellsystem |
Wo Tasks sichtbar sind
- AgendaView: Eigener Abschnitt "Aufgaben" unterhalb der Topics (pro Kontext, ungefiltert)
- Personen-Tab "Aufgaben": Alle Tasks zugewiesen an diese Person (kontextübergreifend)
- Chip im Text: Visuelles Feedback (grau = offen, grün/durchgestrichen = erledigt)
Chip-Visualisierung
- Unverknüpft
[ ]: grauer Chip, Klick öffnet Modal - Offen
[ ]: grauer Chip - Erledigt
[✓]: grüner Chip mit Strikethrough - Nicht gefunden (gelöschter Task): ausgegraut
Extensibility: Externe Quellen
Das source-Feld und externalId/externalUrl bereiten künftige Integrationen vor:
source: 'email'— geplant: geflagte E-Mails via Graph-API → Task;contextIdbeim Import Pflichtsource: 'api'— geplant: POST/api/tasks/importvon Drittsystemen;contextIdim Payload Pflicht- Sync läuft über denselben Endpunkt wie lokale Tasks — externe Quellen pushen einfach in dieselbe
tasks-Tabelle - Index auf
(userId, source, externalId)für spätere Deduplizierung vorhanden
Bekannte Einschränkungen
[T:...]-Chips können nicht per Shortcut entfernt werden (Task existiert in DB)- Assignee-Auflösung von
-> ABBRzu vollem Namen: der extrahierte Wert wird direkt verwendet (keine automatische Abkürzungsauflösung im Modal-Prefill) - Tasks im AgendaView zeigen alle Tasks des Kontexts (kein Status-Filter)
Implementierungshinweise (für Wartung)
Architektur: Wo HistoryEntries auftauchen
HistoryEntries sind topic-scoped, nicht context-scoped. Ein HistoryEntry gehört zu einem Topic (entry.topicId), das Topic gehört zu einem Kontext (topic.contextId). Daraus folgt:
HistoryEntry.text
→ gerendert in RenderedMarkdown
→ Task-Chip-Klick braucht entry.id + entry.topicId
→ ContextPage löst daraus contextId auf
Der nicht-offensichtliche Fall: EventCard
EventCard zeigt einen Termin (Kontext vom Typ meeting), nicht direkt einen HistoryEntry. Die Meeting-Notizen sind aber intern als HistoryEntry gespeichert — unter dem speziellen Topic notesTopicId(event.id). Dieser Mechanismus ist im EventCard-Code versteckt (liveQuery auf notesTopicId). Wer EventCard nur von außen sieht ("das ist eine Meeting-Karte"), erwartet dort keinen HistoryEntry-Kontext — und vergisst deshalb, historyEntryId zu übergeben.
Merksatz: Überall wo entry.text oder ein abgeleiteter Text (title/body-Split) gerendert wird, muss entry.id als historyEntryId mitgegeben werden — egal wie die Komponente heißt.
historyEntryId muss überall explizit übergeben werden
RenderedMarkdown wird an ~10 Stellen instanziiert. Ohne historyEntryId-Prop öffnet ein Task-Chip-Klick das Modal mit historyEntryId: null — der Task wird angelegt, aber der Quelltext ([] … → [T:id]) wird nicht aktualisiert. Der Fehler ist stumm: kein Fehler, kein visuelles Feedback, nur der Chip bleibt [ ].
Vollständige Instanz-Übersicht:
| Datei | Quelle | historyEntryId | Notiz |
|---|---|---|---|
HistoryItem.svelte |
entry |
✓ | |
JournalView.svelte:626/640 |
entry (daily-log) |
✓ | |
JournalView.svelte:674 |
entry (Meeting-Scope) |
✓ | |
EventCard.svelte |
notesEntry (versteckt) |
✓ | notesEntry = erster Entry aus notesTopicId(event.id) |
DashboardView.svelte:539/541 |
entry |
✗ | read-only Dashboard, kein Task-Flow vorgesehen |
DashboardView.svelte:569 |
entry |
✗ | read-only Dashboard |
ArchivedView.svelte |
entry |
✗ | archivierte Einträge, kein Task-Flow vorgesehen |
WiedervorlageCard.svelte |
entry (body-Split) |
✗ | Wiedervorlage-Kontext, kein Task-Flow vorgesehen |
PersonsView.svelte |
task.text |
— | kein HistoryEntry |
EditableMarkdown.svelte |
Topic-Body | — | kein HistoryEntry, topicId wäre theoretisch möglich |
Wenn zukünftig Task-Chips auch in Dashboard/Archiv/Wiedervorlage aktiv werden sollen, müssen dort historyEntryId und topicId ergänzt werden.
Debugging-Anleitung
Symptom: Chip klickbar, Modal öffnet, aber Text bleibt [] … (kein [T:id]).
Diagnose im Browser-Devtools-Konsole:
document.querySelector('[data-task-new]').closest('.markdown-content').parentElement.className
→ Klasse identifizieren → zugehörige Komponente finden → prüfen ob historyEntryId Prop gesetzt ist.
Empfohlenes Refactoring: HistoryEntryText-Komponente
Die Wurzel des Problems ist, dass RenderedMarkdown ein generisches Render-Primitive ist, aber der HistoryEntry-Kontext implizit mitgegeben werden muss. Eine dedizierte Wrapper-Komponente würde das explizit machen:
<!-- HistoryEntryText.svelte -->
<script lang="ts">
import RenderedMarkdown from './RenderedMarkdown.svelte';
import type { HistoryEntry } from '@ka-note/shared';
interface Props { entry: HistoryEntry; class?: string; textOverride?: string; }
let { entry, class: className = '', textOverride }: Props = $props();
</script>
<RenderedMarkdown
text={textOverride ?? entry.text}
{class}
historyEntryId={entry.id}
topicId={entry.topicId}
/>
Verwendung dann z.B.:
<HistoryEntryText {entry} /> <!-- statt RenderedMarkdown text={entry.text} -->
<HistoryEntryText {entry} textOverride={body} class="text-sm" /> <!-- body-Split-Fall -->
Vorteile:
historyEntryIdkann nicht mehr vergessen werden — es ist Pflicht-Prop (TypeScript-Fehler sonst)- Neue Views die HistoryEntry-Text rendern, verwenden automatisch die richtige Komponente
RenderedMarkdownohnehistoryEntryIdbleibt erlaubt für echte read-only-Kontexte (Dashboard, Archiv)
Aufwand: ~7 Stellen umbauen, kein Logik-Änderung. Kein dringender Handlungsbedarf, aber sinnvoll beim nächsten größeren Refactoring-Sprint.