Cleaning
The cleaning plan, the tasks actually done and the photographs proving it — three collections, and what a missing record does not prove.
Cleaning in the app is a plan and its proof. The restaurant writes down what has to be cleaned, where and how often; staff tick the tasks off as they do them; some of those tasks demand a photograph. Three collections, one per layer, and the join between them is where most of the work is.
| Collection | Scope | What a record is |
|---|---|---|
cleaning-tasks | cleaning-tasks:read | A line of the plan: what must be cleaned, where, how often, whether it needs a photograph. |
cleaning-task-records | cleaning-task-records:read | A task actually done — when, and by whom. |
cleaning-task-pictures | cleaning-task-pictures:read | The photograph proving it was. Carries a file. |
All three share the snapshot envelope: id, deleted,
capturedAt, receivedAt, sequence and the rest sit beside the data
described below.
cleaning-tasks
The plan itself. These change rarely — a restaurant writes its plan once and edits it when the kitchen changes — so this is the collection you walk least often and cache longest.
| Field | Type | Meaning |
|---|---|---|
index | integer, required | The task's position in the plan, as the app orders it. |
name | string, required | What has to be cleaned, in the restaurant's words. |
description | string | null | Longer instructions, when someone wrote them. |
areaId | string | null | The area it belongs to. null on a restaurant that does not split itself into areas. |
recurrence | integer, required | How often the task comes round, as a plain integer. |
requirePhotoProof | boolean | null | true when the task demands a photograph. null means the app never set it. |
isDeleted | boolean, required | The app's own soft delete. Read the note below. |
{
"id": "c9a4f7e2-1b83-4d05-a6f1-90e3b7c4d218",
"collection": "cleaning-tasks",
"deleted": false,
"capturedAt": "1789601204773",
"receivedAt": "1789601399042",
"sequence": "1",
"data": {
"index": 3,
"name": "Nettoyage de la trancheuse",
"description": "Démonter la lame, dégraisser, désinfecter.",
"areaId": "7d2c0b61-4e8a-49f3-9c27-5a1b8e60d3f4",
"recurrence": 1,
"requirePhotoProof": true,
"isDeleted": false
}
}
recurrence carries no unit. It is an integer and the record does not say
what it counts. Show it as the app shows it rather than rendering "every 1
days" from a guess.
isDeleted is not the envelope's deleted. A task the restaurant removed
from its plan comes back with isDeleted: true and deleted: false: it is
still a live record, describing a line that no longer applies. Filter on both,
and keep the deleted ones around — old records still point at them.
cleaning-task-records
A task done. Small on purpose: which task, when, who.
| Field | Type | Meaning |
|---|---|---|
timestamp | string, required | When the task was done, epoch milliseconds as a string. |
cleaningTaskId | string, required | The cleaning-tasks record it satisfies. |
userId | string | null | The staff member who did it, when the app recorded one. |
{
"id": "5f8b3d16-2c90-4a77-b4e8-31d0a9c65f7b",
"collection": "cleaning-task-records",
"deleted": false,
"capturedAt": "1789775012558",
"receivedAt": "1789775204910",
"sequence": "2",
"data": {
"timestamp": "1789774800000",
"cleaningTaskId": "c9a4f7e2-1b83-4d05-a6f1-90e3b7c4d218",
"userId": "2774953d-8d9b-4a68-8ec4-1209edd90777"
}
}
cleaningTaskId is required, so every record has a task. The reverse does not
hold: a task can sit in the plan with no record against it for weeks, and that
is the normal state of a weekly task on a Tuesday.
cleaning-task-pictures
The photograph. It carries a file, which makes it the one collection here you cannot consume from the records endpoint alone.
| Field | Type | Meaning |
|---|---|---|
timestamp | string, required | When the picture was taken, epoch milliseconds as a string. |
cleaningTaskRecordId | string | null | The cleaning-task-records record it proves. Nullable — see below. |
part | number | null | Undocumented, and a decimal rather than a counter. Carry it through rather than interpreting it — the same caveat as on traceability labels. |
groupId | string | null | Ties those pictures together. Records sharing a groupId belong to the same proof. |
asset | object, required | Names the file: objectKey and status always, plus contentType, byteLength and sha256 once it is uploaded. |
{
"id": "2d71c8a9-6e34-4b0f-8517-ac93e25d0b46",
"collection": "cleaning-task-pictures",
"deleted": false,
"capturedAt": "1789775118330",
"receivedAt": "1789775301774",
"sequence": "3",
"data": {
"timestamp": "1789774860000",
"cleaningTaskRecordId": "5f8b3d16-2c90-4a77-b4e8-31d0a9c65f7b",
"part": 1,
"groupId": "a03e5c88-71bd-4f92-b6d4-2e8f10c73a5b",
"asset": {
"objectKey": "cleaning-task-pictures/2d71c8a9/1.jpg",
"status": "uploaded",
"contentType": "image/jpeg",
"byteLength": 812043,
"sha256": "9f2a…"
}
}
}
The bytes are a second call, against the record's own id:
GET /v1/restaurants/{restaurantId}/collections/cleaning-task-pictures/records/{recordId}/assets
It answers with a list of signed URLs, each valid for fifteen minutes, each
carrying its own authorisation so the download needs no header. Fetch the bytes
now, store the bytes rather than the link, and call the endpoint again when you
need a fresh URL. The rules — what appears, what sha256 is for, why
contentType is worth reading — are on
the collections page, and they are the same here.
asset.status tells you whether there are bytes to fetch. It is pending
or uploaded. A pending asset is a photograph the device has announced and
not yet finished sending: the record is real, the file is not, and the assets
endpoint answers without it. Read the status before you treat an empty asset
list as a photograph that was never taken.
cleaningTaskRecordId is nullable. A picture can exist without pointing at
a record, so a join that assumes it is always there drops photographs on the
floor. Count them separately rather than discarding them silently.
Joining the three
The shape you almost certainly want is one row per task done, with its plan entry and its photographs:
cleaning-task-records.cleaningTaskId→cleaning-tasks.idcleaning-task-pictures.cleaningTaskRecordId→cleaning-task-records.id
Three walks, joined on your side. There is no endpoint that does it for you, and there is no filter by task or by date — you walk each collection whole and reconcile, as the envelope page describes.
Order on capturedAt, not receivedAt: a tablet in a cold room uploads when it
finds signal, and a picture taken at 09:00 can arrive at 14:00 — after the
record it belongs to, or before it.
What a missing record does not mean
A plan entry with no matching record is not proof the task was skipped. It
may have been done on a device that has not uploaded yet, or done before
capture was switched on for that restaurant. The same goes for a task marked
requirePhotoProof: true with no picture against it: the photograph may be
mid-upload, and an asset that has not finished uploading is absent rather than
broken.
So do not print a cleaning completion rate from this API and call it compliance. What you can say honestly is what arrived, and when — see the completeness note before you put a number in front of anyone.