Documentation navigation

Wayfarer

API and SSE

Wayfarer’s API is implemented by the controllers under Areas/Api/Controllers/. It serves the web application, the Vue Trip Editor, integrations and the separate mobile client. Treat the live controllers and DTOs as the contract; this page groups the current surface by responsibility rather than duplicating every action signature.

Development exposes Swagger UI at /swagger. Swagger is not enabled by the normal Production middleware path.

Authentication modes

Wayfarer has three API access patterns:

api/mobile/* controllers use the shared incoming token authority, normally via the mobile token accessor, and are bearer/API-token paths. They do not create an ASP.NET authentication principal merely from the token.

Acquire and replace a connection token

Users obtain the canonical incoming credential through Settings → Connect apps. This is a User-role cookie Identity surface, with an active database owner, HTTPS and MVC antiforgery; a bearer header cannot authorize credential issuance. GET shows only safe metadata. Create returns 201, Replace returns 200, and a stale/competing state returns 409 without a secret. Replacement checks the observed row ID and issuance timestamp atomically and invalidates the previous canonical verifier.

The successful JSON contains only token, tokenId and issuedAt. Plaintext is revealed once after commit, never recovered from storage. Page/response outcomes are no-store; Done/navigation clears browser token and QR state without revocation. If the response is lost, refresh safe status and require another explicit replacement; do not automatically retry credential POSTs. Existing extra administrative tokens remain separate from this single-credential User workflow.

Prove the current bearer account

GET /api/mobile/me is the additive account-proof contract for successor clients (#844). Send exactly one Authorization: Bearer <connection-token> header to the verified, trusted HTTPS server origin. The scheme is case-insensitive; cookie sessions and arbitrary authorization schemes cannot authorize this route. It uses the same stored hash/legacy incoming-token matching and active-account checks as existing mobile APIs, with a fresh lookup on each HTTP request.

Success is HTTP 200 with Content-Type: application/json and exactly:

{ "userId": "<ApplicationUser.Id>" }

Treat userId as an opaque, case-sensitive string scoped to the verified server origin/instance. Two valid tokens for the same active account return the same ID; different accounts return different IDs. No username, display name, email, roles, token value/hash, token metadata or other profile fields are returned. This is a read-only proof: it does not ingest locations, update last-use counters, rotate credentials, invoke visits/providers or broadcast SSE. Existing infrastructure still performs request logging, bounded lookup admission and database reads.

Outcome HTTP contract Consumer interpretation
Missing, malformed, invalid, revoked token; inactive/deleted account; cookie only 401 JSON {"message":"Invalid or missing API token."}, no login redirect No bearer account proof
Per-IP / global lookup capacity exhausted 429 / 503 JSON {"message":"API token lookup capacity exceeded."}, Retry-After: 5 Temporary overload; honor retry hint, do not reject credentials
Store/server failure Existing generic API 500 envelope, including requestId Transient/unproved outcome; do not classify as invalid credentials
Cancellation / lost response No usable proof; the existing API error boundary may emit a generic 500 if still writable Preserve the candidate as unverified
Older backend without this route Usually 404 (possibly an incompatible response) Unsupported/unverified candidate, not invalid credentials

Responses from the action, including authentication, overload and handled lookup errors, carry Cache-Control: no-store. The endpoint adds no token/profile logging; deployment must retain the existing HTTPS, trusted-proxy/origin and secret-safe logging configuration. These routed server tests do not establish production deployment, Android or iOS behavior.

This initial wire contract does not require a new API namespace, application version or client change in published WayfarerMobile. A successor must require a successful, well-formed account proof before committing candidate credentials or claiming same-owner continuity. Offline, unsupported, malformed or otherwise unproved responses must preserve existing credentials/data and hold or isolate candidate work. Public Trip success, anonymous settings, URL equality and token hashes are not identity proof. Account partitions, in-flight fences, atomic credential replacement and delivery eligibility remain WayfarerApp #5 responsibilities; this GET never authorizes transferring old work to a new account/server.

Observation history and current presence (#855)

Check-in keeps three independent clocks: timestamp becomes stored UTC LocalTimestamp (human final-submit time for deliberate Check In), nullable observedAtUtc is the original sensor fix instant, and Timestamp remains server receipt time. Ordinary log-location retains its capture-time and accuracy/time/ distance filtering semantics, including legitimate filtered-ping visit detection.

New clients can add coordinateOrigin (sensor, manual-map, or unknown), observedAtUtc (an offset/UTC instant) and observedAgeAtEventMs (a nonnegative integer representing reliable monotonic fix age at the frozen event). Omit age when reboot or clock change makes it uncertain. These assertions are not sensor attestation. Accuracy, provider, source and isUserInvoked keep their meanings; queued automatic check-ins retain isUserInvoked=false and automatic SSE labels. The check-in route does not change passive log-location admission thresholds.

Only affirmative sensor evidence with known age, consistent event-minus-fix age (at declared millisecond precision), fix <= event <= receipt, and receipt-minus-fix within the effective positive LocationTimeThresholdMinutes window can advance presence. The default is five minutes. Future/clock-skew tolerance is zero: no existing time contract grants drift. Clock disagreement or a disabled/nonpositive window fails closed; it never widens freshness. This does not reject valid history for age, accuracy, manual coordinates or missing evidence. Syntactically malformed new JSON values use existing MVC 400 handling. Unknown bounded origin text and negative age remain historical-only assertions.

The nullable server-owned PresenceObservedAtUtc records an admitted observation. Clients and portable files cannot set it. PostgreSQL serializes postcommit current selection; observation instant then row ID provide deterministic non-regressing order, including equal-time fixes. A late-finishing older request cannot replace current state. Cache entries are advisory; automatic admission queries the same durable current selection used by both authorized Group latest adapters. Admitted points remain last-known after aging; live styling expires from this UTC observation clock, independently of displayed event time. All owner/public/embed and User/Manager Group maps share that expiry rule; idle Group markers transition to last-known without waiting for another upload and cancel expiry when removed. Existing public/Group audience and hidden-area/delay rules still govern reads and protected SSE delivery.

Historical-only check-ins persist and acknowledge the immutable account/UUID winner, including legacy clients and pre-migration unknown rows. They can send a non-live owner history refresh hint, but no Group presence hint or arrival-time visit candidate, open/update/close, cooldown or visit notification. Arrival-time Visit location/count associations exclude them; explicit historical backfill remains separate. Replays return the first winner without repeating effects. A successful acknowledgement does not promise completed visit work or notification delivery.

Migration LocationObservationEvidence adds four nullable columns and an ordering index, without rewriting stored rows. Native CSV/GeoJSON properties and GPX/KML metadata retain the three raw evidence fields; absent/malformed optional import metadata stays unknown. Imports always remain historical. Owner receipt, search, export and database recovery retain raw evidence; shared Group/public DTOs expose only derived presence. Coordinate/event edits clear coordinate-bound evidence and admission. This release changes legacy live semantics and must be coordinated with successor WayfarerApp readiness; it does not establish that old clients are retired.

Antiforgery boundary

Browser mutations that rely on cookie Identity use MVC antiforgery, normally through [ValidateAntiForgeryToken]. This includes the Trip Editor mutation family and many group/invitation/backfill/browser mutations.

PUT /api/location/{id} is the explicit dual-auth boundary: CookieFirstAntiforgeryAttribute requires antiforgery whenever an authenticated cookie selected the request, even if a bearer header is also present. Only a successfully resolved bearer request with no selected cookie authority is token-free.

Bearer-only mobile/integration mutations do not require MVC antiforgery. Do not remove antiforgery from a browser mutation merely because a bearer path exists elsewhere.

Route families

Family Current responsibility Typical authority
/api/version Application version anonymous
/api/icons, /api/tags, /api/settings UI/reference data endpoint/controller rules
GET /api/activity shared catalog, ordered by Name, [{id,name,description}] active Identity cookie first, otherwise existing opaque Bearer token; direct 401 on missing/invalid/inactive identity
/api/users user lookup, caller/self statistics from cookie Identity claims, authenticated activity/management actions endpoint rules; statistics use the caller’s NameIdentifier; cookie roles and ownership for management
/api/trips trip reads/search, public trip discovery/images and legacy trip mutations/clone public where explicitly exposed; otherwise token/cookie ownership
/api/trips/{tripId}/editor Vue Trip Editor state, metadata, regions, areas, places, segments, tags, share progress and geocode search User cookie + ownership; mutations antiforgery
/api/trip-editor/{tripId}/segments/{segmentId}/route-proposals external route proposal generation User cookie + ownership + antiforgery
/api/location location ingestion, queries, edits, deletion and stats token/cookie depending on action; ownership required
GET /api/location/history, /history/overview, /history/{locationId} unsampled owner pages/candidate inspection, independent aggregation and complete detail strict active-owner bearer only
/api/visit, /api/backfill visit queries/mutations and visit backfill authenticated/owned browser API
/api/groups, /api/invitations group membership, visibility, location sharing and invitations authenticated membership/role; browser mutations antiforgery
GET /api/mobile/me minimal stable current-account proof, no writes active bearer/API token only
/api/mobile/groups mobile group/member/location/visibility operations bearer/API token
/api/mobile/visits mobile recent-visit recovery bearer/API token
/api/mobile/routing routing profiles, capability and route requests bearer/API token
/api/sse, /api/mobile/sse live/reload-hint streams public eligibility, cookie Identity or bearer according to stream family

The Trip Editor controller is intentionally split across TripEditorController.cs and partial files for areas, places, segments and tag/share-progress behavior. Add an editor endpoint to that cohesive family rather than creating an unrelated controller.

Mobile routing geometry and instructions (#930)

POST /api/mobile/routing/route returns instruction fromIndex and toIndex as zero-based, inclusive positions in the exact returned geometry. Each span satisfies 0 <= fromIndex <= toIndex < geometry.length; point/destination instructions may have equal indices. Optional provider instructions may be absent; instruction coverage need not partition the entire route.

Leg flattening, exact anchor restoration, anchor insertion, repeated-coordinate removal and simplification preserve instruction boundary identity. Required instruction vertices are retained alongside anchors, then indices are remapped without changing instruction text, type, distance or duration. Interior boundaries retain their coordinates; boundaries at snapped semantic anchors follow the existing exact requested-anchor restoration contract.

The existing geometry limits remain: simplify above 1,000 coordinates, prefer 500, never return more than 1,000, and keep deviation within 25 metres. If required boundaries and fidelity cannot fit, return succeeded:false, outcome:"invalid-response" without partial geometry or instructions. Already consumed provider credit remains consumed. Trip Editor route proposals and their protected contexts use the same remapped instructions and existing provider-route-budget-unsatisfied failure.

Trip Editor Place address metadata

PUT /api/trips/{tripId}/editor/places/{placeId} uses the prepared, original-aware combined contract documented in conditional Place structure. Coordinate-only intent preserves Address. Changed coordinates clear the complete five-field enrichment tuple; unchanged coordinates retain it. Explicit Web Address and trusted metadata choices keep their existing compatibility/replacement rules, including consumed provider admission if domain persistence fails. All intended and necessary derived effects commit together. Unknown outcomes remain held; prepare, refresh and matching values do not establish noncommit.

Ownership and role checks

Authentication establishes a caller, not resource access. Controllers/services also enforce trip ownership, group membership/management, target-user constraints and role requirements such as Admin, Manager or User.

For token APIs, resolve the user through the existing request-owned resolver/mobile accessor. Do not re-query raw token values or cache authenticated identities across requests.

JSON and spatial conventions

Controller JSON is case-insensitive on input and permits named floating-point literals where the configured serializer allows them. API/publication DTOs should still sanitize values before emission.

PointJsonConverter writes NetTopologySuite Point values as:

{ "longitude": 23.72, "latitude": 37.97 }

The converter does not implement Point deserialization. Input endpoints use their own DTO shapes; do not send arbitrary NTS geometry JSON unless that endpoint explicitly defines it.

Spatial persistence uses longitude as X, latitude as Y and SRID 4326.

API errors and request IDs

ApiErrorResponseMiddleware normalizes terminal /api 401, 403 and 404 responses that reach it into JSON with status, error and message while preserving the HTTP status. Unhandled API exceptions become a generic 500 envelope and include requestId = HttpContext.TraceIdentifier; the same request ID is placed in Serilog context by RequestIdLoggingMiddleware.

Individual actions may return narrower domain errors or compatibility response shapes. Do not assume every non-success response has one universal schema.

API-token lookup admission can return an overload response with Retry-After: 5. Location-ingestion admission can return its bounded overload response with Retry-After: 12. Clients should honor the status and Retry-After instead of tight-loop retrying.

Location ingestion and idempotency

Send Content-Type: application/json and Authorization: Bearer <connection-token> to POST /api/location/log-location for automatic capture or POST /api/location/check-in for an intentional check-in. The ordinary connection guide owns the GPSLogger configuration; these endpoints accept JSON POST, not GET/query-token logging.

GpsLoggerLocationDto accepts:

Fields Meaning
latitude, longitude Decimal degrees, ±90/±180; (0,0) is rejected.
timestamp Capture DateTime; ISO UTC Z and numeric offsets preserve the capture instant. No-offset local time is converted using coordinates. Non-nullable DTO defaults are not required-field validation.
accuracy, altitude, speed Optional metres, metres, metres/second respectively.
locationType, notes, activityTypeId Optional capture description, notes and existing activity ID.
source, isUserInvoked, provider, bearing Optional origin, invocation flag, sensor/provider and bearing in degrees.
appVersion, appBuild, deviceModel, osVersion, batteryLevel, isCharging Optional capture metadata; battery percentage is 0–100.

For both check-in and log-location, 2026-08-22T10:00:00+02:00 and 2026-08-22T08:00:00Z store capture LocalTimestamp as 08:00Z. An offset-free 2026-08-22T10:00:00 at Athens coordinates retains coordinate-local interpretation and stores 07:00Z; invalid or ambiguous coordinate-local times retain their existing conversion error. MVC binds numeric offsets to a host-local DateTime representing the same instant; the literal offset text is not retained. TimeZoneId still comes from coordinates, independently of the supplied offset. Timestamp remains server receipt UTC time, separate from capture LocalTimestamp.

Omit unavailable measurements. Metadata does not change endpoint filtering: isUserInvoked on log-location is not a check-in bypass. Automatic capture applies accuracy, time and distance thresholds and duplicate-timestamp rules. A 200 can be { "success": true, "skipped": true, "locationId": null }, with no new row. A saved automatic point returns skipped: false and its integer locationId. Check-in success returns message and the publication-safe location object. Check actual saved history rather than equating HTTP success with persistence.

Anonymous GET /api/settings exposes the effective configured time, distance and accuracy thresholds. Defaults are 5 minutes, 15 m and 50 m respectively; configured values take precedence. The real settings service loads persisted row ID 1 or throws if it is missing; the controller’s defensive null-result fallback is not the shipped accuracy default.

POST /api/location/check-in and POST /api/location/log-location accept an optional Idempotency-Key header containing a GUID. The key is unique per user. A persisted replay returns the prior success before new-work admission; concurrent duplicate persistence is reconciled against the same database invariant.

For a new UUID-keyed create, the Location winner commits before optional personal provider admission/contact. Only the newly inserted winner attempts immediate geocoding; a losing insert, persisted replay or failed initial insert consumes no creation-geocoding admission/contact. Keyless requests retain their existing enrich-before-insert sequence. Different keys and accounts remain independent.

Successful immediate enrichment saves the complete address/provenance tuple before the winning response and SSE publication. The short post-HTTP save checks the same owner and numeric row, unchanged coordinates, untouched address fields and current provider, credential, verification, consent and selection generations. Owner edits or another retained result take precedence; responses and publication use saved state. No database transaction spans provider HTTP. A concurrent replay can observe the committed winner before its optional address is saved.

Provider failure, request cancellation, process death or an enrichment-save failure after the initial commit can leave a valid Location without an optional address. Normal provider unavailability retains a successful create; request cancellation propagates, and other errors can still return 500 after persistence. A same-key retry acknowledges that row without contacting again. Admissions already consumed remain charged even when no address is saved. Creation adds no automatic enrichment workflow or durable attempt: eligible wholly missing addresses can be processed through the existing explicit missing-address enrichment controls, or a history import’s user-wide enrichment opt-in. An already enabled workflow may also select eligible rows during reconciliation. With no such opt-in, the post-commit/pre-enrichment gap has no automatic recovery or globally exactly-once external-contact guarantee.

Before any automatic threshold or duplicate-timestamp skip, a supplied valid key gets a fresh database check under the account deletion fence. A deletion marker returns the terminal 410 described below, before any Visit decision. If the keyed winner is already persisted, log-location returns success: true, skipped: false and its numeric locationId, preserving the first writer’s data. Otherwise the skip remains success: true, skipped: true, locationId: null. This check proves only what is persisted at lookup time; a later commit is acknowledged on a keyed retry. A lookup failure is an error, never evidence of an unpersisted skip.

Automatic keyed captures selected for persistence enter visit detection only after their Location insert succeeds. Failed inserts, losing keyed conflicts and persisted replays add no visit hit. Genuine time/distance/duplicate-timestamp filtered pings still feed visit detection; a skip that instead finds a persisted same-account keyed winner adds no hit. Accuracy rejects stay outside visit detection. Unpersisted filtered pings have no durable creation receipt, so repeated deliveries and hard crashes carry no universal once-only visit guarantee. Keyless automatic pings and manual check-in retain their existing visit behavior.

Use a new key for a new logical location write and retain the same key when retrying that write. An invalid GUID is a 400.

Manual check-in intentionally bypasses automatic accuracy/time/distance filtering; both paths retain authentication, coordinate validation, bounded admission, persistence and immediate post-write work. Admission can return 429/503: honor the actual Retry-After header. Invalid credentials need correction, and malformed JSON/coordinates need a corrected request. Basic GPSLogger setup does not require an idempotency key or claim its queue implements Wayfarer’s retry strategy.

Deleted location identities and key status

Owner deletion of a stored UUID-keyed Location retains a minimal LocationDeletionMarkers(UserId, IdempotencyKey) decision. Bearer single deletion, Web cookie/antiforgery bulk deletion and owner cookie/antiforgery delete-all all record markers atomically with row removal. Unknown or foreign IDs cannot create markers. Deleting all Locations retains existing markers; a genuinely new UUID can still be stored. There is no automatic restore or replacement-key bypass.

Both ingest routes return HTTP 410 for a confirmed deleted owner/key:

{"success":false,"code":"location_deleted","message":"Location was deleted."}

The response contains no location, locationId, successful skip or Retry-After. Deletion takes precedence over passive thresholds and ingestion saturation. Ordinary persisted replay, keyless writes and genuine filtered skips keep their existing response shapes. A marker is account-owned metadata, not anonymous data; it contains no coordinates, notes, device data, old integer ID or deletion time.

Keyed creation, each import batch, owner deletion and relevant provider/presence effects acquire the same transaction-scoped PostgreSQL account lock before other locks. Marker/winner decisions use subsequent READ COMMITTED statements after lock acquisition. Provider admission is atomic with retained-row validation; provider HTTP runs outside the transaction, and results are revalidated before saving. Deletion during that HTTP returns 410 without stale Location publication or Visit effects. An already admitted external contact cannot be recalled.

POST /api/location/key-status is a read-only bearer operation using the existing active-account token resolver, with no target-owner, Group or role override:

{"keys":["18c8818c-24d5-46cd-a370-c238ff7e8126"]}

Supply 1–100 entries, using the existing parseable UUID domain, including a historical singleton zero UUID. Malformed, missing, non-string or oversized input returns whole-request 400. UUID aliases collapse to one distinct result. HTTP 200 returns success: true and results, with one of these shapes per requested key:

{"key":"18c8818c-24d5-46cd-a370-c238ff7e8126","state":"deleted"}
{"key":"18c8818c-24d5-46cd-a370-c238ff7e8126","state":"present","locationId":123,"localTimestamp":"2026-10-19T12:00:00Z"}
{"key":"18c8818c-24d5-46cd-a370-c238ff7e8126","state":"unknown"}

The bounded indexed query uses one coherent database statement snapshot. Responses use Cache-Control: no-store; request bodies and keys are not logged. No UUIDs are added to public or Group projections. A foreign UUID reports only the caller’s own state, normally unknown. Authentication failures remain request errors.

present survives date moves and edits; its canonical UTC event timestamp locates the ordinary owner day projection, and is not an edit revision. unknown includes never-uploaded, filtered and pre-marker-era absence. It authorizes neither local erasure nor blind replay of an ambiguous attempt. Missing or sampled day/Group results are never deletion evidence. Keyless history has no portable exact-key discovery guarantee. Devices query their own keys in chunks of 100; there is no history enumeration, durable cursor or change feed.

Markers last for the account’s lifetime, including normal backups/restores, with no TTL, quota eviction or cleanup. Complete account erasure cascades markers and credentials. Coverage starts at deployment: earlier deleted UUIDs cannot be reconstructed and remain unknown. Restoring an older whole-database backup cannot preserve deletion intent absent from that backup. Deliberate restore requires a separately accepted contract.

Successor clients must persist deleted/410 as terminal under the original server + account + UUID, hide the observation from ordinary Timeline, and stop matching replay and incompatible mutations. A late 200, import Completed, date move or account-switch callback cannot reverse that decision. Never automatically mint another UUID to bypass deletion. Keep unsent originals on unknown and retain necessary scoped cancellation/recovery evidence or user work requiring explicit resolution. WayfarerApp integration remains a separate acceptance gate; unfamiliar 410 retry handling is not guaranteed in released clients.

Personal history bearer reads (#918 I918-B)

The UTC history index includes capture status after migration 20261024172605_LocationHistoryUtcStatusCoverage, enabling covered capture membership/uncertainty reads where the planner selects them. This changes no request, response, filter, coverage, readiness or timeout semantics. Use the maintenance procedure and bounded measurements; the sub-second performance target remains pending maintainer disposition.

GET /api/location/history, GET /api/location/history/overview and GET /api/location/history/{locationId:int} require one strict Bearer header and the current active owner. Cookies cannot authorize them and there is no owner parameter. Missing/revoked credentials or an inactive account return the existing normalized API 401; missing and foreign detail share the same generic 404. Authority is checked at request time (#907); an already authorized read may finish after revocation/deactivation. Every success/error is Cache-Control: no-store.

These are live browsing reads, not backup, synchronization or deletion evidence. One read-only repeatable-read database view covers preparation, data, counts and publication per response. Later requests can observe changes. Stable identity is trusted normalized server origin + subjectId + locationId; nullable UUID is an additional portable alias in D format. Absence, exhausted paging and detail 404 never establish keyed deletion (#838 retains that authority).

Collection request and exact predicates

Both collection routes share one normalized SQL scope, echoed in scope, and a subject/rules-bound scopeKey. Values must be URL encoded. Unknown keys, repeated scalars, conflicting/irrelevant parameters, invalid Unicode/NUL, nonfinite numbers, empty/reversed intervals and unsupported precision are controlled 400 errors. Operational query input is bounded by 8192 UTF-8 bytes and the serving request-target limit. There is no history-retention or one-/two-year range cap.

Parameter Meaning
timeBasis Required utc, capture, viewer.
timeMode Required interval, days, daily; UTC supports interval only.
viewerZone Required serving NodaTime IANA zone for display in every mode; aliases echo the canonical zone. Never the host zone.
startUtc, endUtc UTC interval only: explicit-offset RFC3339, normalized UTC [start,end).
fromLocal, untilLocal Capture/viewer interval: offset-free ISO datetime, [from,until).
fromDate, throughDate Days/daily: inclusive ISO calendar dates.
fromTime, untilTime Daily only, HH:mm[:ss[.ffffff]]; equal values invalid.
q Optional literal whole phrase, at most 256 Unicode scalars before normalization. Whitespace-only means no predicate.
activityIds, includeUnassigned Comma-separated positive catalog IDs (OR) and boolean. Resolve retained redirects, deduplicate/sort/echo canonical IDs. Null activity is distinct from an activity named Unknown. Omission means all; includeUnassigned=true alone means null only. Empty/false-only selections are invalid.
west, south, east, north All four or none: exact WGS84 rectangle, no padding. West greater than east wraps; equal longitude/nonpositive latitude spans are invalid. Longitude [west,east) canonicalizes recorded +180 to -180; latitude [south,north), including +90 at the world maximum.

Calendar years are 0001–9999, with at most six fractional digits; next-day bounds must be representable. Event UTC is LocalTimestamp, not Timestamp or presence admission. Continuous wall intervals include intervening daytime. Days mean whole calendar days, independent of elapsed-day length. Recurring overnight dates name start days: 10–12 October 22:00–02:00 includes early 13 October and excludes early 10 October. Both fold occurrences match, gaps/skipped dates create no records, and historical offsets can include seconds. Capture predicates use I918-A’s stored facts. Viewer predicates use serving NodaTime transition segments with wall arithmetic inside PostgreSQL, independently of PostgreSQL’s installed TZDB.

Time AND text AND activity AND area apply before paging/aggregation. Text searches Notes and Address, FullAddress, ProviderAddressLine1, AddressNumber, StreetName, PostCode, Place, Region, Country, ResolvedFeatureName. It uses the exact I918-A normalizer and equivalences: visible sanitized Notes, compatibility normalization, selected Latin/Greek accent folding, invariant case and collapsed whitespace. cafe matches café; %, _ and backslash are literal. No phrase crosses fields; no fuzzy/stemming/translation or script transliteration. Short/punctuation phrases use exact strpos; useful trigrams use escaped literal LIKE over prepared C-collated text. No raw Notes history scan, per-row coordinate conversion, provider lookup or mutation occurs.

Pages, overview and coverage

Common envelope: {subjectId,profile,scopeKey,scope,derivationVersions,observedAtUtc, consistency:"live-request",coverage,...}. observedAtUtc is observation time, not a commit/revision watermark. coverage has membership:"complete"|"incomplete", resultExtent:"page"|"aggregation"|"candidatesPage" and unresolvedCandidateCount as a decimal string. This exact conservative candidate count covers the entire scope before continuation. It is not a confirmed match count. Reliable false predicates exclude rows; otherwise unevaluable required time/area evidence remains unknown. Zero known matches plus candidates is not “no history”. Complete membership is not exhaustive retained history or sync proof.

History adds pageSize (default 100, max 500), continuation and resultSet=matches|unresolvedCandidates (default matches). Response adds {items,nextContinuation}. Matches use owner-summary-1 and (eventUtc DESC,locationId DESC) at microsecond precision. Candidates use owner-unresolved-1, ID descending, filterMembership:"undetermined" and safe reasonCodes. They are inspection records, not matching history. The two cursors are independent. No full match total is attached to every page.

Continuations are protected with a dedicated Data Protection purpose and bind subject, complete normalized predicate, interpretation/derivation versions, profile, result set, order and page size. They do not require a surviving anchor. Tampered/retired/unknown-version/foreign/changed-scope tokens return generic 400 continuationInvalid, without silently restarting. A new valid credential for the same owner may resume. Page size, result set and overview grid do not change matching scope; their cursors still bind applicable controls.

Inserts below an anchor may appear later; above-anchor inserts need refresh. Event edits can repeat seen records moved below or miss unseen records moved above; deduplicate by stable identity and refresh. Text/activity/coordinate edits change membership/cells. Deletes remove later results; previous positives may be stale. Neither absence nor a repeated record authorizes deletion.

Overview uses owner-overview-1 and accepts grid only beyond common filters (integer 1–16, default 16). It independently groups every definite match, with no sampling/top-N/chronology prerequisite. It adds decimal-string signed-64-bit knownMatchCount, mappedMatchCount, unmappedMatchCount, plus grid and cells (at most 256). Known=mapped+unmapped; cell counts sum to mapped. In world UTC/viewer scope, unknown positions may be definite unmapped matches. With an area filter unknown position instead contributes a candidate. Candidates never enter matching cells.

Each cell contains {cellKey,bounds,occupiedBounds,displayPoint,recordCount, earliestEventUtc,latestEventUtc,singleton}. Bounds partition the selected area; wrapping areas use an unwrapped longitude internally and canonical bounds on the wire. Membership and published bounds use the same representable numeric edges, retaining parent endpoints exactly. Shared edges belong to the cell beginning there, with the existing north-pole exception. Preserve the returned numeric precision when requesting choices; counts and choices agree on unchanged data, while separate reads retain live-request semantics. Occupied bounds enclose actual matching coordinates; displayPoint is their synthetic midpoint. Extrema do not assert continuous presence. Singleton has actual ID/UUID only for count=1; multiple records have no arbitrary representative. Cells are aggregates, not recorded Locations, routes or Visits. Dense clusters, distant sparse destinations and coincident records all remain reachable.

Request a cell’s exact bounds on overview or history for refinement/paged choices, keeping time/text/activity, then use direct detail. This works with chronology closed. These requests create an explicit new area scope; a page never changes map geography. Consumers fence stale session/query/viewport replies and preserve selection by identity independently of matching. No automatic camera reset follows refresh. The routed composition below qualifies backend responses; consumer camera, selection/merge fences and native interaction remain separately owned.

Map-first composition and reproducible choices (#918 I918-C)

IndependentOverviewRetainsDistantAndCoincidentRecords executes the production MVC routes against PostgreSQL with the main chronology initially unused. Its unchanged 2025 fixture has 65 Athens home records, three coincident Tokyo records and one New York record. World grid=4 returns three occupied cells, with counts 65, 3, 1; known/mapped counts are 69 and unmapped is 0. Both sparse destinations survive. Tokyo’s returned bounds are successively refined with grid 4 and 16:

GET /api/location/history/overview?timeBasis=utc&timeMode=interval&viewerZone=Europe%2FAthens&startUtc=2025-01-01T00:00:00Z&endUtc=2026-01-01T00:00:00Z&grid=4
GET /api/location/history/overview?timeBasis=utc&timeMode=interval&viewerZone=Europe%2FAthens&startUtc=2025-01-01T00:00:00Z&endUtc=2026-01-01T00:00:00Z&west=90&south=0&east=180&north=45&grid=4
GET /api/location/history/overview?timeBasis=utc&timeMode=interval&viewerZone=Europe%2FAthens&startUtc=2025-01-01T00:00:00Z&endUtc=2026-01-01T00:00:00Z&west=135&south=33.75&east=157.5&north=45&grid=16
GET /api/location/history?timeBasis=utc&timeMode=interval&viewerZone=Europe%2FAthens&startUtc=2025-01-01T00:00:00Z&endUtc=2026-01-01T00:00:00Z&west=139.21875&south=35.15625&east=140.625&north=35.859375&pageSize=2

Send Authorization: Bearer <active-owner-token> on every request. These numeric bounds are the actual fixture’s returned bounds, not its coincident occupiedBounds. In general, copy all four returned numbers without rounding, padding or reconstructing edges. Keep the other common filters. Follow the exact choice page’s URL-encoded nextContinuation with the same page size and scope; this yields the third Tokyo identity. Coincident points still produce one cell with count 3 and singleton:null; further spatial zoom cannot choose a record. Choose an actual items[].locationId, then request:

GET /api/location/history/<selected-locationId>?viewerZone=Europe%2FAthens

The complete detail example below is a captured fixture response. Fixture IDs, UUIDs, subject, scope hashes, observation time and continuations vary per run. After detail, the test opens chronology with those same final bounds and page size

  1. Its independently obtained pages return the same three IDs exactly once in order, retain the choice scopeKey, and leave refreshed overview count 3. Changing page size requires a new first page, not reuse of a differently bound cursor. This demonstrates backend composition, not a mounted UI or native client.

Counts/choices agree because no writes occur between these calls. Real later requests can observe inserts, edits or deletes; refresh and identity deduplication remain necessary. A selected detail’s not-evaluated does not add it to counts.

Profiles and complete owner detail

owner-summary-1 declares locationId, nullable uuid, eventUtc, position, captureTime, viewerTime, nullable activityTypeId, activityNamePreview and labelPreview. Each preview is {text,truncated} capped at 160 Unicode scalars; it is not a full retained field. Position has {status:"available",longitude,latitude} or {status:"unavailable",reason}. Time facts have {status,local,zoneId,offsetSeconds, version} when available; unavailable/pending facts carry a safe reason and version, without invented wall values. Viewer facts use the serving rules. Capture uses prepared versioned facts; stored TimeZoneId is preserved separately and does not override coordinate derivation. An unresolved event is an unavailable status object in candidate inspection. Summaries/candidates cannot clear richer retained detail.

Detail accepts only viewerZone, and remains independent of active filters, list traversal, overview and collection preparation. Envelope: {subjectId,profile:"owner-detail-1",observedAtUtc,derivationVersions,record, unavailableFields,filterMembership:"not-evaluated"}. record includes every field below, including known nulls. Unrepresentable stored numeric/time fields are omitted with an unavailableFields reason, never serialized as nonfinite JSON or fabricated null. Other unavailable evidence has explicit position/time status objects.

Detail fields Stored authority
locationId, uuid, eventUtc, storedTimestampUtc, storedTimeZoneId Id, IdempotencyKey, LocalTimestamp, Timestamp, TimeZoneId. Timestamp can be imported; it is not universally receipt/modified time.
coordinateOrigin, observedAtUtc, observedAgeAtEventMs, presenceObservedAtUtc Retained fix/event/admission evidence. No invented clocks.
position, accuracy, altitude, speed, bearing, locationType Stored coordinates/type/measurements: accuracy/altitude meters, bearing degrees, legacy speed may be m/s or km/h (no stronger unit guarantee).
activityTypeId, activityName, notesHtml Nullable catalog ID/current joined name; complete non-mutating RichNotes.Normalize publication, without a new truncation cap.
address, fullAddress, providerAddressLine1, addressNumber, streetName, postCode, place, region, country Complete corresponding stored text.
resolvedFeatureName, resolvedFeatureType, reverseGeocodingProvider, reverseGeocodingStorageMode, reverseGeocodedAt Complete retained enrichment/provenance.
source, isUserInvoked, provider, appVersion, appBuild, deviceModel, osVersion, batteryLevel, isCharging Corresponding private owner metadata.
captureTime, viewerTime Explicit evidence status and version provenance, as above.

No original submitted offset/device-local zone or modification revision is invented. No credentials, navigation entities or internal derivation/search columns are published. Detail can select an out-of-filter ID; not-evaluated proves neither matching nor outside status. Consumers preserve originating scope, richer retained facts, immutable originals, drafts and absorbing known-key deletion under their session/query/record fences. Detail must not add a selected record into filter counts.

A complete owner-detail-1 example from the map-first fixture (all declared fields are present, including known nulls):

{
  "subjectId": "migration-fixture-869fc504af0548c39d018ac9f9269d98",
  "profile": "owner-detail-1",
  "observedAtUtc": "2026-10-24T06:20:46.970730Z",
  "derivationVersions": {
    "time": "history-time-1;GeoTimeZone-6.1.0;TZDB: 2026c (mapping: 48.2)",
    "text": "history-text-1;RichNotes-2;AngleSharp-1.7.1.0;runtime-10.0.13;unicode-31129-0000799900000000000000000000007f",
    "interpretation": "history-query-1"
  },
  "record": {
    "locationId": 69,
    "uuid": "93ff5f6c-ec4c-4dce-bd30-a106ba7c005b",
    "storedTimeZoneId": "imported-zone",
    "coordinateOrigin": null,
    "observedAgeAtEventMs": null,
    "locationType": null,
    "activityTypeId": null,
    "address": "Tokyo station",
    "fullAddress": null,
    "providerAddressLine1": null,
    "addressNumber": null,
    "streetName": null,
    "postCode": null,
    "place": null,
    "region": null,
    "country": null,
    "resolvedFeatureName": null,
    "resolvedFeatureType": null,
    "reverseGeocodingProvider": null,
    "reverseGeocodingStorageMode": null,
    "source": "import",
    "isUserInvoked": null,
    "provider": null,
    "appVersion": null,
    "appBuild": null,
    "deviceModel": null,
    "osVersion": null,
    "batteryLevel": null,
    "isCharging": null,
    "eventUtc": "2025-10-10T03:00:00.123456Z",
    "storedTimestampUtc": "2026-01-01T00:00:00.000000Z",
    "observedAtUtc": null,
    "presenceObservedAtUtc": null,
    "reverseGeocodedAt": null,
    "accuracy": null,
    "altitude": null,
    "speed": null,
    "bearing": null,
    "notesHtml": "<p>café station</p>",
    "activityName": null,
    "position": {
      "status": "available",
      "longitude": 139.76,
      "latitude": 35.68
    },
    "captureTime": {
      "status": "available",
      "local": "2025-10-10T12:00:00.123456",
      "zoneId": "Asia/Tokyo",
      "offsetSeconds": 32400,
      "version": "history-time-1;GeoTimeZone-6.1.0;TZDB: 2026c (mapping: 48.2)"
    },
    "viewerTime": {
      "status": "available",
      "local": "2025-10-10T06:00:00.123456",
      "zoneId": "Europe/Athens",
      "offsetSeconds": 10800,
      "version": "history-time-1;GeoTimeZone-6.1.0;TZDB: 2026c (mapping: 48.2)"
    }
  },
  "unavailableFields": {},
  "filterMembership": "not-evaluated"
}

CompleteDetailPreservesFieldsAndClassifiesUnrepresentableValues separately proves 873,632 UTF-8 bytes of distinct multilingual Notes survive complete publication (1,203,277 response bytes with JSON escaping). It also checks every stored address component, retained feature/provider/storage/time provenance, UUID, event/fix/ admission clocks and the imported zone. This is an observed supported payload, not a maximum Notes size or a promise about arbitrary process memory capacity.

In its exceptional legacy witness, speed:null is a known null; nonfinite accuracy and an unrepresentable storedTimestampUtc are absent from record and instead appear as "accuracy":"unrepresentableStoredValue" and "storedTimestampUtc":"unrepresentableStoredValue" in unavailableFields. A stale capture generation produces captureTime.status:"pendingPreparation", without overwriting the canonical event, UUID or stored zone. The distinct candidate/ preparation test qualifies incomplete counts and 503 gates. The reviewed inherited finite year-0001 capture derivation limitation remains classified as unresolved; valid UTC/viewer/detail facts remain usable. No broader fix is claimed here.

Preparation and protective limits

The I918-A runbook remains upgrade authority. Capture collections require all-owner ready time generation; nonempty q requires all-owner ready text generation, including matching target/active/writer versions, physical indexes and zero pending work in the response snapshot. Affected operations return 503 historyPreparationRequired. UTC/viewer nontext reads and direct detail remain available with explicit pending capture evidence when applicable. Exceptional current-version unresolved evidence instead returns labelled incomplete 200.

Time/text generation IDs are independent semantic contracts, separate from actual runtime/package/globalization provenance reported by history-data status. Qualified compatible servicing retains rows, readiness/checkpoints and cursor binding; a real or unjustified semantic change changes the affected ID and retires old continuations. #937 changes both historical timezone and parser behavior; both old generations require controlled rebuilding. Exact already-qualified R1 IDs are retained as opaque names, including their historical embedded versions. The earlier fixture example above records its original pre-R1 generation names; it is historical evidence, not the current serving profile.

Missing environment qualification is a distinct 503 historyQualificationRequired for capture/viewer predicates or nonempty searches that require it. It cannot be repaired by rebuilding. UTC nontext collections and direct detail stay usable; unqualified capture/viewer facts say unavailable with reason historyQualificationRequired, retaining stored capture-version provenance. derivationVersions.time / .text is null when that serving generation lacks qualification, never a false current-data claim. The failure is neither an unresolved record nor pendingPreparation. Qualified but stale row publication keeps its existing pendingPreparation semantics. Canonical fields and safe detail Notes remain available. See admission and upgrade rules.

Collections use separate nonwaiting admission at two reads per owner/four globally, independently of ingestion. Detail has an independent four-owner/sixteen-global pool. A fifteen-second collection end-to-end budget (detail thirty seconds) covers lookup, SQL, projection and buffered JSON before success; remaining-time cancellation also reaches SQL through a transaction-local statement timeout. This ceiling follows the 24 October 2026 maintainer decision in #918; the sub-second query-plus-projection target remains unchanged. These are protective engineering limits, not a production latency/capacity promise. Disconnects cancel work; incomplete evaluation never becomes a partial successful page/overview. The final operating-range report records the new 25-year fixture and modest mixed use. Earlier five-second performance receipts below remain historical evidence. I918-C proves the complete large-detail witness above. The bounded query correction completes all 158 repeated routed observations, including full-period overviews at both agreed scales and all twenty concurrent reads. Warm broad overviews take 570–580 ms for 210,240 records and 1,892–1,918 ms for 1,051,200 records. The under-one-second target remains partly unmet: 78 observations exceed it, including recurring/combined workloads and all concurrent/post-write observations. Backend #918 performance acceptance remains pending that measured limitation and the separate review/CI/maintainer gates; see the full results and resource observations. Five-second completion and small-fixture plans do not override the target misses.

Controlled 400 codes are parametersInvalid, activityInvalid, continuationInvalid. Owner admission is 429 historyOwnerAdmissionExceeded with Retry-After: 5; global admission is 503 historyAdmissionUnavailable with that hint. Token lookup retains its existing admission behavior. Other controlled 503 codes are historyPreparationRequired, historyQualificationRequired, historyBudgetExceeded, historyStoreUnavailable. Errors use fixed messages, without SQL, credentials or private query content; unexpected failures retain the generic API 500 path. No provider/media acquisition, presence/Visit, queue, ingestion or persistent read effects are introduced.

Owned location history and corrections

Legacy bearer history helpers include GET /api/location/chronological and chronological-stats with dateType (day, month, year), year, and the corresponding month/day; has-data-for-date and check-navigation-availability provide date/navigation information. Ownership derives from the resolved account. Cookie-only search, bulk-delete and Web location-management endpoints are separate.

Owner chronology returns all period rows for a day. Dense month/year queries may sample map markers, while owner totalItems still counts the full period. This is a map selection, with no complete-history cursor or snapshot guarantee. Use the owner table and export tools for finding and exporting stored history.

Group history has a different selection/count contract. Cookie Web POST /api/groups/{groupId}/locations/query returns its unpaged selection as totalItems/results; it ignores pageSize/continuationToken and returns no pagination flags. Bearer POST /api/mobile/groups/{groupId}/locations/query pages the selected markers. Date-query totalItems counts bbox-filtered selected markers before paging; the viewport fallback retains its underlying bbox count.

Both history adapters reuse the unpaged selection in GroupTimelineService. Web retains raw exact subject IDs and subject blocks in the existing reader’s order. Mobile retains blank removal/ID trimming and global server timestamp/ID ordering before paging. For either adapter, multiple authorized subjects restrict history to a day.

For example, the Phase C PostgreSQL same-cell month witness stored 3,001 rows: owner chronology returned five markers with owner total 3,001; Group history returned five markers, totalItems=5, hasMore=false and isTruncated=false. Those flags exhaust the selected markers, not stored history. Bearer hasMore describes remaining selected markers, and nextPageToken is null on the final page. isTruncated is true when hasMore is true or the whole selection exceeds the current page; it can remain true on the final page, as in a six-marker selection returned across two three-marker pages.

Bearer continuation searches a freshly recomputed selected-marker set on every request. Malformed tokens or vanished anchors restart from the beginning. Keep the same Group, users, date, viewport and zoom scope, and defensively deduplicate across changing selections: this provides neither snapshot stability, exactly-once iteration nor complete stored-history pagination.

chronological-stats and check-navigation-availability accept case-insensitive date types without trimming, years 1–9999, applicable months 1–12, and valid calendar days (including leap days). Invalid supplied components return 400 after bearer resolution; missing/inactive bearer authority remains 401. MVC binding errors return the framework’s 400 response before action-level authentication. Statistics errors use { "success": false, "message": "..." }; navigation errors use { "success": false }. Unexpected statistics failures remain 500.

Statistics requires month/day for day mode and month for month mode. Navigation retains optional month/day inputs and its existing fallback flags; a supplied day without a month must be 1–31. Both actions ignore numeric components irrelevant to the selected mode. Statistics filters capture/event LocalTimestamp over an inclusive UTC period, including the final day/month of year 9999. Navigation uses the server-local current date, retains all previous-navigation flags, and disables future or unrepresentable next periods.

Cookie Web GET /User/Timeline/GetChronologicalStats, GetChronologicalStatsDetailed and CheckNavigationAvailability validate semantic calendar components after cookie authorization and current-user resolution. They recognize day/month/year case-insensitively without trimming, accept years 1–9999 and valid applicable months/days, and return 400 for invalid bound selections. Statistics keeps its required-component-first errors and { "success": false, "message": "..." } body; navigation uses { "success": false }. The summary counters and detailed visit arrays remain distinct DTOs. Genuine downstream statistics failures retain their distinct safe 500 messages. Login/AccessDenied redirects, anonymous XHR 401 and missing-user 401 take precedence over calendar validation.

These conventional Web MVC actions intentionally preserve optional binding fallbacks: empty/unparseable month/day binds to null, causing the existing statistics required-component error or navigation partial-selection fallback. Missing/unparseable year binds to 0 and receives the semantic year 400. Ignored mode-irrelevant extras remain ignored, including unparseable strings; there is no blanket ModelState rejection or framework error envelope. Navigation still allows day mode with neither optional field, month only or day only (day 1–31), and month mode without a month. All six flags, month/leap-day clamps and the server-local clock retain their meanings; unrepresentable next periods are unavailable.

Shared chronology and day/month availability queries, and Web/bearer statistics and navigation, no longer fail solely because constructing the next query or navigation period overflows. December 9999 and 9999-12-31 remain valid selections. History preserves inclusive UTC LocalTimestamp predicates, owner filtering, sampling and projection; Group selection and response contracts are unchanged. Npgsql still maps DateTime.MaxValue to PostgreSQL infinity: maximum upper bounds include infinity and PostgreSQL timestamps beyond year 9999, rather than imposing a strict finite cutoff. The finite final PostgreSQL microsecond is 9999-12-31 23:59:59.999999 UTC. Extreme stored timestamps/zones may still fail coordinate-local projection (for example, that final finite UTC microsecond at Athens coordinates). This existing conversion limitation is outside the bounded successor-arithmetic correction; timestamps are not clamped or rewritten (#849).

PUT /api/location/{id} accepts a partial LocationUpdateRequestDto: paired latitude/longitude, notes, localTimestamp, activityTypeId or activityName, and explicit clearNotes/clearActivity flags. An absent field is unchanged; clear flags take precedence. Coordinates are bounded decimal degrees and timestamps follow the current UTC/coordinate-zone conversion. The ID is the server’s integer location ID, matched within the selected owner’s rows. If a cookie selected the account, antiforgery is required even with a bearer header.

Supplying coordinates, including unchanged coordinates, requests an optional provider refresh. Its admission and result save revalidate the retained owner row under the account deletion fence; provider HTTP holds no SQL locks. Deletion or a missing row returns the ordinary owner 404, while a newer owner edit returns 409 with success: false and message: "Location changed. Reload and retry.". The stale patch and enrichment do not overwrite that newer edit. Optional provider failure or no selected provider still permits a valid coordinate patch. A contact already admitted before deletion cannot be recalled; its result is discarded. Admission reads fresh provider selection and profile authority under their row locks after legacy preparation. Deselection or consent withdrawal committed before admission consumes no provider quota and makes no provider contact; a later change does not refund an already committed admission.

Activity writes resolve a current positive activityTypeId first, then a permanent redirect for IDs removed during catalog duplicate cleanup. The stored FK is the survivor even after a client catalog refresh; queued Mobile IDs do not expire. PUT’s activityName fallback uses the database’s NFC/full Unicode-folded identity with surrounding whitespace trim only and never creates entries. Clear flags, absent/null values, unknown and negative offline IDs retain the existing action semantics, as do first-writer replay, ownership and response envelopes. Catalog lookup overload still returns the existing 429/503 response with Retry-After.

DELETE /api/location/{id} uses bearer ownership and returns an ordinary success object with id; a missing/non-owned row is 404. For example, a bearer-only correction can send PUT /api/location/123 with { "notes": "Corrected note" }. It does not grant access to another account’s history or imply a bulk-delete API.

Owned Trip Region and Place integrations

The general Trip API supports selected bearer-owned plan operations used by Mobile and custom integrations. GET /api/trips lists the caller’s Trips; canonical public GET routes are discovery /api/trips/public, images /api/trips/public/{id}/images, detail /api/trips/{id} and boundary /api/trips/{id}/boundary. Public Trips permit anonymous reads. Discovery and images expose only public Trips, even to an owner; private images return 404. Private detail/boundary reads require a valid API token belonging to the Trip owner; cookie ownership alone is insufficient for those actions. Holding a token does not supply User cookie authority for the Vue editor.

Full detail reads use a fresh context and one read-only PostgreSQL Repeatable Read snapshot for the root, all children and private bearer token/account resolution. Public detail skips token lookup; its 200 response does not prove bearer identity. A concurrent visibility change can leave an in-flight read authorized for the old public snapshot, but cannot expose later private children through that authority. Absent Trips remain 404 before token lookup; private denials remain 401. Lookup admission retains 429/503 with Retry-After: 5.

The complete supported graph is eagerly projected and buffered with the configured MVC JSON options before a successful response. Missing required loaded content, unusable Area/custom-route geometry or projection failure gives a whole-read 500 (The trip could not be loaded.), without a reduced graph. Legitimate empty collections, unlocated Places, nullable endpoints/orders, Area holes, cross-Region waypoints, fractional measurements and null legacy provenance remain supported. centerLat and centerLon remain independently numeric or null, including both asymmetric combinations. GET performs no repair, domain save or provider fetch.

A linked 30-second budget covers connection acquisition, queries, projection and JSON preparation, with transaction-local transaction_timeout = '30s' as a server backstop. The snapshot and fresh context end before response transfer; cleanup has an independent five-second budget. Deadline/database failures cannot qualify as

  1. Caller cancellation propagates from the action; delivery after disconnect is not guaranteed. This read does not repair already committed writer anomalies or establish a client revision/conditional-write protocol (#853).

Trip detail exposes the stored updatedAt as a best-effort freshness hint: a changed value can justify refetching, but equality does not prove every nested field is unchanged. Wall-clock stamping is not a universal, guaranteed monotonic aggregate revision. No version, ETag, optimistic concurrency condition or synchronization cursor is promised.

Operation Route JSON input
Create Region POST /api/trips/{tripId}/conditional/regions Complete original Region/canonical identity/order set and value; see checked creation.
Update Region PATCH /api/trips/{tripId}/conditional/regions/{regionId} Authored name/notes with matching client originals; see Region scalars.
Order Regions Prepared PUT /api/trips/{tripId}/conditional/regions/order Complete original normal-Region material/vector and canonical identity/order; see protected Region ordering.
Delete Region Preview/confirmed POST /api/trips/{tripId}/regions/{regionId}/deletion-preview and /deletion Explicit four-way Place/Area disposition and expiring content approval; see Region deletion.
Create Place POST /api/trips/{tripId}/conditional/places Original destination and complete nullable Place order set; absent fallback additionally needs Region-set proof. See checked creation.
Update Place Conditional scalar PATCH or prepared coordinate/move/order actions Supported Place contracts; unconditional Place PUT rejects without mutation.
Delete Place Preview/confirmed POST /api/trips/{tripId}/conditional/places/{placeId}/deletion-preview and /deletion Content-bound confirmation even without dependents; see Place deletion.

The supported Web and bearer creation contracts, examples, acknowledgement rules, atomic footprints and unknown-outcome holds are documented in checked Region/Place creation (BD2-06/07). Both older bearer POSTs reject unconditional creation. Current Web uses the protected creation URLs; no new client falls back to unconditional creation. Server-generated identities are not durable retry receipts.

Trip/Region/Place IDs are GUIDs. The authenticated account must own the Trip and related Region. Same-Trip Place moves, coordinates and complete ordering use prepared conditional actions. Web conditional Region center edits use one center coordinate pair with its original evidence. Values must be finite and in inclusive WGS84 bounds. Bearer Region scalars select Name/Notes only. Returned coordinate arrays use [longitude, latitude]; absent or historically nonfinite Points project as null without repairing storage.

PostgreSQL enforces one exact-name Unassigned Places Region per Trip, independently of owner/order. Creation preserves existing canonical stored order; historical repair requires separate authority. See database rollout.

Web Trip creation commits the Trip and one server-owned order-0 fallback in one graph save. Form child graphs are excluded. Failed/cancelled graphs are cleared before the normal error audit can save again. Image warm-up runs after commit; a scheduling failure retains the created Trip and redirect with a warning. Native/generic imports reject duplicate exact-name fallbacks with the existing 422 validation_failed envelope before replacement or graph mutation. Cloning rejects such malformed source aggregates before creating a destination. Native v2/v3 and clone compatibility still permit missing fallbacks; existing canonical orders, metadata and ordinary Region names retain their meaning.

Unconditional Region PUT rejects all owned payloads with 409 mutation-originals-required, including canonical metadata and empty/no-op requests. Supported bearer conditional Region editing permits canonical Notes while protecting its name and order. The Web editor retains its restriction on canonical Region edits; canonical deletion remains forbidden (403).

Place scalar edits use selected originals and exact dirty intent. Coordinate, move, complete vector and combined Web contracts, atomic effects and safe conflict/unknown semantics are documented in conditional Place structure. No supported client falls back to an unconditional Place writer.

Place and Region destruction require nonmutating preview followed by an explicit confirmed POST, including targets without dependents. Approval binds material content, ordering and actual lifecycle effects, and expires after ten minutes. The participating unconditional DELETE adapters reject without mutation; an identity-only header cannot authorize them. Known precommit rejection requires fresh review; lost/unclassified replies remain unknown without blind replay. See Place deletion and mobile requests, four-way Region deletion, checked creation examples and prepared Place examples.

Supported bearer scalar edits use the original-aware Trip metadata, Region, Area Notes and Segment Notes contracts below. Public discovery, public-Trip cloning, Mobile Group, recent-visit, hosted-routing and SSE families retain membership, ownership, visibility and provider gates. These are limited API contracts, not unrestricted account access or full Web editor parity; Web invitations, provider backfill, account/admin tools and editor mutations keep their cookie/role/antiforgery rules.

Protected Trip metadata (#853 Slice 3)

Two opt-in routes share one PostgreSQL original-value comparison and dirty-field commit. This slice protects Trip Name/Notes and the existing Web Trip settings form; it does not qualify other entity mutations or finish #853.

Writer Protected route Selected fields and authority
Bearer PATCH /api/trips/{tripId}/conditional name, notes; current active incoming token and Trip owner. Cookie alone is insufficient.
Web settings PATCH /api/trips/{tripId}/editor/metadata/conditional name, notesHtml, isPublic, coverImage, center, zoom; active User cookie, ownership and antiforgery.

Send { "expected": { ... }, "changes": { ... } }. Include only authored fields, with a separately present original value for every effective changed field. Unknown, duplicate, wrongly cased or malformed keys and empty effective changes return 400 mutation-request-invalid. New names are trimmed and contain 1–200 characters. Omitted/null desired Notes means Unchanged; present "" means Clear. Original Notes can be null, and null differs from empty. Rich HTML uses the existing shared sanitization/normalization; data-image input is rejected.

For example, clearing originally null Notes uses:

PATCH /api/trips/{tripId}/conditional
Authorization: Bearer <token>
Content-Type: application/json

{"expected":{"notes":null},"changes":{"notes":""}}

Web metadata responses add originalValues, preserving normalized nullable notesHtml, exact original cover URL and both nullable center coordinates. The existing non-null display notesHtml and all other display fields remain compatible. Pin each edited field’s originalValues when editing starts; never infer an original from an empty display default or replace it merely because props refreshed. Web expected/changed cover uses {rawUrl: ...} or null. Expected center can contain independently nullable latitude/longitude; a desired center is a finite WGS84 pair or null to clear both. Zoom remains nullable or 0–19. Publication becoming private atomically disables ShareProgress; both existing share-progress toggles guard their final write against current publication.

Comparison uses fresh state after the final Trip row lock, with current owner, active account and, for bearer, a still-valid locked incoming credential. A stale selected field returns 409 mutation-base-stale, with zero domain writes or thumbnail/warm-up effects and no disclosure of current values. Disjoint dirty fields survive in either save order. Definitively unapplied database contention can return mutation-contention. Missing or non-owned targets remain concealed 404; unavailable authentication is 401. Existing role/antiforgery denials remain.

Bearer 200 contains contract: "bd2-1", success, applied and canonical trip: {id,name,notes,updatedAt} after commit. An equal desired value can return applied:false; it does not prove a new Clear occurred. Web retains the existing EditorMutationResult metadata/affected slices, URLs and warnings. Thumbnail invalidation and the respective Web/bearer image warm-up policies follow a confirmed successful commit. updatedAt is advisory and is never the condition. These responses are neither a coherent full Trip baseline nor durable receipts.

On conflict, the current Vue form retains authored fields, original comparisons and the visible error through refreshes. New edits or map captures made during a save survive its older response. An explicit resolution must review and adopt a new base and compare again; there is no force-overwrite or automatic rebase. A missing route/original-value contract holds the save without falling back to an unconditional endpoint. A lost reply or cancellation does not prove no effect and must not trigger blind replay.

The metadata owner retains an immutable snapshot of each submitted conditional request separately from confirmed originals and the latest authored fields (#853 F2). Pending or uncertain metadata keeps the draft/navigation obligation even when a newer reversal equals the old baseline. Transport/server failures, unreadable bodies and unusable or wrong-Trip acknowledgements show Trip settings change not confirmed. Save, Save & Exit and Cancel / Reset are held; parent refresh and closing/reopening settings cannot erase the attempt or latest values. A fresh read cannot establish that the predecessor is terminal. A valid reply correlated with that request advances confirmed originals and preserves newer edits, which can then be saved explicitly. Before adoption, the reply must match the immutable submitted Name, cover, publication, center and zoom changes with .NET whitespace trimming for Name/desired cover, nullable center clearing and the derived sharing disable when making private. Unsubmitted fields may reflect disjoint concurrent edits; newer draft values never validate an older attempt. Mismatches in these comparable fields retain uncertainty without replay or Saved. Notes must supply typed, complete originals consistent with their display projection, within the same Trip and correlated immutable attempt. The client and server HTML policies differ (for example, Quill/client allow sms: links while the server removes their href; tag and image-proxy canonicalization also differ). The response supplies no canonical submitted-Notes echo or digest, so it does not establish exact submitted-Notes equality for nonempty HTML. An explicit empty Clear must return empty Notes. Otherwise, client validation uses the existing display/original consistency check rather than duplicating the server sanitizer. For nonempty submissions, an internally consistent unrelated same-Trip Notes reply remains a synthetic M1 limitation; no emitting server path is demonstrated. Malformed or inconsistent Notes replies still hold before adoption. Cover display validation follows the server’s Unicode whitespace rule: U+0085 projects to null, while U+FEFF remains material. Blank stored covers display as null, separately from exact originalValues.coverImage; acknowledgement validation never normalizes those originals. These checks are defensive client validation, not evidence that the server emits inconsistent submitted values. Validation, authentication, authorization, unavailable-route/scope and conditional-conflict rejections from this endpoint are definitive no-write failures and remain correctable. No automatic retry or synthesized originals are used.

Leave without saving explicitly abandons retained metadata and other unsaved edits after a warning: the earlier request may still finish, and leaving does not undo it. Staying retains the hold. Refresh/reset is not abandonment. This state exists only in the mounted editor; leaving/reloading loses it, with no durable cross-reload recovery. A superseded Save & Exit continuation cannot navigate over another draft, even when its delayed acknowledgement succeeds. The conditional metadata endpoint and database transaction are unchanged.

The existing Web Trip Settings sharing owner also retains its submitted sharing attempt separately from the confirmed baseline and latest checkbox choice (#853 C3). An in-flight or uncertain attempt keeps navigation protection active even if a newer disable equals the old baseline. Network/server failures or an unusable sharing acknowledgement show Sharing change not confirmed; subsequent Save and Save & Exit cannot dispatch again, report Saved or navigate. Metadata refresh, reset and closing/reopening the settings surface do not retire that attempt or overwrite the latest choice. Explicit validation/authentication or authorization rejection remains an ordinary correctable save failure.

The bounded available action is Leave without saving, with confirmation that the earlier request may still finish and the retained choice will be lost. Staying preserves the hold; leaving is deliberate abandonment, not reconciliation or remote rollback. There is no cross-page/reload persistence, retry, corrective replay, receipt or terminality guarantee. A fresh read, timeout, fetch abortion or reopened surface does not supply terminal evidence. A genuine successful sharing reply advances the baseline while retaining a newer disable; its next explicit save still works. The sharing endpoint and database write are unchanged. Discarding or closing the initiating settings session revokes its pending Save & Exit continuation without retiring the sharing attempt or rejecting a valid acknowledgement. A still-current save uses the shared navigation check against all current unsaved editor drafts before exiting.

Rejected unconditional writers (#853 reconciliation F1)

The coordinated new backend/Web and rewritten mobile release uses the protected contracts below. Each retained route in this inventory rejects an authenticated, owned request with 409 mutation-originals-required, including empty/no-op payloads. Rejection performs no domain save, freshness stamp, warm-up or thumbnail invalidation and returns no current private values. Missing or foreign scope is concealed with 404; invalid/missing authority still rejects, and Web mutations still require the User cookie and antiforgery. Malformed bearer DTOs can reject with 400 before the action. No originals are synthesized or unprotected fallback performed.

Rejected route Supported original-aware replacement
PATCH /api/trips/{tripId}/editor/metadata PATCH /api/trips/{tripId}/editor/metadata/conditional
PUT /api/trips/{tripId} PATCH /api/trips/{tripId}/conditional
PUT /api/trips/{tripId}/editor/regions/{regionId} PATCH /api/trips/{tripId}/editor/regions/{regionId}/conditional
PUT /api/trips/regions/{regionId} PATCH /api/trips/{tripId}/conditional/regions/{regionId}
PUT /api/trips/{tripId}/editor/areas/{areaId} PATCH /api/trips/{tripId}/editor/areas/{areaId}/conditional
PUT /api/trips/areas/{areaId} PATCH /api/trips/{tripId}/conditional/areas/{areaId} (Notes)
PUT /api/trips/segments/{segmentId} PATCH /api/trips/{tripId}/conditional/segments/{segmentId}

The current Vue callers already select these protected routes. Trip publication, cover and map view; Region cover and center; and Area Name, Fill and Polygon remain authored through their existing Web conditional contracts. Original-aware complete Region ordering and existing Area ordering remain separate actions. Segment route/Notes combined saves retain their single protected transaction. Rejecting these seven adapters does not widen bearer fields or change excluded standalone Area geometry behavior. There is no old-client compatibility adapter or notice.

F2’s mounted-client correction is described above. The F1 route disposition alone does not accept C1/C2/C5 or close #853; fresh independent review remains required.

Protected Region scalars (#853 Slice 4)

The current Web Region form and opt-in bearer scalar action share one locked compare/apply authority. Only authored fields enter changes; every effective field needs its pinned original in expected. UpdatedAt is not a condition.

Writer Protected route Allowed fields and authority
Bearer PATCH /api/trips/{tripId}/conditional/regions/{regionId} name, notes; active incoming bearer, active account and owned Trip/Region.
Current Web form PATCH /api/trips/{tripId}/editor/regions/{regionId}/conditional name, notesHtml, coverImage, center; User cookie, active account, ownership and antiforgery.

For example, changing Notes without replaying an untouched Name, cover or center:

{"expected":{"notes":null},"changes":{"notes":"<p>New notes</p>"}}

Omitted/null desired Notes means Unchanged; present "" means Clear. Originals distinguish null from empty and use the shared normalized rich HTML. Names are trimmed and nonblank; ordinary Regions cannot adopt Unassigned Places, ignoring case and surrounding whitespace. Bearer can edit the reserved Region’s Notes only. The reserved Web Region remains noneditable. Order and children are never writable through these scalar operations.

The Web Region DTO adds originalValues containing name, nullable notesHtml, the exact stored coverImage: {rawUrl} or null, and center: {latitude,longitude} or null. Existing display fields remain compatible. Cover changes use a trimmed absolute HTTP(S) URL or null to clear; center changes use a finite WGS84 pair or null to clear. The Vue draft owner pins originals per field, adopts only clean fields on refresh, and retains newer authored edits across a delayed reply. Conflict feedback and dirty originals survive unrelated updates. Reset/discard explicitly adopts the current visible base; the next save still compares it.

The server locks Region then parent Trip in lifecycle order, uses fresh persisted values, and locks/revalidates the current active account and incoming credential after waits. It updates only requested Region properties and a current-safe Trip timestamp in one transaction. No Region order, child, route, provenance or measurement is changed. Missing/foreign scope returns concealed 404; changed original parent scope returns 409 mutation-scope-stale; stale selected fields return 409 mutation-base-stale; confirmed unapplied contention can return mutation-contention. Invalid/duplicate/unknown/ambiguous input is 400. Rejected operations perform no image/cache work. Bearer retains its existing post-commit warm-up policy; Web Region saves retain their existing lack of external warm-up.

Bearer success returns contract: "bd2-1", success, applied and a canonical region DTO. An already equal desired value can return applied:false. Web keeps EditorMutationResult with the changed Region in data and affected.regions. These are committed operation results, without durable receipts. Lost replies and cancellation remain uncertain to the client and never authorize blind replay. Within the current Region editor session, an uncertain save holds the draft and further saves until explicit review/reset or discard. It supplies no durable receipt, cross-reload recovery or settlement of the separate Trip-sharing debt.

Writer disposition: the bearer and complete Web Region PUTs reject without mutation; see the seven-route inventory. Current Vue uses the conditional PATCH and fails closed for missing original evidence. Region ordering uses the paired BD2-11 contract; individual unconditional order writes reject. Protected canonical Region rules and current-safe freshness are unchanged. No native mobile qualification is claimed.

Protected Area editing (#853 Slice 5)

Current Web Area saves and opt-in bearer Notes share one PostgreSQL compare/apply boundary. Supply original Region identity and only authored dirty fields:

Writer Protected route Selected fields and authority
Bearer PATCH /api/trips/{tripId}/conditional/areas/{areaId} notes; active incoming token, account and current owned Area → Region → Trip.
Current Web form PATCH /api/trips/{tripId}/editor/areas/{areaId}/conditional name, notesHtml, fillHex, geometry; active User cookie, current ownership and antiforgery.
{"expected":{"regionId":"original-region-guid","notes":null},"changes":{"notes":"<p>Authored notes</p>"}}

Each effective changed field requires an independently present original. Notes originals use the shared normalized rich HTML while preserving null versus empty; desired Notes omitted/null means Unchanged, "" means Clear. Data-image sources are rejected and desired rich HTML is sanitized. Web Name remains trimmed and nonblank; Fill uses the existing #RRGGBB validation and lowercase persistence. Unknown/duplicate fields, empty effective changes and writable parent/order fields are rejected with 400 before any domain writes or cache work.

The compatible editor Area projection adds originalValues: original regionId, name, nullable notesHtml, exact stored nullable fillHex, and geometry comparison evidence. Display Notes/Fill keep their existing empty/default values. Geometry evidence is area-polygon-1: followed by uppercase SHA-256 of fixed little-endian EWKB with SRID. It retains every XY double, exterior/interior ring, hole, vertex order and orientation; no rounding or topological normalization is performed. It is a narrow persisted Polygon comparison, not authorization or an aggregate revision. For a geometry change, put that original string in expected.geometry and the full authored GeoJSON Polygon in changes.geometry. Desired geometry retains finite paired [longitude,latitude] WGS84 positions, closed rings and existing Polygon validation. Notes-only saves need no geometry evidence and neither replace nor normalize the current Polygon.

The transaction locks Area → current Region → current owned Trip, consistent with #848 keep/delete ordering, then locks/rechecks the active account and incoming bearer credential after waits. It compares original parent identity and selected fields using fresh untracked rows. Only selected Area columns plus a current-safe Trip UpdatedAt are written in the same commit; sibling orders, Region scalars, Places, Segments, waypoints, provenance, measurements and visits remain unchanged. An Area kept in canonical Unassigned Places remains editable; its old parent base returns 409 mutation-scope-stale, never a move back. Stale selected fields return 409 mutation-base-stale; confirmed unapplied contention can return mutation-contention. Missing/foreign scope is concealed with 404 and unavailable current authority returns 401. Known failures write no domain rows or warm-up work.

Bearer 200 returns contract: "bd2-1", success, applied and canonical area; Web retains EditorMutationResult, with the Area in data and affected.areas and no order replacement. A desired value already equal to current can return applied:false. Bearer image warm-up runs only after an applied commit; Web Area saves continue to schedule no external work. These are committed results, without durable receipts: cancellation or commit/reply loss remains uncertain to callers.

The Vue Area owner pins field originals independently of props, adopts only clean fields on refresh and retains newer draft/map Polygon edits and explicit reversals across a delayed acknowledgement. Parent drift cannot silently adopt a new Region. Conflicts/validation preserve drafts; uncertain results hold further saves in the current session until explicit current-state review/reset or discard. Neither reset nor discard proves that an earlier uncertain write failed. Existing polygon map-work Done/Cancel and Area create/delete/order workflows remain in place. Missing originals or an unsupported conditional route fail closed with no PUT fallback.

Writer disposition: bearer and complete Web Area PUTs reject without mutation; see the seven-route inventory. The separate cookie/antiforgery PUT /api/trips/{tripId}/editor/areas/{id}/geometry remains ownership/parent scoped and writes only Geometry plus Trip freshness, preserving Notes. It remains unprotected for competing Geometry replacements, as excluded from the fixed Area Notes operation. Current-form combined scalar/ Polygon saves use the conditional route atomically. No schema migration or native mobile qualification is included; #853 remains incomplete and open.

Protected Place scalars (#853 Slice 7)

Writer Route Authority / write intent
Opt-in bearer PATCH /api/trips/{tripId}/conditional/places/{placeId} Active incoming bearer and account; selected name, notes, iconName, markerColor.
Current Web scalar-only form PATCH /api/trips/{tripId}/editor/places/{placeId}/conditional User cookie plus antiforgery and current active owner; notesHtml maps to the same canonical Notes.
Current combined Web edit PUT /api/trips/{tripId}/editor/places/{placeId} Prepared original-aware lifecycle save, including authored scalars.
Unconditional bearer Place PUT PUT /api/trips/places/{placeId} Rejected with mutation-originals-required; supported callers use conditional actions.
{
  "expected": {
    "regionId": "f948ab48-615c-4eb4-abbb-fbf226402281",
    "notes": null,
    "iconName": null
  },
  "changes": {"notes": "<p>Authored notes</p>", "iconName": "marker"}
}

expected.regionId is required original-parent evidence, never writable. Each selected scalar needs its own present original. Notes compare the exact published RichNotes.Normalize value, preserving null versus empty; icon/color originals preserve nullable storage rather than rendered marker / bg-blue defaults. Desired Notes omitted/null means Unchanged; "" clears; nonempty HTML is sanitized. Names are trimmed, nonblank and contain no null characters; PostgreSQL text has no length cap. Invalid Unicode escapes also reject with 400. Desired icons/colors must belong to the existing editor catalogs. Explicit marker / bg-blue resets are valid; omitted fields never normalize storage. Duplicate, unknown, wrong-case, nested, malformed, structural or empty-effective requests return 400 without effects. No protected client falls back to PUT.

Both PATCH routes share one PostgreSQL transaction: Place → live Region → Trip, then current account and incoming bearer credential validation after waits. This uses the child/parent suffix of the lifecycle/Segment lock order without taking profile/Segment locks. Compare selected originals and original parent, update only authored scalar columns plus current-safe maximum Trip UpdatedAt, and commit once. Same-field or parent drift is a nonleaking, definitively unapplied 409; foreign/missing Place/Region/Trip remains concealed with 404. Disjoint edits survive both orders. Unrelated coordinates, Address, enrichment, routes, orders and visits do not cause scalar conflicts while the parent remains valid. #848 keep-to- Unassigned invalidates the old parent proof; an explicitly reviewed new parent can be used there.

No geometry, Address, enrichment/provenance, membership/order, sibling, visit, Segment/waypoint, route/provider or measurement columns are written; no geocoding or provider lookup occurs. Existing bearer Place image warm-up runs only after an applied commit. The Web path retains its existing no-warm-up behavior. Bearer success returns contract: "bd2-1", success, applied, and canonical place (id, regionId, name, notes, iconName, markerColor). An accepted no-op returns applied:false without mutation SQL or warm-up. Web returns the existing complete EditorMutationResult<EditorPlaceDto> with matching data and affected.places, unchanged orders/IDs/visits and the normal warnings contract.

Editor Place publication adds originalValues with regionId, name, nullable canonical notesHtml, nullable iconName and nullable markerColor. The draft owner pins these separately from parent props, follows clean-field refreshes and retains dirty/newer intent and in-flight reversals. Coordinate picks stay temporary until Done; Cancel preserves clean refreshed coordinates and existing authored fields, so a subsequent Name-only save still uses conditional PATCH.

Before any parent/original/preview/Saved adoption, the client validates the matching complete Place, including nullable resolvedFeatureName / resolvedFeatureType when present. All Place saves reject Region/Area/Tag mutations and unrelated metadata, options or orders. Complete lifecycle saves may publish unique Place orders, dependent Segments, unique Segment order and visit progress; scalar PATCH cannot publish those effects. Create requires only its target Region’s Place order. Prepared combined updates require the exact submitted source/destination vectors and every changed Place DTO; unrelated-only, omitted-member, duplicate or oversized footprints reject. Source membership and the target’s full material original must match the edited base. Coordinate effects require complete Segment order and visit progress projections, even when no Segments are affected. Malformed success replies retain intent and the prior comparison baseline under an unknown-outcome hold, without Saved or another HTTP attempt.

Late replies from discarded/superseded sessions are retired. Correctable 400/409 retains originals and content; network/408/500/incomplete-200 outcomes hold the draft against blind replay. Explicit Reset/discard never proves an earlier operation failed to commit; neither response nor UpdatedAt is a durable receipt.

Structural/combined Web saves now use the BD2-08/09/10 prepared lifecycle contract, compare applicable scalar/parent/content/order and material dependency originals, and commit once. Participating unconditional Place writers cannot bypass those protections. Final #853 C1/C2/C5 reconciliation, independent review and maintainer acceptance remain separate gates.

Protected Segment Notes (#853 Slice 6 and C4)

Writer Route Selected intent
Opt-in bearer PATCH /api/trips/{tripId}/conditional/segments/{segmentId} expected.notes and changes.notes; current active incoming token/account and owned Segment → Trip.
Current Web Notes-only form PATCH /api/trips/{tripId}/editor/segments/{segmentId}/conditional expected.notesHtml and changes.notesHtml; User cookie, current active account/ownership and antiforgery.
Current Web route/combined edit PUT /api/trips/{tripId}/editor/segments/{segmentId} Strict segment-route-notes-1 dialect with originalRouteToken and optional authored Notes; one existing transaction.
Rejected bearer writer PUT /api/trips/segments/{segmentId} 409 mutation-originals-required; no mutation. Use the protected Notes PATCH.
{"expected":{"notes":null},"changes":{"notes":"<p>Authored notes</p>"}}

Both PATCH routes use the same server-owned compare/apply authority. Originals must be independently present, exactly matching published normalized Notes, including the distinction between null and empty. Editor Segment publication adds originalValues.notesHtml; display notesHtml still defaults to "". Neither a preview nor UpdatedAt nor the aggregate token substitutes for the original Notes. Desired "" explicitly clears; null/omitted desired Notes means Unchanged, rejected here as an empty effective change. Unknown, duplicate, wrong-case or malformed keys and all non-Notes fields return 400. Desired HTML uses existing rich-Notes sanitation and data-image rejection.

The PostgreSQL transaction locks Segment → owned Trip, a subset of the existing profile → Segment → Place/Area → Region → Trip order used by route/lifecycle and #848 deletion owners. It rereads scope, account activity and incoming bearer credential after waits. Only Segment Notes and a current-safe maximum Trip UpdatedAt are written, without EF aggregate replay or provider/cache warm-up. Route geometry, waypoint rows/indices, profile, provider provenance, measurements, orders and unrelated Trip metadata remain intact. Stale Notes/scope returns a nonleaking known-unapplied 409; foreign/missing scope is concealed with 404; revoked/inactive authority cannot write. Precommit contention is reported as 409 only after successful rollback. Cancellation and lost commit/HTTP acknowledgements are not no-effect receipts and are never automatically retried.

Bearer success returns contract: "bd2-1", success, applied and segment containing exact id and canonical notes. A matching desired value can return applied:false without SQL writes. Web keeps EditorMutationResult with the complete Segment in data/affected.segments, no order replacement, exact nullable originals, a fresh opaque aggregate token and separate originalRouteToken. Tokens expose no raw xmin and are neither authorization nor durable receipts; token strings can differ even when their protected row versions match.

The Vue owner selects PATCH only for truly Notes-only intent, pins original evidence independently of display/props and retains later Notes edits/reversals across acknowledgements and reactive App/TripSidebar updates. Feedback and parent slice adoption belong to the originating editor session: explicit discard retires late success/failure and cannot overwrite a newer completed save. Uncertain undiscarded attempts remain dirty and held from blind replay; 400/409 remain correctable. Missing original evidence or unsupported PATCH fails closed. Explicit Reset/reload adopts current displayed server state; discard/reset does not prove an earlier uncertain attempt failed.

Complete create/PUT acknowledgements, including the confirmed route-clear PUT, must validate the entire Segment mutation envelope before adopting any draft, proposal, preview, parent or Saved state. The complete data must match the authoritative affected.segments entry, expected Trip/target, nullable Notes originals, opaque aggregate token and nonempty original route token. Any supplied Segment order must contain unique IDs; creation requires an order containing the created ID exactly once. The authoritative order is not compared with possibly stale client membership. Missing or inconsistent acknowledgements are unknown outcomes even with HTTP 200. Keep authored Notes, route/proposal, waypoints, manual duration and the original dependency token, and require explicit review/reset before another save.

If route, proposal or semantic edits begin during a Notes PATCH, their desired state and original dependency token remain pinned. The acknowledgement holds further saves for explicit review/reset rather than silently rebasing that route intent. The same hold preserves later route/proposal edits and reversals begun during a complete PUT, including through delayed acknowledgements and parent refresh. The older acknowledgement can publish its committed parent slice but cannot replace later intent or silently supply it a new route base.

Independent Segment route and Notes (#853 package R / C4)

The current Web editor sends route/combined edits to the existing PUT using this strict dialect. Creation retains its complete-draft contract. No bearer route authoring endpoint is added.

{
  "contract": "segment-route-notes-1",
  "originalRouteToken": "server-issued-original-route-proof",
  "fromPlaceId": null,
  "toPlaceId": null,
  "waypointPlaceIds": [],
  "waypointRouteVertexIndices": [],
  "mode": null,
  "estimatedDistanceKm": null,
  "estimatedDurationMinutes": 12,
  "estimatedDurationSource": "Manual",
  "route": null,
  "expected": {"notesHtml": null},
  "changes": {"notesHtml": "<p>Independently authored Notes</p>"}
}

originalRouteToken is purpose/version/user/Trip/Segment-bound evidence from the original authoritative view. Its fixed material projection covers Segment scope and order, endpoints, ordered waypoint identities/positions/vertex indices, geometry, mode/profile identity, exact saved distance/duration/source, complete provider provenance, and saved anchor Region/Trip scope and geometry. It excludes Notes and physical xmin. Current profile eligibility/calculation inputs and proposed anchors still undergo the existing locked validation. A later parent refresh or proposal preparation never exchanges an old draft’s route base.

Notes use their independently pinned exact nullable originalValues.notesHtml. Omit expected/changes for route-only saves. Missing/omitted/null desired Notes is Unchanged and never assigns or normalizes stored Notes, including historical null. A desired string is Set; "" explicitly clears. Both require an independently present exact nullable original. Originals are not sanitized into a match; desired HTML uses the existing sanitizer and rejects embedded data images.

Route and authored Notes originals are compared before mutation in the existing Serializable transaction with the established lock order and current owner/account checks. Independent route and protected Notes edits from one original view can commit in either order, including queued lock waits. Combined edits commit route effects, Notes and required freshness once. Stale route/dependency evidence returns 409 segment-route-stale; stale authored Notes returns 409 segment-notes-stale. Either conflict, validation failure, proposal/consent failure or rolled-back application failure leaves both sides, children and freshness unchanged. Keep the draft and explicitly review/reset the authoritative base before resolving a conflict. No automatic rebase or PATCH-then-PUT decomposition occurs.

The new proposal-generation POST uses contract, originalRouteToken and providerMode, omitting aggregateConcurrencyToken. Generation, protected proposal contexts and final acceptance use the same route boundary. Notes alone preserves valid generation/proposal/clear authority; material drift still rejects. Geometry, indices, provider selection/credential/routing generations, catalog, trusted measurements/provenance and the proposal’s original ten-minute expiry remain protected. Destructive route clearing still requires explicit consent bound to the original route and exact submitted destructive intent, with its original five-minute expiry. Confirmation resubmits the captured request, retaining any later client draft. New evidence is never inferred from old contexts.

A maximum of three transaction attempts is allowed only for PostgreSQL-proven serialization/deadlock aborts after successful rollback and tracker recovery. Every restart retains identical intent, original route/Notes evidence and original proposal/consent expiry. Exhaustion is known-unapplied 409 segment-write-conflict. Transport, commit acknowledgement and HTTP uncertainty never trigger replay; unknown outcomes retain the client hold even when desired content is later reversed.

Compatibility: the new PUT rejects unsupported contracts, missing originals, unknown/duplicate/wrong-case fields and mixed legacy fields with 400. It omits the old required top-level notesHtml and aggregateConcurrencyToken, so older servers reject it rather than ignore its conditions. Missing new acknowledgement evidence also retains an unknown-outcome hold. Reload an older editor after upgrading; there is no unconditional fallback. Old complete-draft PUT requests and old proposal contexts retain conservative xmin protection, including Notes-first conflicts. Protected Notes PATCH and combined Web transaction semantics are unchanged. The unconditional bearer Notes PUT now rejects under the F1 disposition above. C4 retains its accepted evidence; C1/C2/C5 acceptance remains separate. #907 is separately complete via #917; C3 sharing lost-reply behavior was accepted via #919. No schema migration or native Mobile qualification is part of C4.

Saved Trip measurements and native KML

A Segment has one active saved distance/duration set. Automatic/Manual owns duration intent independently of route provenance. Protected provider proposal acceptance establishes provider estimates; ordinary editor JSON cannot assert provider authority. Qualified persistent provenance comes from the saved RouteProvider and persistent storage marker, with a supported mapping mode when present. Provider credentials, verification, availability and current planning speeds do not participate in reading, transferring or cloning an already accepted result.

New native Trip exports declare WayfarerSchemaVersion=3. Geometry, identities, waypoint integrity, ownership and numerical validity remain checked. The native version controls the following distinct representations:

Version Duration Measurement handling
v1 / recognized versionless legacy DurationMin Established Manual normalization and geometry/planning reconciliation.
v2 Integer DurationSeconds Established canonical compatibility checks; no reinterpretation as fractional time.
v3 Integer DurationTicks Exact active TimeSpan ticks; values must be nonnegative multiples of 10, matching PostgreSQL interval microseconds.

V3 writes DistanceKm with round-trip double precision and carries existing credential-free source metadata in RouteProvenance JSON: Provider, StorageMode, MappingMode, GeneratedAt, Attribution, InstructionsJson, TransportProfileId, ProviderConfigurationId, ProviderConfigurationVersion. Configuration/profile IDs in this provenance are historical scalar identities, not execution authority. The actual planning profile still resolves through TransportProfileKey. JSON members are bounded and duplicate/unknown claims, unsupported retention rights and invalid normalized instructions are rejected. Neither DurationTicks in older versions nor DurationSeconds in v3 is accepted.

V3 imports and clones preserve valid nulls, active values and Manual intent without numerical recalculation. Finite nonnegative historical estimates remain valid even when current geometry/planning produces different numbers. Missing source metadata stays unknown; numerical differences never invent provider attribution. Export does not rewrite existing rows. Manual geometry/anchor changes clear obsolete provider provenance and calculate the deliberate Wayfarer result; unchanged-route metadata edits preserve the saved set. Planning-speed mutations retain qualified provider estimates and reconcile Wayfarer estimates.

SSE families

SseService sends ordinary SSE data: frames containing JSON and optional comment heartbeats. Transport is best-effort and bounded; durable database state is authoritative.

Current web streams include:

Mobile equivalents require request-owned bearer authority for an active token owner; cookies alone do not authorize them. GET /api/mobile/sse/visits subscribes only to that owner’s channel. GET /api/mobile/sse/group/{groupId} retains the same active membership and protected delivery-time recipient/source privacy checks as the Web Group stream. Heartbeats bypass protected-frame leases; immediate credential revocation of an already-open stream is not promised.

Content-free reload hints are deliberately small. The exact current hint types are import-state, enrichment-state, invitation-state and membership-state. On a hint, fetch current authenticated state; do not infer durable details from the hint itself.

Location/group streams carry typed JSON events such as location updates/deletions and membership-related events. Consumers must tolerate reconnect/reload rather than treating SSE as a durable queue.

Client rules worth preserving

See Four-way Region deletion for the additive cookie/bearer preview and confirmed-operation contracts, including unknown outcomes.

Back to top