Patrol Sync¶
Recommendations for synchronizing patrol-related data for offline use. Aimed at mobile clients that want to minimize downloads and the number of checks to detect changes.
This guide covers patrol types (the catalog of types, like “Foot”, “Vehicle”), the tracked-by schema (who can be reported as segment leader), the patrol configuration (teams and the asset / member rosters), and patrols and patrol segments (the actual patrol instances and their segments). Patrol types — the list, the detail, the schema routes and the revision history — the tracked-by schema, the patrol configuration routes and segment list/detail support conditional GET; patrol list/detail do not. The patrol types list and detail support both ETag / If-None-Match and Last-Modified / If-Modified-Since; the patrol type schema routes, the revision history, the tracked-by schema, the patrol configuration routes and segment list/detail support ETag / If-None-Match only.
Note: Two kinds of ETag appear below. Shareable ETags are derived from data that is identical for every caller — the patrol types list and detail, the raw schema routes (pre_render absent or false), the revision history, segment list and detail, and the patrol configuration (config/default) — so a stored value can be reused across users and profiles. Per-user ETags are derived from a response that depends on the caller: the tracked-by schema (salted with the requesting user’s ID and the user-profile header when present, because the set of selectable leaders depends on the caller’s permissions), any schema route called with pre_render=true (rendered $ref choices are resolved against the caller’s permissions) and the resolved patrol configuration (config/default/resolved, whose rosters are scoped to the caller). The segment and patrol configuration ETags are unsalted hashes of the served body; the resolved one is per-user only because its body is. Cache per-user ETags per authenticated identity; never share them between users or profiles.
Note: When you send both If-None-Match and If-Modified-Since, the server evaluates If-None-Match and ignores If-Modified-Since (RFC 9110 §13.2.2). Prefer If-None-Match — it detects changes that a one-second-resolution timestamp can miss.
Note: The web UI currently supports only one patrol segment per patrol. The API allows multiple segments per patrol, but clients that aim for parity with the web UI should treat one segment per patrol as the supported case.
Patrol Types (catalog for offline)¶
Patrol types define the kinds of patrols (e.g. Foot, Vehicle). Sync the list for offline so the app can create patrols and show type metadata (display name, icon, priority, etc.).
Recommended: List with conditional GET¶
Use the patrol types list as the single source for the catalog. The response ETag reflects any change to the returned list. Use If-None-Match to avoid re-downloading when nothing has changed.
The list also sends Last-Modified, and If-Modified-Since is supported. Last-Modified is the newest updated_at across all patrol types — including deactivated ones, which the default list hides — so deactivating a type still advances it. It can therefore report “modified” when your filtered view is in fact unchanged; If-None-Match is the more precise check and is preferred.
Endpoint
GET /api/v1.0/activity/patrols/types
Query parameters
Parameter |
Description |
|---|---|
|
When |
|
When |
|
ISO 8601 date/time. Return only types whose |
Each distinct combination of query parameters has its own ETag — on this list include_inactive, include_schema and updated_since; on the schema routes pre_render, plus s_format when pre_render=true; on the revision history page and page_size — so store the ETag alongside the exact request you made and replay that exact request.
Optimizing “do I need to download?”
Store the ETag from the last successful list response, keyed by the query parameters you sent.
On the next sync, repeat the same URL and query parameters and send If-None-Match: your stored ETag value.
If the server responds 304 Not Modified, nothing changed — skip downloading the body and keep your cached patrol types.
If the server responds 200 OK, replace your cache with the new response and store the new ETag.
This minimizes both checks (one conditional GET) and downloads (no body when unchanged).
Optimizing full download
Initial / full sync: Call the list once. The list is small and metadata-only, so one request is enough. Add
include_inactive=trueif your client needs to display or reconcile deactivated types (e.g. to label historical patrols).
Single patrol type (detail)¶
GET /api/v1.0/activity/patrols/types/<patrol_type_id_or_value>
The path segment accepts either the patrol type UUID or its unique value slug — one route serves both, matching the v2 event-types endpoint. (The route regex is DRF’s default [^/.]+, so a value containing / or . is not addressable here; the same constraint applies to v2.)
Supports conditional GET via ETag / If-None-Match and via Last-Modified / If-Modified-Since, where Last-Modified is that type’s own updated_at. Use when you need to refresh or check a single type; for full catalog sync, the list endpoint with conditional GET is simpler.
The include_schema parameter is supported here as well as on the list, with the same semantics: the type’s stored schema document, which is the empty envelope until one is written. include_schema=true yields a different ETag than the no-param request, so the two response shapes never share a cache entry. The include_schema=true ETag also covers the schema content itself, so editing only a type’s schema invalidates it. Note that the default, schema-less ETag is not insulated from schema writes in general: it covers updated_at, which an ordinary PUT/PATCH of the schema bumps. It stays put only for a schema write that does not touch updated_at.
Like the list, the detail route hides deactivated types by default: a GET for a type with is_active=false returns 404 Not Found unless you pass ?include_inactive=true. The write methods (PUT, PATCH, DELETE) are unaffected and always reach the type, deactivated or not.
Patrol type response fields¶
Field |
Writable |
Description |
|---|---|---|
|
No |
UUID primary key. |
|
Yes |
Unique string identifier for this type (e.g. |
|
Yes |
Human-readable label. |
|
No |
Sort order. Nullable. Read-only here; reorder via the rank endpoint. |
|
Yes |
Raw stored icon identifier. Empty string when no explicit icon has been set. |
|
No |
Resolved icon identifier. Equals |
|
Yes |
Default priority for patrols of this type. |
|
Yes |
Whether this type is available for new patrols. |
|
Yes |
The type’s |
Patrol type write endpoints¶
Method |
URL |
Description |
|---|---|---|
|
|
Create a patrol type. |
|
|
Replace a patrol type. |
|
|
Partially update a patrol type. |
|
|
Delete a patrol type. |
<id> in the table above also accepts the type’s unique value slug in place of the UUID — the same dual id/value lookup as the GET detail endpoint applies to PUT, PATCH, and DELETE.
value must be unique per tenant. Attempting to create a duplicate returns 400 Bad Request. Required permissions: add_patroltype, change_patroltype, or delete_patroltype respectively.
Reordering: POST .../patrols/types/<id_or_value>/rank reorders the catalog (see Ranking). A rank write rewrites the target’s ordernum and advances its updated_at; when the write triggers a rebalance, every renumbered sibling row gets a fresh updated_at too. Both fields are in the ETag field set, so If-None-Match observes any reorder, Last-Modified / If-Modified-Since moves forward for the whole catalog, and updated_since returns every row whose position changed. If-None-Match remains the recommended validator: it detects changes a one-second-resolution timestamp can miss.
Writing a schema
schema is an optional body field on POST, PUT, and PATCH. It takes the same {"json": ..., "ui": ...} document shape as the v2 event-type schema.
Omitting it is a no-op. On
POSTthe new type gets the empty schema envelope; onPUTandPATCHthe stored document is left exactly as it was. Clients that never sendschemaare unaffected by this field.Sending it replaces the stored document wholesale — it is not merged into the existing one.
The document is validated against the schema meta-schema (JSON Schema draft 2020-12;
json.$schemamust behttps://json-schema.org/draft/2020-12/schema), and everyuientry that names asectionmust reference a key that exists inui.sections. A document that fails any of these checks returns 400 Bad Request and nothing is saved."schema": nullis rejected with 400 Bad Request; omit the key instead.The stored document may differ from what you sent. Accepted documents are normalized on save:
additionalProperties: falsebecomesunevaluatedProperties: false, andunevaluatedItems: falseis added to collection-field arrays that omit it. The write response does not show this — read the type back withGET ?include_schema=trueto see exactly what was stored.Write responses never include
schema.POST,PUT, andPATCHresponses omit the key entirely, even when the request wrote one;include_schemais honoured onGET(list and detail) only. Follow a write with a conditionalGET ?include_schema=trueif you need to confirm the stored document.The served document is the raw stored one, exactly as saved. It is not a rendered schema: choice lists and
$refare not expanded (the same distinction as the v2 event-typeinclude_schema). Use the schema endpoints below withpre_render=truewhen you need the rendered form.Envelope-root extras are rejected.
readonly,icon_id, andimage_urlalongsidejson/uireturn 400 Bad Request under theschemaerror key: they are event-type-only accommodations, and for a patrol typeicon_id/image_urlare read-only derived fields of the type itself.auto-generateis accepted at the root (it is the cross-owner auto-generation marker) but no patrol-type auto-generation exists today, so it is stored and served with no effect.
List all patrol type schemas¶
GET /api/v1.0/activity/patrols/types/schemas
Returns one entry per patrol type — the bulk companion to the single-type endpoint below, and the endpoint to use for a full schema sync. Query parameter pre_render=true returns rendered schemas (choice lists expanded, $ref resolved); without it, schemas are raw. include_inactive and updated_since behave exactly as on the patrol types list.
The body is {"count": <n>, "results": [...]}, where each result carries value, status (success / partial / failure), schema, and — on anything but success — an errors array whose entries have a stable machine-readable code plus human-readable message and hints. A single unreadable document therefore never fails the batch: the response is 207 Multi-Status when any entry failed, and 200 OK only when every entry succeeded.
The response carries an ETag (no Last-Modified) and honours If-None-Match: a match answers 304 Not Modified with no body. Without pre_render the ETag is a digest of the stored documents, computed in the database alongside the served rows, so the validator and the body always describe the same state; a 304 saves the bandwidth and the body build, not the database read. It is shareable across users. With pre_render=true it is a hash of the rendered body, so it also moves when the data behind a $ref changes (a new choice value, a renamed subject); it is per-user and must not be shared. Only a 200 carries an ETag: a 207 batch has no validator, so re-request it unconditionally until every entry succeeds. pre_render, include_inactive and updated_since each yield their own ETag, and so does s_format when pre_render=true — replay the exact request.
Single patrol type schema¶
GET /api/v1.0/activity/patrols/types/<patrol_type_id_or_value>/schema
Returns the schema for one patrol type. Like the detail route, the path segment accepts either the UUID or the unique value slug, and a deactivated type returns 404 Not Found unless you pass ?include_inactive=true.
Query parameters
Parameter |
Description |
|---|---|
|
When |
|
Shape of expanded choice fields when |
The response body is the bare {"json": ..., "ui": ...} document — the same shape include_schema=true puts under the schema key on the list and detail routes. A patrol type that has never had a schema written to it serves the empty schema envelope. Note that pre_render=true also drops the document root’s unevaluatedProperties: false, so that data captured under an older revision of the schema still validates.
Only the $ref sources the requesting user is allowed to read are resolved. An unresolvable reference does not fail the request — the property renders as an empty schema, so validate rendered content rather than relying on the status code alone.
If the stored document cannot be served at all (it is empty, or it is not a {json, ui} object), the response is 422 Unprocessable Entity with a bare {"errors": [...]} body using the same error entries as the bulk endpoint.
The response carries an ETag (no Last-Modified): a hash of the served document, shareable across users without pre_render and per-user with pre_render=true, and 304 Not Modified on an If-None-Match match. A 422 carries no validator. include_inactive does not change the body, so it does not change the ETag.
Patrol type updates (revision history)¶
GET /api/v1.0/activity/patrols/types/<patrol_type_id_or_value>/updates
Returns the revision history for one patrol type, newest first and paginated (count / next / previous / results, with page and page_size) — unlike the patrol types list, which is a bare array. Each entry carries time, action, user, updated_fields, and sequence; a create reports no updated_fields (it is a full snapshot, not a diff), and every later entry names only the fields that changed. Use when you need to show or sync change history; not required for basic catalog + schema sync. Each page carries an ETag (no Last-Modified) — a hash of the page as served, so it moves when a revision is added and when a listed user’s display details change — and If-None-Match is honoured with 304 Not Modified. The ETag is shareable across users; each page / page_size combination has its own.
The path segment accepts the UUID or the value slug, as the detail route does, and that route’s deactivated-type rule applies here too: 404 unless ?include_inactive=true. Requires view_patroltype or view_patrol, the same as the read endpoints.
Tracked-by schema (segment leaders / “reported by”)¶
When creating or editing patrol segments, clients often need the list of subjects (and optionally users) that can be selected as segment leader (“tracked by”). This is provided by the tracked-by schema endpoint. It has an ETag so you can avoid re-downloading when the set of possible leaders has not changed.
Endpoint
GET /api/v1.0/activity/patrols/trackedby
Returns metadata describing the schema for “tracked by” (e.g. subject list). Requires view_patrol; the subjects offered are then limited to the ones the caller can see. The ETag is derived from the set of subjects available to the user. Use If-None-Match for conditional GET; 304 Not Modified means the schema/options are unchanged.
Optimizing “do I need to download?”
Store the ETag from the last successful response.
On the next sync, send If-None-Match: your stored ETag value.
304 — keep your cached schema; 200 — replace cache and store the new ETag.
Patrol configuration (teams and rosters)¶
The teams, assets and members a patrol form offers come from the tenant’s patrol configuration. See Patrol configuration for the response shapes, the preview-feature gate and the permission.
Endpoints
GET /api/v1.0/activity/patrols/config/default
GET /api/v1.0/activity/patrols/config/default/resolved
Both routes carry an ETag (no Last-Modified) and honour If-None-Match with 304 Not Modified. The config/default ETag is shareable across users. The config/default/resolved ETag is per-user: its rosters are scoped to the caller’s subject permissions and user-profile, so cache it per identity. Besides membership, name and activity changes to the roster subjects and changes to the teams, the resolved ETag also moves when a roster subject’s status icon changes (for example its device’s radio state).
Patrols (list and detail)¶
Patrols are the actual patrol instances (with one or more segments). The list and detail endpoints do not support ETag or Last-Modified. Use query parameters to limit results and paginate; for incremental-style sync you can use the filter with a date_range to fetch patrols overlapping a time window.
Endpoints
GET /api/v1.0/activity/patrols
GET /api/v1.0/activity/patrols/<patrol_id>
List query parameters
Parameter |
Description |
|---|---|
|
JSON object for advanced filtering. Example: |
|
If |
|
Filter by state(s). Allowed values depend on |
Responses are paginated. There is no ETag on the list or detail; you cannot use conditional GET to skip re-downloading. To minimize data transfer, use filter (e.g. date_range) and pagination, and store the last sync time or last requested range for incremental pulls.
List order
The default (page-number) list is ordered by state group first, then by serial_number descending (newest first) within each group:
opendonecancelledany other state
The order is deterministic, so walking the pages returns each patrol exactly once, as long as the data does not change while you page. Within a state group the serial order matches the order of the use_cursor=true mode, which orders the whole list by serial_number descending without grouping by state.
Patrols without a serial_number are not listed in either mode. They can still be fetched by UUID from the detail endpoint.
Single patrol
Detail returns one patrol by UUID. No ETag; refetch when you need the latest state.
Include flags (list and detail)
Both endpoints accept four flags that drop parts of the payload. Every flag defaults to true, so a request without them gets the full payload. This is unlike the events list, where include_notes and include_related_events default to false. The flags are the cheapest way to cut a patrol response down: a client that only shows the patrol list can pass all four as false.
Parameter |
When |
|---|---|
|
The top-level |
|
The |
|
The |
|
Each segment’s |
last_activity_at
Every patrol carries a read-only last_activity_at: the latest of the patrol’s own updated_at and the updated_at of each of its segments. Editing a segment does not change the patrol’s updated_at, so use last_activity_at to tell whether the patrol or any of its segments has changed. Linking events to a segment, or unlinking them, does not change last_activity_at.
Patrol request payloads (POST, PATCH, PUT)¶
Create patrol: POST /api/v1.0/activity/patrols
Update patrol: PATCH or PUT /api/v1.0/activity/patrols/<patrol_id>
Request body (JSON). All fields are optional on create; omit fields you are not changing on update.
Field |
Type |
Description |
|---|---|---|
|
string |
Patrol objective (text). Can be blank/null. |
|
integer |
Priority. Allowed values: |
|
string |
Patrol state: |
|
string |
Patrol title. Max 255 characters. Can be blank/null. |
|
array |
List of note objects. Each note: |
|
array |
List of segment objects (see Patrol segment request payloads below). When creating a patrol you can include segments in this array; do not send |
Validation: scheduled_start must be earlier than scheduled_end in any segment. The stored state follows from the patrol’s segments and can differ from the one you send; see Patrol state rules.
Patrol segments (list and detail)¶
Patrol segments are the legs of a patrol (each has a type, time range, leader, etc.). The web UI supports only one segment per patrol; the API may return or accept multiple segments per patrol, but the web UI does not. List and detail carry an ETag (no Last-Modified) and honour If-None-Match with 304 Not Modified.
Endpoints
GET /api/v1.0/activity/patrols/segments
GET /api/v1.0/activity/patrols/segments/<segment_id>
List is paginated. The ETag is shareable across users holding view_patrol. It is a hash of the body as served — absolute URLs included — so store it per base URL and replay the exact request. On the list, each include_pauses / page / page_size combination has its own ETag, and because every page carries the total count, every page’s ETag moves whenever a segment that list includes is created or deleted, on any page — a paused leg moves only the include_pauses=true variants. The ETag also moves when a linked event changes, when a revision author’s display name changes, and when the segment leader’s status icon changes. A 304 saves bandwidth, not server work — the body is still built to be hashed — so do not poll aggressively.
Segment–events
GET /api/v1.0/activity/patrols/segments/<patrol_segment_id>/events
Returns events linked to a patrol segment. No ETag.
Patrol segment request payloads (POST, PATCH, PUT)¶
Segments can be created or updated in two ways: nested in a patrol (in patrol_segments when posting to POST /patrols or PATCH/PUT /patrols/<id>) or standalone via the segments list.
Create segment (standalone): POST /api/v1.0/activity/patrols/segments
Update segment (standalone): PATCH or PUT /api/v1.0/activity/patrols/segments/<segment_id>
When nested in a patrol payload, omit patrol (the server sets it). When posting to the segments endpoint, patrol is required (patrol UUID).
Field |
Type |
Required (standalone) |
Description |
|---|---|---|---|
|
UUID |
Yes (standalone only) |
Patrol ID. Omit when segment is nested in a patrol body. Fixed after create: a different |
|
string |
No |
Patrol type |
|
object |
No |
Segment leader (who is tracked). Object: |
|
string |
No |
ISO 8601 date/time. Scheduled start. |
|
string |
No |
ISO 8601 date/time. Scheduled end. Must be after |
|
object |
No |
Actual patrol time. |
|
object |
No |
Start point. |
|
object |
No |
End point. Same shape as |
|
UUID |
No |
Include when updating an existing segment (nested or standalone). |
Read-only in responses (do not send when creating/updating): image_url, icon_id, events.
A standalone POST may carry a client-generated id, so an offline client can name the segment before it syncs. If a segment with that id already exists — typically a retried POST whose first response was lost — the request returns 409 Conflict with {"error_message": "..."}. Nothing is created and the existing segment is left unchanged. To change the existing segment, PATCH or PUT it. The same applies to POST /api/v1.0/activity/event/<event_id>/segments.
Patrol state rules¶
A patrol’s state depends on all of its legs (segments), whatever order they come back in. A leg is running when its time_range has a start_time and no end_time; it has ended when its end_time is in the past; it has a planned end when its end_time is in the future. A scheduled end is the separate scheduled_end field. A new or never-started leg sent with neither start_time nor end_time is stored with no time range and does not count as running; a leg that has started cannot have its start_time cleared. A leg counts as added or updated only when the write creates it or changes one of its own fields; resending a leg unchanged writes nothing, and changing only its members or assets does not count. A patrol is saved by any POST, PATCH or PUT on the patrol endpoints, with or without patrol_segments. A save that changes none of the patrol’s own fields (such as state, title, priority or objective) adds nothing to the patrol’s updates and by itself sends no realtime das.patrol.update; a leg, note or roster change in the same request still sends one. The auto-end check is a server task (periodically_maintain_patrol_state) that runs every minute.
# |
Given |
When |
Then |
|---|---|---|---|
1 |
An |
The auto-end check runs |
The patrol is |
2 |
An |
The auto-end check runs |
The patrol stays |
3 |
An |
A user ends it (the web UI closes all legs) |
The patrol is |
4 |
An |
A user cancels it |
The patrol is |
5 |
An |
The patrol is saved |
The patrol stays |
6 |
A |
A running leg is added or updated |
The patrol is |
7 |
A |
A leg with a planned end is added or updated |
The patrol stays |
8 |
A |
The patrol is saved |
The patrol is |
9 |
A |
A running leg is added or updated |
The patrol is |
10 |
A |
A leg with a planned end is added or updated |
The patrol stays |
11 |
A |
The patrol is saved |
The patrol stays |
12 |
Any patrol |
A leg is closed through the segment endpoints ( |
No change. Only the auto-end check or a patrol save ends a patrol. |
13 |
An |
The patrol is saved (for example, a |
The patrol is |
14 |
Any patrol |
One |
The patrol is |
15 |
A |
Only that leg’s |
The patrol stays |
Repairing patrols damaged by the old auto-end check¶
Before ERA-14077, the auto-end check saved multi-leg patrols every minute and could leave a patrol done while a leg was still running. Each of those saves wrote a patrol revision that patrol history never shows, up to about 1.37 million on one patrol. The repair_patrol_auto_end_damage management command cleans both up, once the fix is deployed:
It deletes patrol update revisions whose stored change holds nothing but
updated_at. Every other revision is kept: created, deleted and relation-deleted ones, and every update that records a field, including fields patrol history does not show (priority,objective,provenance,serial_number).It reopens every
donepatrol that still has a running leg (row 8), which writes a patrol revision and sendsdas.patrol.update. It never closes anopenpatrol (that is left to the auto-end check, which honours a leg’s scheduled end) and never changes acancelledone.
It only reports by default. Run it for one tenant first and read the counts it prints per tenant, then re-run with --apply:
python manage.py repair_patrol_auto_end_damage --tenant <domain-or-id>
python manage.py repair_patrol_auto_end_damage --tenant <domain-or-id> --apply
python manage.py repair_patrol_auto_end_damage --apply # every tenant
Revisions are deleted in batches of --batch-size (default 1000), each its own statement, so a run can be interrupted and started again. A tenant whose settings cannot be loaded is skipped and makes the command exit non-zero.
Patrol sub-resources (notes, files)¶
Patrol notes and files are nested under a patrol:
GET /api/v1.0/activity/patrols/<patrol_id>/notes
GET /api/v1.0/activity/patrols/<patrol_id>/notes/<note_id>
GET /api/v1.0/activity/patrols/<patrol_id>/files
GET /api/v1.0/activity/patrols/<patrol_id>/files/...
These endpoints do not expose ETag or Last-Modified. Refetch as needed.
Note request payloads: Create note with POST .../patrols/<patrol_id>/notes with body { "text": "..." }. Update with PATCH/PUT .../notes/<note_id>. The patrol is implied by the URL.
File request payloads: Create/update patrol files via the files endpoint; the payload typically includes usercontent_id, usercontent_type, and optionally comment, ordernum. See the API or schema for the full file upload format.
Permissions¶
How each /api/v1.0/activity/patrol* route is authorized. Permissions are activity.* codenames.
Routes |
Class |
Read (GET / HEAD / OPTIONS) |
Write |
|---|---|---|---|
|
|
|
POST |
|
|
|
Same |
|
|
|
Same |
|
|
Any authenticated caller; events are filtered to the categories the caller can read. No patrol permission is checked |
POST needs the |
|
|
|
POST |
|
|
— |
POST |
|
|
|
PUT |
|
|
|
— (read-only; other verbs answer |
|
|
|
— (POST answers |
Notes
*_patroleverywhere.PatrolObjectPermissionsalways checks thePatrolmodel’s codenames, so segment, note and file writes needadd/change/delete_patrol, not a segment, note or file permission.Leader visibility. On a patrol or segment detail, the caller must be able to see the leader subject of the patrol’s latest segment, or be that leader; a patrol with no segments, or a leader that is not a subject, passes. The patrols list applies a looser filter: a patrol is listed if it has no segments, or any of its segments has no leader, a visible subject as leader, or a user as leader.
Segment types have no permissions of their own. They are read and written exactly like patrol types;
view_segmenttypeandchange_segmenttypeare not consulted. See Segment schemas.Caller-scoped content. The resolved patrol configuration lists only subjects the caller can see (see Patrol configuration), and the tracked-by schema offers only the leaders the caller can see.
pre_render=true. A rendered schema resolves each$refagainst the caller’s own permissions. A source the caller cannot read renders as an empty choice list ("title": "Empty schema") under a200, never a403, so two users can get different bodies (and ETags) from the same route.Order of checks on
patrols/config/default. (1)patrol_schemaspreview gate,404for everyone while off; (2) authentication,401; (3)ViewPatrolPermission,403withoutview_patrol; (4) configuration-name lookup,404for any name butdefault. The permission check precedes the name lookup, so a caller withoutview_patrolgets403for every name and cannot probe names through the404.404, not403, when a preview feature is off. The segment-type and patrol-configuration routes are gated on the tenant’spatrol_schemaspreview feature. The gate runs before authentication and permissions, so while it is off every caller, including an anonymous one, gets404. A403means the feature is on and the caller lacks the permission.Superusers pass every check above. Anonymous callers get
401.
Django admin: patrol configuration¶
The configuration the API serves is edited on the ACTIVITY › Patrol Configuration admin page. Its permissions are independent of the API’s: editing keys on the model’s own activity.*patrolconfiguration codenames, not on view_patrol, so a user can edit the configuration without being able to read patrols, and vice versa.
Action |
Requires |
Notes |
|---|---|---|
Page exists |
|
|
View |
|
Django default; |
Change rosters |
|
Django default; no override on the admin class |
Add |
|
Per-tenant singleton; once the row exists the add action is gone for everyone |
Delete |
— |
Always refused, superusers included |
Add / change / delete a team (inline) |
|
|
The patrol_schemas preview feature does not change who may edit. While it is off, the page hides the asset roster field and the teams inline and leaves the member roster editable for anyone holding change_patrolconfiguration.
Summary for mobile clients¶
Goal |
Recommendation |
|---|---|
Fewest “has anything changed?” checks |
Use conditional GET where supported: patrol types (list, detail, schema routes, revision history), tracked-by schema, patrol segments (list, detail) and patrol configuration ( |
Patrol types catalog |
GET …/patrols/types with If-None-Match; treat 304 as “no download needed”. One list call is enough for the full catalog. Returns active types only by default — add |
Tracked-by / segment leaders |
GET …/patrols/trackedby with If-None-Match; 304 means schema/options unchanged. |
Patrol type change history |
GET |
Patrols list |
No ETag. Use filter (e.g. date_range with |
Patrol detail |
No ETag. Refetch when you need the latest state. |
Segment detail |
GET …/patrols/segments/<segment_id> with If-None-Match; 304 means the segment is unchanged. Shareable across users. |
Patrol type schemas |
GET …/patrols/types/schemas for the whole catalog, or GET …/patrols/types/<id_or_value>/schema for one type. Add |
Where conditional GET works |
Patrol types list and detail, via ETag / If-None-Match or Last-Modified / If-Modified-Since; patrol type schema routes, revision history, the tracked-by schema, segment list/detail and the patrol configuration routes, via ETag / If-None-Match only. Prefer If-None-Match everywhere. Patrols support neither. |