docs: Handbuchseite zum Erfassen und Beschreibung der Schnittstelle

Die Beschreibung sagt auch, was die Schnittstelle bewusst nicht kann —
Bereich setzen, Aufgaben loeschen, Zeitbloecke anfassen. Das ist eine
Entscheidung und kein Versehen, und wer davorsteht, soll es erfahren,
statt es fuer eine Luecke zu halten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-12 11:42:47 +02:00
co-authored by Claude Opus 5
parent 10b49cf8df
commit 2e57713a8e
6 changed files with 278 additions and 14 deletions
+5 -2
View File
@@ -39,6 +39,10 @@ wird.
- **Integrationen**: Zammad-Tickets werden zu Aufgaben, Telegram erfasst und - **Integrationen**: Zammad-Tickets werden zu Aufgaben, Telegram erfasst und
hakt ab, und der Spiegel bringt dienstliche Belegung nach Hause — ohne hakt ab, und der Spiegel bringt dienstliche Belegung nach Hause — ohne
Titel Titel
- **Schnellerfassung vom Handy**, die auch im Funkloch nichts verliert; der
Planer lässt sich als App auf dem Startbildschirm ablegen
- **Öffentliche Schnittstelle** mit Tokens je Anwendung, Rechten je Token und
jeder schreibenden Nutzung im Protokoll ([Beschreibung](docs/API.md))
- Anmeldung über einen OIDC-Anbieter, über Passkeys oder mit E-Mail und - Anmeldung über einen OIDC-Anbieter, über Passkeys oder mit E-Mail und
Passwort — je Instanz einzeln zuschaltbar Passwort — je Instanz einzeln zuschaltbar
- Rollen `Benutzer` und `Verwaltung`; jede schreibende Aktion wird - Rollen `Benutzer` und `Verwaltung`; jede schreibende Aktion wird
@@ -49,8 +53,7 @@ wird.
### Geplant ### Geplant
Schnellerfassung vom Handy, eine öffentliche API und ein MCP-Server. Ein MCP-Server. Siehe [ROADMAP.md](ROADMAP.md).
Siehe [ROADMAP.md](ROADMAP.md).
## Bildergalerie ## Bildergalerie
+158
View File
@@ -0,0 +1,158 @@
# Öffentliche Schnittstelle
Für fremde Anwendungen, Skripte und Automaten. Wer den Planer selbst bedient,
braucht sie nicht.
Basisadresse: `<APP_URL>/api/v1`
## Ausweisen
Jede Anfrage trägt einen Token:
```bash
curl -H "Authorization: Bearer planer_ab12cd34_…" \
https://planer.example.de/api/v1/selbst
```
Alternativ `X-Planer-Token: planer_…`, falls das Werkzeug keine
Authorization-Kopfzeile kann.
Tokens legst du in der Anwendung unter **API** an (nur Verwaltung). Der Wert
erscheint **einmal**; danach liegt in der Datenbank nur sein Fingerabdruck.
Wer ihn verliert, legt einen neuen an.
## Rechte
Drei Stück, und **keines schließt ein anderes ein**:
| Recht | Was damit geht |
| ----------- | -------------------------------------- |
| `lesen` | Aufgaben abfragen |
| `erfassen` | Neue Aufgaben anlegen — mehr nicht |
| `schreiben` | Bestehende Aufgaben ändern und abhaken |
Ein Dienst, der nur meldet „hier ist etwas Neues", bekommt `erfassen` und
sonst nichts. Das ist der häufigste Fall und der, bei dem ein mitgelesener
Token am wenigsten anrichtet.
## Fehler
| Status | Bedeutung |
| ------ | ----------------------------------------------------------- |
| 400 | Die Anfrage war fehlerhaft (Feldliste steht in der Antwort) |
| 401 | Kein gültiger Token |
| 403 | Dieser Token darf das nicht |
| 404 | Gibt es nicht |
401 sieht immer gleich aus — ob ein Token unbekannt, abgelaufen oder
zurückgezogen ist, verrät die Schnittstelle nicht. Von außen soll sich nicht
abfragen lassen, welche Tokens es gibt.
## Endpunkte
### `GET /selbst`
Wer bin ich, was darf ich? Braucht **kein** Recht — sonst müsste man raten,
ob der Token falsch ist oder nur zu wenig darf.
```json
{ "name": "Wartungsplaner-Import", "rechte": ["erfassen"] }
```
### `GET /aufgaben` · Recht `lesen`
| Parameter | Standard | Bedeutung |
| --------- | -------- | ------------------- |
| `grenze` | 25 | Höchstens 100 |
| `versatz` | 0 | Zum Blättern |
| `offen` | — | `true` oder `false` |
```json
{
"aufgaben": [
{
"id": "clx…",
"titel": "#4711 Drucker klemmt",
"notiz": null,
"status": "OFFEN",
"prioritaet": 3,
"aufwandMinuten": null,
"faelligAm": null,
"quelle": "API",
"ref": "ticket-4711",
"url": "https://…",
"erstelltAm": "2026-03-17T08:20:00.000Z",
"bereich": null,
"projekt": null
}
],
"gesamt": 42,
"grenze": 25,
"versatz": 0
}
```
### `POST /aufgaben` · Recht `erfassen`
```bash
curl -X POST https://planer.example.de/api/v1/aufgaben \
-H "Authorization: Bearer planer_…" \
-H "Content-Type: application/json" \
-d '{"titel":"Drucker klemmt","ref":"ticket-4711"}'
```
| Feld | Pflicht | Bemerkung |
| ---------------- | ------- | ----------------------------------- |
| `titel` | ja | bis 200 Zeichen |
| `notiz` | nein | bis 4000 Zeichen |
| `ref` | nein | Kennung beim Aufrufer — siehe unten |
| `url` | nein | Verweis ins Ursprungssystem |
| `faelligAm` | nein | `JJJJ-MM-TT` |
| `prioritaet` | nein | 1 (dringend) bis 5 (irgendwann) |
| `aufwandMinuten` | nein | 1 bis 1440 |
Antwort: `201` mit der angelegten Aufgabe.
**`ref` macht den Aufruf wiederholbar.** Zweimal dieselbe Kennung ergibt
**eine** Aufgabe, nicht zwei — der zweite Aufruf frischt Titel, Notiz und
Verweis auf. Ohne `ref` entsteht jedes Mal eine neue Aufgabe; dann hast du es
so gewollt.
Benutze `ref`, wann immer es drüben eine stabile Kennung gibt. Das ist der
Unterschied zwischen einer Schnittstelle, die man nach einem Netzhänger
gefahrlos wiederholen kann, und einer, die dabei Karteileichen anlegt.
### `GET /aufgaben/<id>` · Recht `lesen`
### `PATCH /aufgaben/<id>` · Recht `schreiben`
```bash
curl -X PATCH https://planer.example.de/api/v1/aufgaben/clx… \
-H "Authorization: Bearer planer_…" \
-H "Content-Type: application/json" \
-d '{"erledigt":true}'
```
Änderbar sind `titel`, `notiz`, `faelligAm`, `prioritaet`, `aufwandMinuten`
und `erledigt`. Mindestens ein Feld muss dabei sein.
## Was die Schnittstelle bewusst **nicht** kann
- **Bereich und Projekt setzen.** Was von außen kommt, landet in der Inbox.
Die Einordnung ist Sache dessen, der plant, nicht dessen, der meldet —
sonst räumt ein fremder Dienst die eigene Ordnung um.
- **Aufgaben löschen.** Abhaken ja, löschen nein. Ein Fehler im fremden
Skript soll keine Planung vernichten.
- **Zeitblöcke, Bereiche, Projekte oder Kalender anfassen.**
Fehlt dir etwas davon, ist das eine Entscheidung und kein Versehen — sag
Bescheid, dann wird darüber geredet.
## Protokoll
Jede **schreibende** Nutzung landet im Protokoll, mit dem Token, über den sie
kam. Lesen nicht: Ein Dienst, der im Minutentakt abfragt, würde das Protokoll
unbrauchbar machen.
Auch ein abgewiesener Zugriff wird vermerkt. Ein Dienst, der immer wieder an
dieselbe Wand läuft, ist entweder falsch eingerichtet oder versucht etwas.
+11 -9
View File
@@ -13,34 +13,36 @@ Anleitung für den Alltag mit dem Planer. Eine Seite je Thema.
3. **[Aufgaben erfassen und wiederfinden](aufgaben.md)** – Schnellerfassung, 3. **[Aufgaben erfassen und wiederfinden](aufgaben.md)** – Schnellerfassung,
Inbox leeren, Filter, Status und Fälligkeit Inbox leeren, Filter, Status und Fälligkeit
4. **[Ideen](ideen.md)** – festhalten, was noch keine Aufgabe ist 4. **[Schnellerfassung vom Handy](erfassen.md)** – als App ablegen, auch
5. **[Projekte](projekte.md)** – mehrere Aufgaben unter einem Ziel, mit ohne Netz erfassen
5. **[Ideen](ideen.md)** – festhalten, was noch keine Aufgabe ist
6. **[Projekte](projekte.md)** – mehrere Aufgaben unter einem Ziel, mit
Fortschritt Fortschritt
6. **[Kalenderkonten](kalenderkonten.md)** – Microsoft 365 und Google 7. **[Kalenderkonten](kalenderkonten.md)** – Microsoft 365 und Google
verbinden, Belegung verstehen verbinden, Belegung verstehen
7. **[Wochenplanung](woche.md)** – Aufgaben per Ziehen und Ablegen in den 8. **[Wochenplanung](woche.md)** – Aufgaben per Ziehen und Ablegen in den
Kalender legen Kalender legen
8. **[Benachrichtigungen](benachrichtigungen.md)** – Push, E-Mail und 9. **[Benachrichtigungen](benachrichtigungen.md)** – Push, E-Mail und
Telegram einrichten, Ruhezeiten setzen Telegram einrichten, Ruhezeiten setzen
9. **[Integrationen](integrationen.md)** – Zammad, Telegram und der Spiegel 10. **[Integrationen](integrationen.md)** – Zammad, Telegram und der Spiegel
zwischen Firma und Daheim zwischen Firma und Daheim
## Verwaltung ## Verwaltung
10. **[Protokoll](protokoll.md)** – nachsehen, wer wann was geändert hat 11. **[Protokoll](protokoll.md)** – nachsehen, wer wann was geändert hat
## Was noch kommt ## Was noch kommt
Diese Seiten entstehen mit den nächsten Meilensteinen: Diese Seiten entstehen mit den nächsten Meilensteinen:
| Thema | Meilenstein | | Thema | Meilenstein |
| ------------------------------- | ----------- | | ----- | ----------- |
| Schnellerfassung vom Handy, API | M7 |
| MCP | M8 | | MCP | M8 |
Der [Fahrplan](../../ROADMAP.md) sagt, was in welchem Meilenstein steckt. Der [Fahrplan](../../ROADMAP.md) sagt, was in welchem Meilenstein steckt.
--- ---
Für fremde Anwendungen und Skripte: [Schnittstelle](../API.md).
Zur Einrichtung einer neuen Instanz: [Schnellstart](../SCHNELLSTART.md). Zur Einrichtung einer neuen Instanz: [Schnellstart](../SCHNELLSTART.md).
Für Betrieb, Sicherung und alle Umgebungsvariablen: [README](../../README.md). Für Betrieb, Sicherung und alle Umgebungsvariablen: [README](../../README.md).
+61
View File
@@ -0,0 +1,61 @@
# Schnellerfassung vom Handy
Die Seite **Erfassen** ist für den Moment gemacht, in dem dir etwas einfällt
und du keine Zeit hast, es einzusortieren: ein Feld, ein Knopf, fertig.
![Die Seite Erfassen](../bilder/erfassen-hell-handy.png)
Alles, was hier hereinkommt, landet in der **Inbox** — ohne Bereich, ohne
Termin. Das Einsortieren passiert später am Rechner, wenn Ruhe dafür ist.
## Als Anwendung ablegen
Der Planer lässt sich auf dem Startbildschirm ablegen und verhält sich dann
wie eine App: eigenes Symbol, kein Browserrahmen, direkt auf dieser Seite.
| Gerät | Weg |
| --------------------- | -------------------------------------------- |
| Android (Chrome) | Menü ⋮ → **App installieren** |
| iPhone (Safari) | Teilen-Symbol → **Zum Home-Bildschirm** |
| Rechner (Chrome/Edge) | Symbol in der Adresszeile → **Installieren** |
## Ohne Netz
**Das ist der eigentliche Punkt dieser Seite.** Im Keller, im Aufzug, im Zug
zwischen zwei Funkzellen: Erfasst wird trotzdem.
Was passiert:
1. Das Erfasste wird **zuerst im Gerät gemerkt**, dann verschickt. Nicht
andersherum — sonst wäre es weg, wenn du den Browser mitten im Senden
schließt.
2. Ohne Netz bleibt es liegen und steht unter dem Feld als „wartet".
3. Sobald wieder Netz da ist, geht es von selbst hinaus. Du bekommst eine
kurze Rückmeldung.
Der Knopf **Jetzt versuchen** stößt das von Hand an, wenn du nicht warten
magst.
> **Geht dabei etwas verloren?** Nein. Jede Erfassung bekommt eine eigene
> Kennung, die mitgeschickt wird. Kommt dieselbe Nachricht zweimal an — weil
> die Verbindung beim ersten Versuch abbrach, die Anfrage aber trotzdem
> durchging —, entsteht trotzdem nur **eine** Aufgabe.
Die Warteschlange liegt in der Datenbank des Browsers, nicht im flüchtigen
Speicher. Sie übersteht das Schließen des Tabs und einen Neustart des
Geräts.
Damit die Seite ohne Netz überhaupt aufgeht, hält der Planer genau **diese
eine Seite** vor. Alle anderen Seiten holt er immer frisch: Ein halb
veralteter Planer wäre schlimmer als gar keiner — ein leeres Eingabefeld
dagegen veraltet nicht.
## Warum nicht die Erfassung auf der Startseite?
Die gibt es weiterhin, und sie ist am Rechner meist der kürzere Weg. Der
Unterschied ist die Verlässlichkeit: Die Schnellerfassung ist die einzige
Stelle, die ohne Netz nichts verliert.
---
Weiter zur [Inbox](aufgaben.md) oder zurück zum [Handbuch](README.md).
+38
View File
@@ -489,6 +489,44 @@ async function main(): Promise<void> {
}) })
} }
// --- API-Tokens -----------------------------------------------------------
// Erfundene Hashes: Mit diesen Zeilen laesst sich nichts aufrufen, sie
// zeigen nur, wie die Liste aussieht.
await db.apiToken.deleteMany({})
await db.apiToken.create({
data: {
benutzerId: benutzer.id,
name: 'Wartungsplaner-Import',
praefix: 'k7m2xq4b',
hash: 'demo-1',
rechte: 'erfassen',
erstelltAm: new Date(Date.UTC(2026, 0, 14, 9, 30)),
letzteNutzung: new Date(Date.UTC(2026, 2, 17, 5, 12)),
},
})
await db.apiToken.create({
data: {
benutzerId: benutzer.id,
name: 'Auswertung Homelab',
praefix: 'r3wp9dt4',
hash: 'demo-2',
rechte: 'lesen',
gueltigBis: new Date(Date.UTC(2026, 8, 30, 22, 0)),
erstelltAm: new Date(Date.UTC(2026, 1, 2, 18, 45)),
},
})
await db.apiToken.create({
data: {
benutzerId: benutzer.id,
name: 'Altes Skript',
praefix: 'z8n4vt6c',
hash: 'demo-3',
rechte: 'lesen,schreiben',
erstelltAm: new Date(Date.UTC(2025, 10, 3, 11, 0)),
widerrufenAm: new Date(Date.UTC(2026, 1, 20, 8, 15)),
},
})
console.log(`Demo-Daten stehen bereit. Anmeldung: ${DEMO_EMAIL}`) console.log(`Demo-Daten stehen bereit. Anmeldung: ${DEMO_EMAIL}`)
} }
+2
View File
@@ -37,6 +37,7 @@ const AUFNAHMEN: Aufnahme[] = [
{ name: 'anmelden', pfad: '/anmelden' }, { name: 'anmelden', pfad: '/anmelden' },
{ name: 'start', pfad: '/' }, { name: 'start', pfad: '/' },
{ name: 'inbox', pfad: '/inbox' }, { name: 'inbox', pfad: '/inbox' },
{ name: 'erfassen', pfad: '/erfassen' },
{ name: 'aufgaben', pfad: '/aufgaben' }, { name: 'aufgaben', pfad: '/aufgaben' },
{ name: 'woche', pfad: '/woche' }, { name: 'woche', pfad: '/woche' },
{ name: 'projekte', pfad: '/projekte' }, { name: 'projekte', pfad: '/projekte' },
@@ -50,6 +51,7 @@ const AUFNAHMEN: Aufnahme[] = [
pfad: '/einstellungen', pfad: '/einstellungen',
ausschnitt: '[data-bild="benachrichtigungen"]', ausschnitt: '[data-bild="benachrichtigungen"]',
}, },
{ name: 'api', pfad: '/verwaltung/api' },
{ name: 'protokoll', pfad: '/verwaltung/protokoll' }, { name: 'protokoll', pfad: '/verwaltung/protokoll' },
] ]