diff --git a/README.md b/README.md index 8056d0e..d9d5393 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,10 @@ wird. - **Integrationen**: Zammad-Tickets werden zu Aufgaben, Telegram erfasst und hakt ab, und der Spiegel bringt dienstliche Belegung nach Hause — ohne 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 Passwort — je Instanz einzeln zuschaltbar - Rollen `Benutzer` und `Verwaltung`; jede schreibende Aktion wird @@ -49,8 +53,7 @@ wird. ### Geplant -Schnellerfassung vom Handy, eine öffentliche API und ein MCP-Server. -Siehe [ROADMAP.md](ROADMAP.md). +Ein MCP-Server. Siehe [ROADMAP.md](ROADMAP.md). ## Bildergalerie diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..c273567 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,158 @@ +# Öffentliche Schnittstelle + +Für fremde Anwendungen, Skripte und Automaten. Wer den Planer selbst bedient, +braucht sie nicht. + +Basisadresse: `/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/` · Recht `lesen` + +### `PATCH /aufgaben/` · 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. diff --git a/docs/handbuch/README.md b/docs/handbuch/README.md index ba80942..efd439f 100644 --- a/docs/handbuch/README.md +++ b/docs/handbuch/README.md @@ -13,34 +13,36 @@ Anleitung für den Alltag mit dem Planer. Eine Seite je Thema. 3. **[Aufgaben erfassen und wiederfinden](aufgaben.md)** – Schnellerfassung, Inbox leeren, Filter, Status und Fälligkeit -4. **[Ideen](ideen.md)** – festhalten, was noch keine Aufgabe ist -5. **[Projekte](projekte.md)** – mehrere Aufgaben unter einem Ziel, mit +4. **[Schnellerfassung vom Handy](erfassen.md)** – als App ablegen, auch + ohne Netz erfassen +5. **[Ideen](ideen.md)** – festhalten, was noch keine Aufgabe ist +6. **[Projekte](projekte.md)** – mehrere Aufgaben unter einem Ziel, mit Fortschritt -6. **[Kalenderkonten](kalenderkonten.md)** – Microsoft 365 und Google +7. **[Kalenderkonten](kalenderkonten.md)** – Microsoft 365 und Google 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 -8. **[Benachrichtigungen](benachrichtigungen.md)** – Push, E-Mail und +9. **[Benachrichtigungen](benachrichtigungen.md)** – Push, E-Mail und Telegram einrichten, Ruhezeiten setzen -9. **[Integrationen](integrationen.md)** – Zammad, Telegram und der Spiegel - zwischen Firma und Daheim +10. **[Integrationen](integrationen.md)** – Zammad, Telegram und der Spiegel + zwischen Firma und Daheim ## 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 Diese Seiten entstehen mit den nächsten Meilensteinen: -| Thema | Meilenstein | -| ------------------------------- | ----------- | -| Schnellerfassung vom Handy, API | M7 | -| MCP | M8 | +| Thema | Meilenstein | +| ----- | ----------- | +| MCP | M8 | 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). Für Betrieb, Sicherung und alle Umgebungsvariablen: [README](../../README.md). diff --git a/docs/handbuch/erfassen.md b/docs/handbuch/erfassen.md new file mode 100644 index 0000000..0478259 --- /dev/null +++ b/docs/handbuch/erfassen.md @@ -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). diff --git a/prisma/demo-seed.ts b/prisma/demo-seed.ts index 4830f74..3a27903 100644 --- a/prisma/demo-seed.ts +++ b/prisma/demo-seed.ts @@ -489,6 +489,44 @@ async function main(): Promise { }) } + // --- 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}`) } diff --git a/screenshots/bilder.spec.ts b/screenshots/bilder.spec.ts index da824ef..9f412cc 100644 --- a/screenshots/bilder.spec.ts +++ b/screenshots/bilder.spec.ts @@ -37,6 +37,7 @@ const AUFNAHMEN: Aufnahme[] = [ { name: 'anmelden', pfad: '/anmelden' }, { name: 'start', pfad: '/' }, { name: 'inbox', pfad: '/inbox' }, + { name: 'erfassen', pfad: '/erfassen' }, { name: 'aufgaben', pfad: '/aufgaben' }, { name: 'woche', pfad: '/woche' }, { name: 'projekte', pfad: '/projekte' }, @@ -50,6 +51,7 @@ const AUFNAHMEN: Aufnahme[] = [ pfad: '/einstellungen', ausschnitt: '[data-bild="benachrichtigungen"]', }, + { name: 'api', pfad: '/verwaltung/api' }, { name: 'protokoll', pfad: '/verwaltung/protokoll' }, ]