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:
1 parent
d1eb87ba25
commit
2b1e36634a
53 files changed
+4254
-636
No files matched your search
@@ -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`.
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
@@ -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: []
|
||||
Reference in new issue
Block a user