Endpoint /vehicles/:id/latest #6

Closed
opened 2026-08-27 22:26:25 +02:00 by erdbruegger · 1 comment
Owner

Können wir den Endpoint bullet-proof machen?
Aktuell zieht der einfach den letzten bekannten Eintrag... Was ja auch formal korrekt ist. Ich halte es jetzt für sinnvoll, dass der Enpoint für jeden Eintrag den zuletzt bekannten werden, lädt und dann in einem Datensatz merged.
Der Payload könnte dabei auch exemplarisch so aussehen:
{
"package_voltage": {
"ts":"yyyy-mm-dd HH:mm:ss",
"value":373.6
},
"soc":{
"ts":"yyyy-mm-dd HH:mm:ss",
"value":70
},
"gps":{
"ts":...,
"lat":...,
},
etc. pp.
}

Also das quasi für jeden Wert sein zuletzt bekannter Eintrag ausgegeben wird. Ich finde das macht am meisten Sinn.

Können wir den Endpoint bullet-proof machen? Aktuell zieht der einfach den letzten bekannten Eintrag... Was ja auch formal korrekt ist. Ich halte es jetzt für sinnvoll, dass der Enpoint für jeden Eintrag den zuletzt bekannten werden, lädt und dann in einem Datensatz merged. Der Payload könnte dabei auch exemplarisch so aussehen: { "package_voltage": { "ts":"yyyy-mm-dd HH:mm:ss", "value":373.6 }, "soc":{ "ts":"yyyy-mm-dd HH:mm:ss", "value":70 }, "gps":{ "ts":..., "lat":..., }, etc. pp. } Also das quasi für jeden Wert sein zuletzt bekannter Eintrag ausgegeben wird. Ich finde das macht am meisten Sinn.
Collaborator

Umgesetzt in e5ea822 (release) — CI grün, deployt, live verifiziert.

Genau der Punkt, den du beschreibst — hier an echten Daten, wie es vorher aussah. Der neueste Record war der vom 25.08., und der führte nur den Kilometerstand:

?flat=1  ->  ts=2026-08-25  soc=70  pack_voltage=null  odometer=null

Formal korrekt, praktisch fast nutzlos: .inx führt keinen SoC, cache_data keine Zellspannungen, also hat jeder einzelne Record Löcher.

Jetzt: je Messgröße ihr letzter bekannter Wert, mit eigenem Zeitstempel

{
  "soc":          { "ts": "2026-08-20T10:00:00.000Z", "value": 70 },
  "pack_voltage": { "ts": "2026-08-10T10:00:00.000Z", "value": 373.6 },
  "odometer":     { "ts": "2026-08-25T10:00:00.000Z", "value": 1500 },
  "gps":          { "ts": "2026-08-10T10:00:00.000Z", "lat": 51.5, "lon": 7.4 },
  "cells":        { "ts": "2026-08-01T10:00:00.000Z", "value": [3.7, 3.8] },
  "source":       { "ts": "2026-08-25T10:00:00.000Z", "value": "json" }
}

Skalare als {ts, value} wie in deinem Beispiel, GPS als {ts, lat, lon} — lat und lon kommen dabei garantiert aus demselben Record, sonst zeigt die Position auf einen Ort, an dem das Auto nie war. Messgrößen, für die es überhaupt keinen Wert gibt, fehlen im Objekt (statt als null dazustehen). Dabei sind auch Höhe, Kurs, Gang, Ladezustand, Zellen, Temperaturen und die Quelle.

