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>
88 lines
3.2 KiB
JavaScript
88 lines
3.2 KiB
JavaScript
/// <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
|
|
}
|
|
},
|
|
)
|