Api for ollebo.com
- Add new maps
- Update maps
- Get maps
Build in docker and run as lamda function in AWS. All actions are trigger by api-gateways calls
Copy the docker-compose.yaml fil from the cp folder to the fins folder (In the fins-manager repo) (ORe what you called it)
In the fins folder (ORe what you called it) The default image is baes to run ad a small docker image in lamda. And for lcoal develoment its beste to use the docker compose image. You also need a postgress database server to store the commands.
Build
docker-compose build
Run
docker compose run api /bin/bash
To deploy build the aws image and push to the registry. then update lamda to use th new image.
./deploy.sh
Will build and push the image
Use the following endpints
https://vystletavc.execute-api.eu-north-1.amazonaws.com/v1/map/ https://api.ollebo.com
This sill trigger the creating of map and the correct path to the file in the s3 bucket need to be correct
--> PUT
{
"name": "grangesberg",
"tags": ["Country", "animals", "road"],
"status": "uploaded",
"access": "public",
"originFile": "users/543524134233/geotiff/odm_orthophoto.original.tif",
"mapid": "12345-12345-12345-12345",
"accessid": "1234-1234-1234-1234",
"action" : "makingMap"
}
response
{
"data": "accepted",
"id": "1"
}
--> POST
{
"id": "3",
"name": "viksjo",
"tags": ["Country", "animals", "road"],
"status": "active",
"url":"none",
"location": [17.822057235629273, 59.413808385194216],
"area": {
"type": "LineString",
"coordinates": [
[17.822057235629273, 59.413808385194216],
[17.825134236343995, 59.41017389364913]
]
}
}
response
´´´ [ { "name": "viksjo", "access": "public", "status": "active", "action": "makingMap", "tags": [ "Country", "animals", "road" ], "location": "Point(17.822057235629273 59.413808385194216)" } ] ´´´
--> GET
URL /map/?lon=17.825134&lat=59.410173
Response
Missions carry a time-series of events (drone telemetry, sensor readings,
images…). Producers PUT events in; consumers read them back over HTTP —
either a one-shot recent history or a live Server-Sent Events (SSE) stream.
The API is the only gateway: producers/consumers talk HTTP, and the API fans
events through NATS JetStream internally. Events are also persisted to the
Postgres/TimescaleDB mission_data hypertable so history survives restarts.
PUT /event/<key> GET /event/<key>/stream (SSE)
│ ▲
▼ │ backfill (Postgres) + live (NATS)
resolve mission ──► Postgres ─────┘
│ (mission_data)
▼
NATS JetStream
public → events.public.<id>
private → events.private.<space_id>.<id>
<key>in the path may be the mission key or its id — both resolve to the same mission (seeGET /mission/validate/<key>).- Events are stored and streamed keyed by the mission's canonical id.
- The stream subject is split by visibility so private traffic is isolated
server-side. The
events.>JetStream stream is (re)bound automatically on every deploy by thenats-initJob (chart/templates/nats-init-job.yaml).
Read visibility is driven by the mission's is_public column
(BOOLEAN NOT NULL DEFAULT FALSE — a mission is public only when explicitly set).
| Mission | GET /recent & /stream |
|---|---|
public (is_public = true) |
open, no auth |
private (is_public = false, the default) |
requires Authorization: Bearer <jwt> whose groups claim contains the mission's space_id; otherwise 403 (bad/expired token → 401) |
This is the same JWT-groups-vs-space_id model used by the maps read
endpoints (GET /maps/, POST /search/).
The Bearer token is a Keycloak JWT (verified via JWKS). Its groups claim
carries the caller's Keycloak group ids, which equal the space.id UUIDs
(dw sets space.id = the Keycloak group UUID) — so a caller can read a private
mission/map exactly when that resource's space_id is one of their group UUIDs.
Non-UUID groups entries are ignored. Verification is enabled by the
JWT_JWKS_URL / JWT_ISSUER / JWT_AUDIENCE env vars (chart jwt.* values);
with JWT_JWKS_URL unset, auth is disabled and only public data is served.
| Method | Path | Purpose |
|---|---|---|
PUT |
/event/<key> |
Append one event (see Event schema). Returns {"data":"stored"}. |
GET |
/event/<key>/recent?minutes=15 |
Last N minutes (1–60) from Postgres, ascending. |
GET |
/event/<key>/stream |
SSE: backfill → ready → live frames, : ping keepalive every 15s. |
PUT |
/mission/<key>/picture/<picture_id> |
Upload the image bytes for a picture event (phase 2). Body is the raw image. Returns {"data":"uploaded","url":…}. |
GET |
/mission/<key>/picture/<picture_id> |
Retrieve the stored image bytes back through the API. |
GET |
/mission/validate/<key> |
Resolve a key → {valid, mission_id, name}. |
Every event carries a type. The endpoint understands four canonical types
(plus two legacy aliases kept working); an unknown type is still stored and
logged, never rejected. Mapped columns land in mission_data; per-type detail
rides in the jsonData JSONB and round-trips verbatim on the live stream.
type |
Meaning | Key fields |
|---|---|---|
location (alias telemetry) |
GPS position of a drone/boat/rover | geopoint [lon,lat], x/y/z, device |
measurement (alias temperature) |
A generic sensor reading — temperature, humidity, solar exposure, anything | value in data; jsonData.{kind,unit} (temperature also fills temp/humidity) |
picture |
A photo, in two phases (announce → upload bytes later) | jsonData.{picture_id,status}; img becomes the stored URL once uploaded |
alert |
A detection ("person detected", etc.) | jsonData.{kind,severity,message} |
Examples (each is a body for PUT /event/<key>):
curl -X PUT https://api.ollebo.com/event/<mission-key> \
-H "Content-Type: application/json" \
-d '{
"type": "telemetry",
"geopoint": [18.0686, 59.3293],
"z": 42.0,
"temp": 21.4,
"device": "drone-01",
"jsonData": {"battery": 98, "heading": 90, "speed": 8.0}
}'
# -> {"data":"stored"}geopoint is [longitude, latitude] (PostGIS order); z is a free numeric
axis, commonly altitude. Any keys you put in jsonData (or extra top-level
keys) come back verbatim in the payload of each live SSE frame, so you can
carry app-specific fields.
backfill/recent rows are DB shape; live frames are
{"timestamp", "payload": <the exact body you PUT>}.
curl -N https://api.ollebo.com/event/<mission-key>/stream
# event: backfill\ndata: {...}\n\n (replayed history, last 15 min)
# event: ready\ndata: {}\n\n (caught up; live follows)
# event: live\ndata: {"timestamp":"…","payload":{…}}\n\nBrowser / frontend:
const es = new EventSource(`https://api.ollebo.com/event/${key}/stream`);
es.addEventListener("backfill", e => render(JSON.parse(e.data))); // history
es.addEventListener("ready", () => console.log("live"));
es.addEventListener("live", e => render(JSON.parse(e.data).payload));
// NOTE: EventSource cannot set an Authorization header — for PRIVATE missions
// use a fetch()-based SSE reader (or an ?access_token=… scheme) instead.Private mission with a token:
curl -N https://api.ollebo.com/event/<private-key>/stream \
-H "Authorization: Bearer $JWT" # JWT.groups must contain the mission space_idGET /event/public/stream is a single SSE feed of every public mission (no
auth). It's live-only (no backfill); each live frame's data includes
mission_id so you know which mission each event came from:
new EventSource("https://api.ollebo.com/event/public/stream")
.addEventListener("live", e => {
const { mission_id, timestamp, payload } = JSON.parse(e.data);
// payload.device, payload.geopoint, payload.jsonData.{assetType,altitude,…}
});A drone announces a photo the instant it takes it, but the bytes may arrive much later (poor connectivity → upload when reconnected). So pictures are two phases:
- Announce —
PUT /event/<key>withtype:"picture"and apicture_idinjsonData(status defaults topending). This is a normal event: it lands inmission_dataand streams live like any other. - Upload the bytes — later,
PUT /mission/<key>/picture/<picture_id>with the raw image as the body. The API stores it in object storage, flips the matching row tostatus:"uploaded"and setsimgto the stored URL, and re-publishes apicture/uploadedframe so live subscribers see the transition. (If the bytes arrive before the announce event, a fresh uploaded row is created instead.)
Retrieve the image any time with GET /mission/<key>/picture/<picture_id> — it
streams the bytes back through the API (works regardless of bucket ACL).
# 1. announce
curl -X PUT https://api.ollebo.com/event/<key> -H 'Content-Type: application/json' \
-d '{"type":"picture","geopoint":[18.0686,59.3293],"jsonData":{"picture_id":"p-1","status":"pending"}}'
# 2. upload the bytes (later)
curl -X PUT --data-binary @photo.jpg -H 'Content-Type: image/jpeg' \
https://api.ollebo.com/mission/<key>/picture/p-1
# -> {"data":"uploaded","url":"https://hel1.your-objectstorage.com/map-storage/<realm>/missions/<id>/pictures/p-1"}
# 3. fetch it back
curl https://api.ollebo.com/mission/<key>/picture/p-1 --output got.jpgObject storage. Bytes go to the same S3-compatible backend as the dw app
(Hetzner Object Storage in prod), using the shared AWS_* conventions and
credentials from the ollebo k8s Secret. Objects are keyed
<realm>/missions/<mission_id>/pictures/<picture_id> in the map-storage
bucket. Config (chart storage.* / env): AWS_ENDPOINT, AWS_BUCKET,
AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY. Locally,
docker-compose runs MinIO as a stand-in (bucket auto-created) so the whole
flow works offline.
code/simulator.py pushes realistic dummy traffic so the GUI has live data to
render. It drives several moving assets of different types (drone / boat /
rover) at different locations plus a fixed measurement sensor, and emits every
event type: location telemetry, measurement (temperature/humidity/solar),
picture (announced then uploaded a few ticks later, simulating bad internet),
and alert detections — all into the public mission. It runs as a pod beside the
API (chart/templates/simulator.yaml, gated by simulator.enabled, on by
default) and can also be run locally:
API_BASE=https://api.ollebo.com \
MISSION_KEY=a744e376-b35c-43ce-8c61-b82d8bb9f9d0 \
python3 code/simulator.py
# knobs (env): INTERVAL_SECONDS, DRONES, BOATS, ROVERS, TEMP_SENSORS, TEMP_EVERY,
# MEASUREMENT_KINDS, PICTURE_EVERY, PICTURE_UPLOAD_DELAY_TICKS,
# ALERT_EVERY, MAX_TICKSMoving assets emit jsonData.{assetType,name,model,altitude,heading,speed, battery,visibility} so the ollebo-maps consumer renders them directly. Turn it
off in prod once real data flows: set simulator.enabled: false in
chart/values.yaml and redeploy.
curl -s https://api.ollebo.com/version
# {"name":"ollebo-api","version":"<git-sha>"} ("dev" for local builds)The full OpenAPI spec (all maps, missions, and event endpoints, with schemas
and auth) is served live: https://api.ollebo.com/doc (raw JSON at
/doc/openapi.json).
test/e2e_mission_stream.py mocks a drone flight into the public mission and
verifies ingest + backfill + live stream + the private auth gate (stdlib only):
API_BASE=https://api.ollebo.com python3 test/e2e_mission_stream.pyOr via the /e2e-mission-stream Claude skill.
test/demo_features.py fires one of each event type into a mission so you can
see the features live — a measurement, a person detected alert, and a
two-phase picture that pulls a real cat photo off the internet
(cataas.com), uploads it, and retrieves it back byte-for-byte (stdlib only):
python3 test/demo_features.py
# watch it land, in another terminal:
curl -N http://localhost:8888/event/<MISSION_KEY>/stream
# knobs (env): API_BASE, MISSION_KEY, CAT_URL (falls back to a generated image offline)