Sync the Fly App's waypoint routes through the API Server

Routes saved in the Fly App's mission library now sync to a new per-user
routes collection on the API Server, in the WaypointMissionSpec JSON
format. The library pulls on load and pushes on every save and delete,
merges last-writer-wins by updatedAt, keeps unacknowledged deletes as
tombstones, and keeps working offline. Each signed-in account has its
own copy on the device; routes saved while signed out go to the first
account that signs in.

The API Server gets list / get / upsert / delete route endpoints with a
PocketBase migration, and the Web App a read-only Routes view on the
dashboard (rebuilt dist).

Tested against a fake PocketBase and a fake server only; not yet tried
with a real sign-in on the tablet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
tajniak81andClaude Opus 5.5 committed 2026-10-20 17:26:45 +02:00
1 parent e49f318a0e
commit 93f4a580e9
18 files changed
+1525 -43

No files matched your search

@@ -0,0 +1,87 @@
/// <reference path="../pb_data/types.d.ts" />
// Creates the `routes` collection: the pilots' saved waypoint missions (the Fly
// App's mission library, GO 4-parity plan P7). Each row holds one route exactly
// as the Fly App's `WaypointMissionSpec.toJson()` writes it (format version 1)
// in `spec`, keyed by the app-generated `route_id` so a tablet can push and pull
// without knowing PocketBase record ids.
//
// Sync is last-writer-wins by the route's own `updatedAt` (copied into
// `route_updated_at` so the API Server can compare without parsing `spec`).
// Deletes are kept as tombstones (`deleted` = true) so a device that was
// offline learns about them instead of re-uploading the route.
//
// Like the logbook and documents, the collection is reached only through the
// API Server's superuser service account, so its API rules stay locked
// (superusers only); the API Server enforces per-role scoping in Go.
//
// Apply by copying into your PocketBase deployment's `pb_migrations/` directory
// and restarting. Written for PocketBase v0.22+/v0.23. Idempotent: the
// collection is created only if absent, so re-running is a no-op.
//
// Depends on 1720300200_add_organizations.js (organizations) and the `users`
// auth collection.
migrate(
(app) => {
// Idempotency guard.
try {
app.findCollectionByNameOrId('routes')
return // already present
} catch (_) {
// create below
}
const orgs = app.findCollectionByNameOrId('organizations')
const users = app.findCollectionByNameOrId('users')
const routes = new Collection({
type: 'base',
name: 'routes',
fields: [
// The Fly App's own id (e.g. "wp-m1x2y3z"); unique across the store.
{ name: 'route_id', type: 'text', required: true, max: 100 },
{ name: 'name', type: 'text', max: 200, presentable: true },
// The route itself, verbatim WaypointMissionSpec.toJson().
{ name: 'spec', type: 'json', maxSize: 2000000 },
// The spec's updatedAt (RFC 3339, UTC) — the last-writer-wins clock.
{ name: 'route_updated_at', type: 'text', max: 40 },
{ name: 'deleted', type: 'bool' },
{
name: 'owner',
type: 'relation',
required: false,
collectionId: users.id,
cascadeDelete: false,
minSelect: 0,
maxSelect: 1,
presentable: false,
},
{
name: 'organization',
type: 'relation',
required: false,
collectionId: orgs.id,
cascadeDelete: false,
minSelect: 0,
maxSelect: 1,
presentable: false,
},
{ name: 'created', type: 'autodate', onCreate: true, onUpdate: false },
{ name: 'updated', type: 'autodate', onCreate: true, onUpdate: true },
],
indexes: [
'CREATE UNIQUE INDEX `idx_routes_route_id` ON `routes` (`route_id`)',
'CREATE INDEX `idx_routes_owner` ON `routes` (`owner`)',
'CREATE INDEX `idx_routes_org` ON `routes` (`organization`)',
],
})
app.save(routes)
},
(app) => {
try {
app.delete(app.findCollectionByNameOrId('routes'))
} catch (_) {
// already gone
}
},
)