# Segment schemas (the `default` segment type) A patrol segment's user-defined fields are shaped by a **segment schema**: one document per site (per tenant), applying to every patrol segment regardless of its patrol type. The schema is owned by a `SegmentType` row. `SegmentType` is a **pseudo-singleton** — a real table that could hold many rows, but in practice exactly one row per tenant is allowed to exist, reserved under the value `default`. The model mirrors v2 `EventType` and `PatrolType` (same `{json, ui}` envelope, same meta-schema validation, same revision auditing); the only structural difference is that there is no create or delete surface. > **Availability.** These endpoints are gated on the per-tenant `patrol_schemas` > preview feature. A tenant without it gets `404` from every route below — the > surface is indistinguishable from one that was never deployed. Segment types > have no permissions of their own: they use the patrol-type permissions. Reads > require `activity.view_patroltype` or `activity.view_patrol` (anyone who can > read patrols needs the segment schema to record segments); `PUT` requires > `activity.change_patroltype`. `activity.view_segmenttype` and > `activity.change_segmenttype` are not consulted. ## Endpoints ``` GET /api/v1.0/activity/patrols/segments/types/default/ retrieve PUT /api/v1.0/activity/patrols/segments/types/default/ replace the schema GET /api/v1.0/activity/patrols/segments/types/default/schema/ the schema document ``` `default` is the only accepted path segment. Any other value — including the row's own UUID — returns `404`; the literal is baked into the URL pattern, so the rejection happens at the resolver. `POST` and `DELETE` return `405`. This table cannot gain or lose records through the API. ### `GET .../types/default/` Returns the record. By default the `schema` field is **omitted**: ```json { "id": "3f1c…", "value": "default", "display": "Default" } ``` Pass `?include_schema=true` to get the document inline: ```json { "id": "3f1c…", "value": "default", "display": "Default", "schema": { "json": { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": {}, "required": [], "unevaluatedProperties": false }, "ui": {"fields": {}, "headers": {}, "order": [], "sections": {}} } } ``` A tenant that has never written a segment schema serves the **canonical empty envelope** shown above, never `null` — the same behaviour as `PatrolType.schema` with an unset schema. That envelope is the stored default, not something the API synthesizes on read, so it is ready to be edited as-is. The response carries `ETag` and `Last-Modified`; both support conditional GET. The `include_schema=true` ETag is distinct from the default one and moves when the document changes. ### `PUT .../types/default/` Replaces the schema document. The body is validated against the shared meta-schema (JSON Schema draft 2020-12) plus the `ui.sections` / `ui.headers` / `ui.order` parent-reference contract. ```json {"schema": {"json": {...}, "ui": {...}}} ``` - Omitting `schema` leaves the stored document untouched. - `"schema": null` is a `400`, not a silent no-op. - `value` and `display` are **reserved**. A body may echo their current values — so a `GET` response can be `PUT` straight back — but any *change* is a `400` naming the reserved value. Nothing is written when that happens; a rejected identity change does not let an accompanying `schema` through. - Of the four envelope-root keys the shared meta-schema tolerates for `EventType`'s benefit, only `auto-generate` is accepted here. `readonly`, `icon_id` and `image_url` are rejected with a `400` under the `schema` key. Every accepted write is recorded as a revision (`SegmentTypeRevision`), including the timestamp-only change a re-save of an identical document produces. ### `GET .../types/default/schema/` Returns the schema document itself (not the record wrapper), with the shared schema-endpoint semantics used by v2 event types: | Query param | Meaning | | --- | --- | | `pre_render=true` | Dereference `$ref` entries and render dynamic fragments inline. | | `s_format` | Choice-field shape when `pre_render=true`: `oneOf` (default) or `enum`. Ignored otherwise; an unsupported value is a `400`. | A `200` carries the bare document. A document-level problem (no schema, a broken envelope, corrupt storage) returns `422` with structured errors: ```json {"errors": [{ "category": "validation", "code": "schema_structure_error", "message": "Schema missing 'json' or 'ui' top-level key.", "hints": [{"message": "A schema document must be a JSON object with top-level \"json\" and \"ui\" sections — update the segment type schema via the API.", "action_type": "fix", "details": {}}], "context": null }]} ``` The hints name only the API on purpose: the Django admin row for segment schemas is a link out to the form builder, with no editable schema widget, so the validated API is the only write path. The response carries an `ETag` (no `Last-Modified`) and honours `If-None-Match` with `304`. It is a hash of the served document: shareable across users without `pre_render`; with `pre_render=true` it moves when the data behind a `$ref` changes and is per-user, so cache it per authenticated identity. Only a `200` carries an ETag; the `422` above has none. `pre_render` yields its own ETag, and so does `s_format` when `pre_render=true`. ## How the `default` row comes to exist Lazily, on first access, and by exactly one mechanism: `SegmentTypeViewSet.get_object` does a `get_or_create`. The first request to any of the routes above creates the row (and its `added` revision); every later request finds it. There is **no seeding migration** — every read and write of a segment schema goes through this viewset (the admin row is a link out to the form builder, which drives the same API), so a deploy-time fleet-wide write would buy nothing a tenant's own first request does not. Consequences worth knowing: - The row's `id` is a fresh `uuid4` **per tenant**. Ids are deliberately *not* shared across tenants here, unlike the `PatrolType` rows seeded by `activity/migrations/0096` — so the shared-primary-key write hazard that applies to patrol types does not apply to segment types. - A tenant whose `patrol_schemas` preview feature is off never reaches the `get_or_create`: the gate runs first, so no row is created for a dark tenant. - The creation is idempotent and tenant-scoped. `SegmentType.objects` is tenant-scoped, so the read half only sees the caller's own row, and the `(das_tenant, value)` unique constraint turns a concurrent double-create into a loser-raises that `get_or_create` retries as a read. - A tenant that has never called the API has no row at all. Nothing else reads `SegmentType`, so that is not observable anywhere. ## Admin The ACTIVITY section of `/admin` carries a link-only row, **Patrol Types and Segments**, which opens the form builder's patrol-types page at `/admin/form-builder/patrol-types`. Like the existing "Event Types and Categories 2.0 (Beta)" row it is a proxy model with every write permission denied; the row is present only when `PATROL_ENABLED` is on.