Grotti 3.6.0: Lauf-Identität nach Filament-Pause, robustere Queue, Projekte/Aufträge list-first und API-Polish.

This commit is contained in:
Dominic Gundel committed 2026-10-25 08:24:24 +01:00
1 parent d1eb87ba25
commit 2b1e36634a
53 files changed
+4254 -636

No files matched your search

+86
View File
@@ -0,0 +1,86 @@
# Grotti HTTP-API (`/api/v1`)
Stand für Android/App-Clients und Integrationen. Nutzer-Anleitungen bleiben unter `docs/Anleitung-*.md`.
## Auth
| Weg | Header / Cookie | CSRF |
| --- | --- | --- |
| Session | Cookie nach `POST /auth/login` | `X-CSRF-Token` bei mutierenden API-Calls |
| Bearer | `Authorization: Bearer grt_…` | nicht nötig |
**Token ausstellen**
- Eingeloggt: `POST /tokens` `{name, role?}` → Klartext einmalig in `token`
- Ohne Session (App): `POST /auth/token` `{username, password, device_name?, role?}` → `201` mit `token` + `user` (Rate-Limit wie Login)
Öffentlich ohne Login: `/auth/status`, `/auth/login`, `/auth/token`, `/auth/setup`, `/version`.
Rollen: `operator` ≤ `admin`. Admin-only u. a. Benutzer, Backup, volle Einstellungen, Drucker anlegen.
**Medien mit Query-Token** (nur GET): `?token=grt_…` für Kamera, Live-Vorschau, Datei-Vorschau (`/dateien/<id>/vorschau`), Projekt-Assets.
## Fehler
```json
{"error": "Text", "code": "kurzcode"}
```
HTTP-Status: 400/401/403/404/409/429/503. Bild-Endpunkte liefern bei fehlendem Bild ebenfalls dieses JSON (nicht nur leeren 404).
## Pagination
Listen akzeptieren `limit` / `offset` wo sinnvoll. Antwort bleibt ein **JSON-Array**; Gesamtzahl im Header `X-Total-Count`.
Ohne `limit` liefern `/dateien` und `/spulen` die volle Liste (Web-UI).
## Bestätigung
Mutierende Druckaktionen brauchen oft `"confirm": true`:
- `POST /senden`, `POST /druck-queue`, `POST /ablauf`
- `POST /projekte/<id>/einreihen`, `POST /auftraege/<id>/einreihen`
- `POST /drucker/<host>/abraeumen`
- `steuern` mit `emergency_stop`, `clear_error`, `exclude_object`
## Server-Info
`GET /version` → `{name, version, api: 1, features: [...]}`.
## SSE Live
`GET /stream` (Session-Cookie oder Bearer). Events:
| Event | Bedeutung |
| --- | --- |
| `ping` | Keepalive (~15 s), Payload `{}` |
| `drucker` | Live-Zustand der Drucker geändert |
| `queue` | Druckwarteschlange geändert |
| `senden` | Sende-Auftrag geändert |
| `ereignis` | Neues Ereignis / Aktivität |
| `aufgaben` | Offene Aufgaben (Platte …) |
Ohne Stream: Client pollt `/drucker?live=1`, `/jetzt`, `/druck-queue`.
## Push an Geräte
Grotti sendet **kein** FCM. Empfohlen: **ntfy** (Topic unter Einstellungen). Die App kann denselben Topic abonnieren; Secrets (ntfy-Token) bleiben serverseitig.
## Wichtige Ressourcen
| Bereich | Pfade |
| --- | --- |
| Drucker | `GET /drucker?live=1`, `POST /drucker/<host>/pause\|fortsetzen\|abbrechen` (Alias `resume`/`cancel`), `…/steuern`, `…/abraeumen` |
| Dateien | `GET /dateien?family=&material=&project_id=&host=&q=&limit=&offset=`, Upload multipart, `…/vorschau`, `…/pfad` |
| Senden / Queue | `POST /senden`, `GET /senden-zuletzt?host=&file_id=`, `GET\|POST /druck-queue?host=&status=`, `POST /ablauf` |
| Projekte | `GET\|POST /projekte`, `GET /projekte/<id>`, `…/laeufe`, `POST …/einreihen`, Assets |
| Aufträge | `GET\|POST /auftraege`, Positionen, `…/einreihen` |
| Material | `GET /spulen?aktiv=1`, Akte, Einlegen |
| Protokoll | `GET /laeufe`, `GET /laeufe/<id>`, `GET /statistik?project_id=` |
| Kamera | `GET /kamera?host=`, `…/stream` |
Pfadnamen sind überwiegend **deutsch**. Aktionen: deutsch + englische Alias für Pause/Resume/Cancel.
## MCP
`python -m grotti.mcp_server` nutzt dieselbe REST-API (Teilmenge). Siehe `docs/internal/MCP.md`.
+10
View File
@@ -134,6 +134,16 @@ Ohne Map: Rinkhals füllt oft Identity T0→Platz0 … — deshalb Grotti bei ei
| Slots anzeigen | Moonraker `mmu` (Rinkhals) | `mmu_ace.py` |
| Kein Firmware-Restart | abgelehnt | Rinkhals #32, `patch_klipper_restart` |
## History nach Filament-Pause
Runout, Rolle tauschen, Fortsetzen: Anycubic schließt den History-Job als `cancelled` und öffnet eine neue Job-ID. Beobachtet am 2026-10-25 auf Kobra 3 A1 (`000040` → `000041`):
- Der neue `start_time` liegt etwa 0,04 s nach dem `end_time` des alten.
- `print_duration` und `total_duration` sind an beiden Einträgen gleich und laufen weiter. Der Zähler beginnt nicht bei 0.
- `/server/history/list` liefert neueste zuerst. Grotti sortiert pro Drucker nach `start_time`, sonst kommt die Fortsetzung vor ihrem Vorgänger dran (`jobs.history_in_order`).
- Nach dem Zusammenlegen hängt die alte Job-ID in `run_jobs` am Lauf. `store.history_job_retired` überspringt sie, sonst schreibt sie `cancelled` zurück.
- Zusammenlegen nur mit Beweis: `print_duration` fortgesetzt und Lücke ≤ 5 s, oder Watcher `paused→printing` (`docs/internal/Lauf-Identitaet.md`).
## Offene Grenzen
- Quellcode von **gkapi** (Task-State-Machine, exakte 10101-Bedingungen) ist nicht öffentlich.
+31
View File
@@ -0,0 +1,31 @@
# Lauf-Identität: ein physischer Druck = ein Lauf
## Regel
Zusammengelegt wird nur mit **Beweis**, nicht nur wegen Zeit und Dateiname:
1. Der neue Moonraker-Job hat `print_duration` ≥ Dauer des Vorgängers − 30 s, und die Lücke `end→start` ist höchstens 5 s (Anycubic setzt `end_time` erst beim Fortsetzen).
2. Oder der Watcher hat `paused → printing` bei gleicher Datei gesehen (`watcher_resume`, `printers.active_run_id`).
Ein Neustart mit `print_duration` ≤ 60 s wird nie zusammengelegt.
## Pfade
| Pfad | Datei |
| --- | --- |
| Beweis | `jobs.is_filament_resume` |
| Fold | `jobs.fold_filament_resume` |
| Abgleich | `jobs.sync_history` (Advisory-Lock) |
| Job-Aliase | `run_jobs` + `history_job_retired` |
| Leere Hülle | übersprungen in `_sync_one_job` |
| Live-Abzug | erst History; Fallback nach 10 min (`LIVE_DEDUCT_FALLBACK_S`) |
| Manuell | `POST /api/v1/laeufe/<id>/verbinden` |
## Queue
In `print_active` heißt nur `complete` fertig. `cancelled`/`standby` werden entprellt (`TERMINAL_CONFIRM_HITS`). Echter Abbruch → Status `cancelled`, nicht „Fertig.“.
## Reparatur
`scripts/laeufe_reparieren.py` (Probelauf standardmäßig, `--apply` nach Freigabe).
Verwaiste `live:`-Läufe nur an offenen History-Jobs **derselben Datei**; `adopt_live_run` greift geschlossene Live-Läufe nur ≤ 2 h.
+1 -1
View File
@@ -27,7 +27,7 @@ Jedes Ereignis landet in `events` und auf der Seite `/aktivitaet`. Telegram ist
| `fertig` | Watcher, `printing`→`complete` | nein, Platte unter „Aufgaben“ | an |
| `fehler` | Watcher, Zustand `error` | ja | an |
| `abbruch` | Watcher, `printing`/`paused`→`cancelled`/`standby` | ja | wie `fehler` |
| `offline` | Watcher, ≥2 Polls weg; auch „wieder erreichbar“ | ja | an |
| `offline` | Watcher, ≥2 Polls weg; auch „wieder erreichbar“. Beendet den Lauf nicht und zieht nichts ab — der letzte Druckzustand bleibt gemerkt | ja | an |
| `spule` | Watcher, Rest unter Job-Gewicht | ja | an |
| `queue` | Dispatcher: Queue-Fehler, Ablauf gestoppt | ja | an |
| `start` | Watcher: Druck startet, Pause, Fortsetzen | nein | aus |
+3
View File
@@ -6,5 +6,8 @@ Nutzer-Handbuch: `docs/Anleitung-*.md`.
| Datei | Inhalt |
| --- | --- |
| [API.md](API.md) | HTTP-API für App/Integrationen (Auth, confirm, SSE, Filter) |
| [openapi.yaml](openapi.yaml) | OpenAPI-Skizze (Client-Generator) |
| [MCP.md](MCP.md) | MCP-Server und Tool-Ablauf |
| [Makros-Kobra.md](Makros-Kobra.md) | G-code/Makros Kobra S1 und 3: Nicht-tun, Vanilla-Alltag, ACE, LeviQ/Wipe |
| [Anycubic-Stack.md](Anycubic-Stack.md) | GoKlipper, MQTT-Start/Stop, ACE-Kanäle |
+180
View File
@@ -0,0 +1,180 @@
openapi: 3.0.3
info:
title: Grotti API
version: "1"
description: |
JSON unter /api/v1. Auth: Session-Cookie oder Bearer grt_….
Mutierende Session-Calls brauchen Header X-CSRF-Token.
Siehe docs/internal/API.md.
servers:
- url: /api/v1
paths:
/version:
get:
summary: Server-Info
security: []
responses:
"200":
description: name, version, api, features
/auth/login:
post:
summary: Session-Login
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [username, password]
properties:
username: { type: string }
password: { type: string }
responses:
"200":
description: user + csrf
/auth/token:
post:
summary: Passwort → API-Token (App)
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [username, password]
properties:
username: { type: string }
password: { type: string }
device_name: { type: string }
role: { type: string, enum: [operator, admin] }
responses:
"201":
description: token (einmalig) + user
/drucker:
get:
summary: Druckerliste
parameters:
- name: live
in: query
schema: { type: integer, enum: [0, 1] }
responses:
"200":
description: Array Drucker
/drucker/{host}/{action}:
post:
summary: Pause / Fortsetzen / Abbrechen
parameters:
- name: host
in: path
required: true
schema: { type: string }
- name: action
in: path
required: true
schema:
type: string
enum: [pause, fortsetzen, abbrechen, resume, cancel]
responses:
"200":
description: OK
/dateien:
get:
summary: Bibliothek
parameters:
- { name: family, in: query, schema: { type: string } }
- { name: material, in: query, schema: { type: string } }
- { name: project_id, in: query, schema: { type: integer } }
- { name: host, in: query, schema: { type: string } }
- { name: q, in: query, schema: { type: string } }
- { name: limit, in: query, schema: { type: integer } }
- { name: offset, in: query, schema: { type: integer } }
responses:
"200":
description: Array Dateien; Header X-Total-Count
headers:
X-Total-Count:
schema: { type: integer }
/senden:
post:
summary: Druck starten oder einreihen
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [file_id, confirm]
properties:
file_id: { type: integer }
hosts: { type: array, items: { type: string } }
confirm: { type: boolean }
when_free: { type: boolean }
ace_maps: { type: object }
bed_leveling: { type: boolean }
responses:
"202":
description: jobs
/druck-queue:
get:
summary: Warteschlange
parameters:
- { name: host, in: query, schema: { type: string } }
- { name: status, in: query, schema: { type: string } }
- { name: limit, in: query, schema: { type: integer } }
- { name: offset, in: query, schema: { type: integer } }
responses:
"200":
description: Array; X-Total-Count
post:
summary: Einreihen
requestBody:
content:
application/json:
schema:
type: object
required: [file_id, confirm]
responses:
"200":
description: Queue-Eintrag
/projekte/{id}/einreihen:
post:
summary: Projektdateien einreihen
parameters:
- name: id
in: path
required: true
schema: { type: integer }
requestBody:
content:
application/json:
schema:
type: object
required: [confirm]
properties:
confirm: { type: boolean }
file_ids: { type: array, items: { type: integer } }
menge: { type: integer }
responses:
"201":
description: jobs
/stream:
get:
summary: SSE Live-Events
responses:
"200":
description: text/event-stream (drucker, queue, senden, ereignis, ping)
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: grt_
cookieAuth:
type: apiKey
in: cookie
name: session
security:
- bearerAuth: []
- cookieAuth: []