feat: Phase 0.5 — remote feed sync via Bearer token auth

Server (bincio/serve/server.py):
- Add _require_auth: accepts session cookie OR Authorization: Bearer token
- POST /api/auth/token: same as /api/auth/login but returns token in body
  (password used once, not stored; mobile stores only the session token)
- GET /api/feed: auth-gated; reads _merged/index.json for the user and
  returns the activities array as JSON

Mobile:
- db/sync.ts: syncFeed(db) fetches /api/feed, upserts each summary into
  local SQLite as origin='remote'; skips locally-imported activities
- db/queries.ts: add upsertRemoteActivity (INSERT ... ON CONFLICT DO UPDATE
  WHERE origin='remote' — never overwrites local imports); fix feed sort
  order to started_at DESC instead of insertion order
- settings.tsx: Connect section — password field (not persisted) + Connect
  button calls POST /api/auth/token and stores token; Disconnect clears it
- index.tsx: ↓ Sync button + pull-to-refresh both trigger syncFeed; cloud
  badge on remote activities; empty state updated
This commit is contained in:
Davide Scaini
2026-04-24 12:07:49 +02:00
parent 79c572bf8b
commit 44b2878b14
7 changed files with 9991 additions and 72 deletions
+19 -1
View File
@@ -52,7 +52,7 @@ export function useActivities(): ActivitySummary[] {
json_extract(detail_json, '$.duration_s') AS duration_s,
json_extract(detail_json, '$.elevation_gain_m') AS elevation_gain_m
FROM activities
ORDER BY created_at DESC
ORDER BY json_extract(detail_json, '$.started_at') DESC
`);
return rows;
}
@@ -85,6 +85,24 @@ export async function insertActivity(
);
}
export async function upsertRemoteActivity(
db: ReturnType<typeof useSQLiteContext>,
id: string,
detailJson: string,
): Promise<boolean> {
const now = Math.floor(Date.now() / 1000);
const result = await db.runAsync(
`INSERT INTO activities (id, source_hash, detail_json, origin, synced_at)
VALUES (?, ?, ?, 'remote', ?)
ON CONFLICT(id) DO UPDATE SET
detail_json = excluded.detail_json,
synced_at = excluded.synced_at
WHERE origin = 'remote'`,
[id, id, detailJson, now],
);
return result.changes > 0;
}
// ── Settings ───────────────────────────────────────────────────────────────
export async function getSetting(