2 · uGo-Server — unsere eigene Schnittstelle unter /api/**¶
Note
Der Bauplan in einem Satz: Der Browser auf dem Tablett spricht ausschliesslich mit dem eigenen Server, dieser spricht mit dem Fahrgestell. Drei Gründe, alle drei tragend:
- Das Fahrgestell setzt keine CORS-Kopfzeilen. Ein unmittelbarer Zugriff aus dem Browser scheitert.
- Die Geräteadresse steht damit an genau einer Stelle in der Betriebsumgebung (
CHASSIS_BASIS) und nicht im ausgelieferten Javascript. Beim Kunden stehen mehrere Geräte, jedes mit eigener Adresse. - Kein Zugriff nach draussen: die Oberfläche kennt nur den eigenen Server.
Alle Antworten sind application/json mit cache-control: no-store, ausser wo unten etwas anderes steht.
2.1 Anmeldemodell¶
| Punkt | Wie es ist | Warum |
|---|---|---|
| Träger | Sitzungskeks, httpOnly, sameSite: strict, Laufzeit 8 Stunden |
httpOnly: ein eingeschleustes Skript in der Oberfläche kommt nicht an den Keks heran |
secure |
ausdrücklich false |
SvelteKit setzt secure von sich aus auf true, ausser bei localhost. Am Tablett über HTTP warf die Webansicht den Keks stillschweigend weg: die Anmeldung meldete 200, angemeldet war niemand. Der Roboter spricht im eigenen Netz HTTP; der Schutz kommt aus dem Netz, nicht aus dem Keks |
| Rolle | ergibt sich aus dem Kennwort | — |
| Bremse | 5 Fehlversuche je Absender, danach 60 Sekunden Sperre → 429 |
Ein vierstelliges Kennwort ist in Sekunden geraten, wenn man beliebig oft raten darf |
| Kein Kennwort eingerichtet | dann verlangt keine Route eine Anmeldung; POST /api/anmeldung antwortet 409 |
Ein frisch aufgesetztes Gerät soll bedienbar sein. Sobald ein Kennwort steht, greifen alle Prüfungen |
Jede Route prüft selbst. Der Türsteher in hooks.server.ts schützt Seiten, nicht die Schnittstelle. Eine Schnittstelle, die sich darauf verlässt, dass jemand anders vorher aufpasst, ist irgendwann offen.
2.2 Alle Routen¶
| Route | Verf. | Zweck / Rumpf | Antwort | Anmeldung |
|---|---|---|---|---|
/api/anmeldung |
GET | Anmeldezustand | {kennwortEingerichtet, angemeldet, rolle} — nie Kennwort oder Kennung |
nein |
| POST | {kennwort} |
200 {rolle} · 401 falsch · 409 kein Kennwort eingerichtet · 429 gesperrt |
nein | |
| DELETE | abmelden | {abgemeldet: true} |
nein | |
/api/chassis/<pfad> |
GET | Durchleitung zum Fahrgestell, Pfad und Suchteil unverändert | Antwort des Gerätes, durchgereicht | nein |
| POST | Durchleitung, Rumpf unverändert | dito | nein | |
| PATCH | Durchleitung, Rumpf unverändert | dito | nein | |
| DELETE | eng begrenzt: nur maps/<Zahl> und mappings/<Zahl> |
403 bei jedem anderen Pfad | ja | |
/api/einstellungen |
GET | Einstellungen lesen | Objekt | nein — die Oberfläche muss wissen, wann sie ins Ruhebild geht |
| POST | Teilmenge setzen. Anerkannt: ruhebildSekunden, zweitanzeige, medien, medienTon, medienLautstaerke, zweitanzeigeAdresse, fahrstuhlumgang, anstellart, uiotAdresse |
der gespeicherte Stand | ja | |
/api/fahrtenbuch |
GET | Auswertung, bewusst alle Tage auf einmal | 365 Zeilen mit drei Zahlen sind wenige Kilobyte; die Anzeige rechnet jeden Zeitraum ohne erneute Frage, und es gibt keine Datumsangaben in der Adresse, die man falsch verstehen kann | nein |
/api/funk |
GET | Dauerverbindung zum Fahrgestell, als SSE weitergereicht | text/event-stream. Ereignisse: lage ({verbunden, grund?}) und meldung (Rohtext des Gerätes). Herzschlag alle 15 s. 501, wenn der Server keinen WebSocket kann |
nein |
/api/karte-aus-aufnahme |
POST | {aufnahmeId, name} — aus einer fertigen Kartenfahrt eine Karte machen |
200 {karte} · 409 mit Klartextgrund |
ja |
/api/kartenarchiv |
GET | Abgleich Gerät ↔ Archiv | Stand · 502 wenn das Gerät nicht antwortet | ja |
| POST | {aktion} ∈ sichern (karteId), alleSichern, zurueckspielen (archivId, zweitname), entfernen (archivId) |
200 je Aktion · 400 unbekannte Aktion · 409 mit Klartextgrund | ja | |
/api/medien |
GET | Liste | {medien:[{datei, art, bytes}]} |
nein |
| POST | mehrteiliges Formular, Feld datei |
200 · 413 über 200 MB · 415 keine anzeigbare Art | ja | |
/api/medien/<datei> |
GET | Datei ausliefern, mit Bereichsanfragen | 200 / 206 / 416 · cache-control: public, max-age=3600 |
nein |
/api/protokolle |
GET | alle Betriebsdaten in einem Zip | application/zip. Enthalten: Serverprotokoll (aktuell + vorher), Fahrtenbuch, Einstellungen, Touren, Regeln, /device/info, /chassis/current-map, /mappings/. Nicht enthalten: Kennwörter, Sitzungsmarken, Mediendateien |
ja |
/api/protokolle/zeilen?anzahl=N |
GET | letzte Protokollzeilen aus dem Ringspeicher, N zwischen 5 und 500, Vorgabe 80 |
{zeilen:[…]} |
ja — im Protokoll stehen Geräteadressen und Fehlertexte |
/api/regeln |
GET | Regeln und Warteschlange | {regeln, planer} |
nein — der Bedienbildschirm zeigt an, was ansteht; das ist Auskunft |
| PUT | {regeln} |
{regeln, planer} |
ja — eine Regel legt fest, wann der Roboter von allein losfährt | |
/api/regeln/aktion |
POST | {aktion} ∈ ausloesen (regelId), taster (kennung), alarm (an, grund), weiter, entfernen (auftragId), alles-anhalten |
{text, planer} · 400 unbekannte Anweisung · 409 mit Grund |
nein, bewusst — siehe unten |
/api/touren |
GET | Touren lesen | {touren} |
nein |
| PUT | {touren} |
{touren} |
ja | |
/api/touren/lauf |
GET | Stand des Laufs | Laufzustand + planer |
nein |
| POST | {aktion} ∈ start (tourId), stop, pause, fortsetzen, bestaetigen |
Laufzustand + planer · 409 mit Grund |
nein, bewusst — siehe unten | |
/api/uiot/<pfad> |
GET | nur lesende Durchleitung zum u-IoT-Modul; Ziel aus einstellungen.uiotAdresse |
Antwort des Moduls im Rohtext · 403 gesperrter Pfad · 409 keine/ungültige Adresse · 502 keine Antwort | ja |
/api/zonen |
GET | ?karte=<id> — Zonen einer Karte |
{karteId, kartenname, fassung, zonen} · 502 wenn das Gerät nicht antwortet |
ja |
| POST | {aktion: "anlegen", karteId, name, ecken} oder {aktion: "entfernen", karteId, kennung} |
{zone} mit kennung, ecken, zonenNachher, fassungVorher, fassungNachher |
ja | |
/api/wolke HINWEIS |
GET | Stand der Cloud-Anbindung | {wolke, identitaet} — Verbindung, Puffer, Sperre, letzte Fassungsfreigabe, Umgebung, Thing-Name, Kunde |
ja |
| POST | {aktion: "anmelden", beitrittscode} |
200 {angemeldet, thingName, kunde, seriennummer} · 409 mit Grund |
ja |
Angelegt wird über /api/zonen ausschliesslich die Langsamzone (regionType 2). Sperrzonen bietet dieser Endpunkt bewusst nicht an, weil ihre Kennzahl nicht belegt ist (siehe 1.8). Ein doppelter Zonenname in derselben Karte wird abgewiesen — technisch wäre er zulässig, aber der Techniker unterscheidet zwei Zonen „Treppenhaus" hinterher nur noch an 24 Hexzeichen und entfernt dann die falsche.
Warning
Warum zwei Routen bewusst OHNE Anmeldung arbeiten.
/api/touren/lauf: Das Anlegen einer Tour ist Technikerarbeit und verlangt ein Kennwort. Das Starten und vor allem das Anhalten ist Bedienung. Wer neben einem fahrenden Roboter steht und ihn stoppen will, hat kein Kennwort dabei und soll auch keins brauchen. Ein Halteknopf, der erst eine Anmeldung verlangt, ist kein Halteknopf. stop leert deshalb auch die Warteschlange: wer hält, will einen stehenden Roboter, nicht einen, der gleich mit dem nächsten Auftrag weitermacht.
/api/regeln/aktion: Hier kommen die Anstösse aus der Umgebung an — ein Ruftaster an der Wand, der Alarmkontakt des Hauses. Diese Geräte tragen kein Kennwort und können keines tragen. Der Schutz liegt eine Ebene tiefer: der Server hört nur im Roboternetz. Das Anlegen von Regeln verlangt weiterhin eine Anmeldung — dort entscheidet sich, was ein Tasterdruck überhaupt bewirken darf.
2.3 Die Durchleitung /api/chassis/** im Einzelnen¶
| Verhalten | Wert | Warum |
|---|---|---|
| Fehlerbild | 504 Zeitüberschreitung, 502 nicht erreichbar |
Genau diese beiden Nummern wertet die Oberfläche als Verbindungsverlust — alles andere ist eine Gerätestörung |
| Zeitgrenzen | 5 s im Regelfall, 45 s für mappings und maps sowie schreibend für chassis/pose und chassis/current-map |
Lesen bleibt schnell, Schreiben darf dauern. GET /chassis/pose wird im Takt abgefragt; bekäme es 45 s, stünde die Oberfläche bei einem hängenden Gerät eine Dreiviertelminute still |
| Weiterleitungen | redirect: 'manual'. Verfolgt wird genau eine: derselbe Ursprung, derselbe Suchteil, nur ein Schrägstrich angehängt |
Die Schnittstelle des Fahrgestells hat keine Anmeldung. Wer im Netz antwortet, soll unseren Server nicht auf eine fremde Adresse schicken können. Der relative Verweis Djangos wird vorher zu einer vollständigen Adresse aufgelöst — fetch in Node wirft sonst „Failed to parse URL" |
| Schrägstrich | export const trailingSlash = 'ignore', und der Pfad wird aus url.pathname genommen, nicht aus params |
SvelteKit schneidet den Schrägstrich sonst ab und leitet mit 308 um, bevor die Handhabung läuft. Beim Fahrgestell käme wieder /maps ohne Schrägstrich an — und POST scheitert dann am APPEND_SLASH-Fehler |
| Kopfzeilen | durchgelassen werden nur content-type, content-length, cache-control, etag |
— |
| Grösse | Antworten über 4 MB (CHASSIS_GROESSTANTWORT_BYTES) werden verworfen |
Ein Gerät, das Megabytes liefert, soll weder das Tablett noch den Server beschäftigen |
| Fehlermeldung | enthält nie die Geräteadresse | Sie ist der einzige Wert, den der Server vor dem Browser voraushat; in einer Fehlermeldung stünde sie in jedem Fehlerbericht |
2.4 Die Durchleitung /api/uiot/** — drei Riegel¶
- Nur GET. Kein POST, kein PUT, kein DELETE. Was das Modul verändert — Türen öffnen, Aufzüge rufen — kommt erst dazu, wenn die Schnittstelle feststeht und jede Handlung eine Rückfrage bekommt.
- Gesperrte Pfade (auch lesend):
config,wifi,cmd,reboot,export,password,passwd,secret. Der Grund ist belegt: das Modul bietet ein „Export Config JSON", und darin stehen Kennwörter im Klartext. Ein Leserecht darauf wäre ein Leserecht auf die WLAN-Zugangsdaten des Kunden. - Nur mit Anmeldung.
Die Antwort wird unverändert durchgereicht — hier wird nichts gedeutet. Was das Modul meldet, soll der Techniker im Rohtext sehen; alles andere wäre Ratenwerk, solange die Schnittstellenbeschreibung des Moduls aussteht. Das ist der Platzhalter, der ersetzt wird, sobald diese Beschreibung vorliegt.