Der eigene Zeitstempel je Größe ist dabei der eigentliche Gewinn: ein Wert von vor drei Tagen ist als solcher erkennbar, statt sich als aktuell auszugeben. Im Dashboard steht deshalb hinter jedem Wert sein Alter („vor 3 h").

Bullet-proof heißt auch: es muss schnell bleiben

Eine Aggregation über die ganze Tabelle wäre pro Abfrage ein Full Scan gewesen. Stattdessen läuft je Feld eine Unterabfrage ORDER BY ts DESC LIMIT 1, die den Index (vehicle_id, ts DESC) nutzt und beim ersten Treffer abbricht — alle zusammen als ein UNION ALL, also ein Roundtrip. Per EXPLAIN gegengeprüft:

Limit
  ->  Index Scan using telemetry_vehicle_ts_idx on telemetry
        Index Cond: (vehicle_id = 1)
        Filter: (soc IS NOT NULL)

Die Laufzeit hängt damit an der Anzahl Felder, nicht an der Anzahl Records.

Entscheidungen (proceed-with-default)

  • Die neue Form ist der Default, die alte bleibt über ?flat=1 erreichbar. Falls irgendwo noch ein Client auf dem flachen Record sitzt, kippt ihm damit nichts weg.
  • Zeitstempel bleiben ISO 8601 in UTC (2026-08-20T10:00:00.000Z) statt yyyy-mm-dd HH:mm:ss aus deinem Beispiel — so wie überall sonst in der API, und mit Zeitzone. Wenn du das Format lieber genau wie skizziert hättest, sag Bescheid.
  • source ist mit drin, damit man sieht, woher der jüngste Stand kommt.

Verifikation (echte Postgres-Instanz, Produktivschema, bewusst lückenhafte Daten)

Vier Records über den August, jede Größe zuletzt zu einem anderen Zeitpunkt gesehen:

  • Jeder gelieferte Zeitstempel gegen ein separates max(ts) je Spalte gegengeprüft — soc → 20.08., Packspannung → 10.08., Odometer → 25.08., GPS → 10.08., Zellen → 01.08. Alle stimmen exakt.
  • Eigentum wirkt pro Feld: Alice ist erst seit dem 05.08. Halterin und bekommt die Werte vom 01.08. (Höhe, Zellen, Temperaturen, Reichweite, Ladezustand, Kurs) gar nicht erst zu sehen — die Zeiträume aus #5 greifen also auch hier.
  • ?flat=1 liefert unverändert den einen Record; Fahrzeug ohne Daten → 404.
  • Selbsttests grün (Abfrage deckt jedes Feld ab, genau ein Treffer je Feld, Sichtbarkeitsbedingung in jeder Unterabfrage, GPS verlangt lat und lon gemeinsam, Falten der Antwort).
  • Live: /health ok, /latest und /latest?flat=1 weiterhin 401 ohne Token, Sandbox-Doku und Dashboard auf neuem Stand, Client-JS der ausgelieferten Seite parst sauber. Testdatenbank wieder entfernt.
**Umgesetzt** in `e5ea822` (release) — CI grün, deployt, live verifiziert. Genau der Punkt, den du beschreibst — hier an echten Daten, wie es **vorher** aussah. Der neueste Record war der vom 25.08., und der führte nur den Kilometerstand: ``` ?flat=1 -> ts=2026-08-25 soc=70 pack_voltage=null odometer=null ``` Formal korrekt, praktisch fast nutzlos: `.inx` führt keinen SoC, `cache_data` keine Zellspannungen, also hat jeder einzelne Record Löcher. ### Jetzt: je Messgröße ihr letzter bekannter Wert, mit eigenem Zeitstempel ```json { "soc": { "ts": "2026-08-20T10:00:00.000Z", "value": 70 }, "pack_voltage": { "ts": "2026-08-10T10:00:00.000Z", "value": 373.6 }, "odometer": { "ts": "2026-08-25T10:00:00.000Z", "value": 1500 }, "gps": { "ts": "2026-08-10T10:00:00.000Z", "lat": 51.5, "lon": 7.4 }, "cells": { "ts": "2026-08-01T10:00:00.000Z", "value": [3.7, 3.8] }, "source": { "ts": "2026-08-25T10:00:00.000Z", "value": "json" } } ``` Skalare als `{ts, value}` wie in deinem Beispiel, **GPS** als `{ts, lat, lon}` — lat und lon kommen dabei garantiert aus *demselben* Record, sonst zeigt die Position auf einen Ort, an dem das Auto nie war. Messgrößen, für die es überhaupt keinen Wert gibt, fehlen im Objekt (statt als `null` dazustehen). Dabei sind auch Höhe, Kurs, Gang, Ladezustand, Zellen, Temperaturen und die Quelle. Der eigene Zeitstempel je Größe ist dabei der eigentliche Gewinn: ein Wert von vor drei Tagen ist als solcher erkennbar, statt sich als aktuell auszugeben. Im Dashboard steht deshalb hinter jedem Wert sein Alter („vor 3 h"). ### Bullet-proof heißt auch: es muss schnell bleiben Eine Aggregation über die ganze Tabelle wäre pro Abfrage ein Full Scan gewesen. Stattdessen läuft je Feld eine Unterabfrage `ORDER BY ts DESC LIMIT 1`, die den Index `(vehicle_id, ts DESC)` nutzt und **beim ersten Treffer abbricht** — alle zusammen als ein `UNION ALL`, also ein Roundtrip. Per `EXPLAIN` gegengeprüft: ``` Limit -> Index Scan using telemetry_vehicle_ts_idx on telemetry Index Cond: (vehicle_id = 1) Filter: (soc IS NOT NULL) ``` Die Laufzeit hängt damit an der Anzahl Felder, nicht an der Anzahl Records. ### Entscheidungen (proceed-with-default) - **Die neue Form ist der Default**, die alte bleibt über **`?flat=1`** erreichbar. Falls irgendwo noch ein Client auf dem flachen Record sitzt, kippt ihm damit nichts weg. - **Zeitstempel bleiben ISO 8601 in UTC** (`2026-08-20T10:00:00.000Z`) statt `yyyy-mm-dd HH:mm:ss` aus deinem Beispiel — so wie überall sonst in der API, und mit Zeitzone. Wenn du das Format lieber genau wie skizziert hättest, sag Bescheid. - **`source` ist mit drin**, damit man sieht, woher der jüngste Stand kommt. ### Verifikation (echte Postgres-Instanz, Produktivschema, bewusst lückenhafte Daten) Vier Records über den August, jede Größe zuletzt zu einem *anderen* Zeitpunkt gesehen: - Jeder gelieferte Zeitstempel gegen ein separates `max(ts)` je Spalte gegengeprüft — soc → 20.08., Packspannung → 10.08., Odometer → 25.08., GPS → 10.08., Zellen → 01.08. **Alle stimmen exakt.** - **Eigentum wirkt pro Feld:** Alice ist erst seit dem 05.08. Halterin und bekommt die Werte vom 01.08. (Höhe, Zellen, Temperaturen, Reichweite, Ladezustand, Kurs) gar nicht erst zu sehen — die Zeiträume aus #5 greifen also auch hier. - `?flat=1` liefert unverändert den einen Record; Fahrzeug ohne Daten → 404. - Selbsttests grün (Abfrage deckt jedes Feld ab, genau ein Treffer je Feld, Sichtbarkeitsbedingung in *jeder* Unterabfrage, GPS verlangt lat und lon gemeinsam, Falten der Antwort). - Live: `/health` ok, `/latest` und `/latest?flat=1` weiterhin 401 ohne Token, Sandbox-Doku und Dashboard auf neuem Stand, Client-JS der ausgelieferten Seite parst sauber. Testdatenbank wieder entfernt.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
aiways/Backend#6
No description provided.