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:
- Cookie Identity: browser requests authenticated by ASP.NET Core Identity.
[Authorize]/role policies and normal MVC ownership checks apply. - API token/bearer: integration and mobile endpoints resolve the
Authorizationvalue through the request-scopedIncomingApiTokenResolver. Current Wayfarer tokens are generated once for the user, stored as SHA-256 hashes and require an active account. - Dual cookie/token endpoints: selected legacy/general API actions can resolve the active cookie user first and otherwise fall back to an API token. The endpoint still owns resource/role checks.
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
- Its independently obtained pages return the same three IDs exactly once in
order, retain the choice
scopeKey, and leave refreshed overview count3. 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
- 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:
GET /api/sse/stream/location-update/{username}: anonymous only when that user’s Timeline is effectively public and live. Eligibility is checked before subscription and again during protected delivery.GET /api/sse/import: cookie-authenticated caller’s import/enrichment reload-hint channel. It derives the protected channel only from the authenticatedNameIdentifier; callers cannot select another user, import or workflow channel. This content-free SSE emits onlyimport-stateandenrichment-statehints, and clients reload authoritative relational state rather than treating the event as display data.GET /api/sse/group-notifications: cookie-authenticated caller’s invitation/membership reload-hint channel.GET /api/sse/group/{groupId}: cookie Identity/NameIdentifierevent stream for active members of an unarchived Group, with heartbeat and protected delivery-time recipient membership and location/deletion source-audience checks. Group roles do not bypass location privacy.
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
- Propagate cancellation for abandoned HTTP work.
- Honor ownership/role failures rather than retrying them as transient errors.
- Honor
Retry-Afteron bounded admission failures. - Reuse a GUID
Idempotency-Keyonly for retries of the same location-ingestion operation. - Treat SSE as a presentation signal over durable HTTP/database state.
- Do not log bearer tokens, provider credentials or private request payloads while diagnosing failures.
See Four-way Region deletion for the additive cookie/bearer preview and confirmed-operation contracts, including unknown outcomes.