fix: read activity shards in GET /api/feed; improve sync feedback
_merged/index.json is a shard manifest with activities:[] when the user
has >FEED_PAGE_SIZE activities. The endpoint now collects from all
index-{year}.json shard files before returning.
SyncResult gains a `total` field (activities received from server) so the
feed screen can distinguish "server returned nothing" from "all already
stored locally". Messages: "No activities on instance" / "Up to date (N)"
/ "X of N activities synced".
This commit is contained in:
+15
-4
@@ -537,13 +537,24 @@ async def get_token(login_req: LoginRequest, request: Request) -> JSONResponse:
|
|||||||
|
|
||||||
@app.get("/api/feed")
|
@app.get("/api/feed")
|
||||||
async def get_feed(user: User = Depends(_require_auth)) -> JSONResponse:
|
async def get_feed(user: User = Depends(_require_auth)) -> JSONResponse:
|
||||||
"""Return the authenticated user's activity summaries (mobile feed sync)."""
|
"""Return the authenticated user's activity summaries (mobile feed sync).
|
||||||
|
|
||||||
|
_merged/index.json is a shard manifest (activities: []) when the user has
|
||||||
|
more than FEED_PAGE_SIZE activities. Collect from all shard files.
|
||||||
|
"""
|
||||||
dd = _get_data_dir()
|
dd = _get_data_dir()
|
||||||
user_dir = dd / user.handle
|
user_dir = dd / user.handle
|
||||||
for index_path in (user_dir / "_merged" / "index.json", user_dir / "index.json"):
|
for index_path in (user_dir / "_merged" / "index.json", user_dir / "index.json"):
|
||||||
if index_path.exists():
|
if not index_path.exists():
|
||||||
index = json.loads(index_path.read_text())
|
continue
|
||||||
return JSONResponse({"activities": index.get("activities", [])})
|
index = json.loads(index_path.read_text())
|
||||||
|
activities: list[dict] = index.get("activities", [])
|
||||||
|
for shard in index.get("shards", []):
|
||||||
|
shard_path = index_path.parent / shard["url"]
|
||||||
|
if shard_path.exists():
|
||||||
|
shard_doc = json.loads(shard_path.read_text())
|
||||||
|
activities.extend(shard_doc.get("activities", []))
|
||||||
|
return JSONResponse({"activities": activities})
|
||||||
return JSONResponse({"activities": []})
|
return JSONResponse({"activities": []})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,12 @@ export default function FeedScreen() {
|
|||||||
setSyncing(false);
|
setSyncing(false);
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
setSyncMsg(result.error);
|
setSyncMsg(result.error);
|
||||||
|
} else if (result.total === 0) {
|
||||||
|
setSyncMsg('No activities on instance');
|
||||||
} else if (result.synced === 0) {
|
} else if (result.synced === 0) {
|
||||||
setSyncMsg('Already up to date');
|
setSyncMsg(`Up to date (${result.total} activities)`);
|
||||||
} else {
|
} else {
|
||||||
setSyncMsg(`${result.synced} ${result.synced === 1 ? 'activity' : 'activities'} synced`);
|
setSyncMsg(`${result.synced} of ${result.total} activities synced`);
|
||||||
}
|
}
|
||||||
setTimeout(() => setSyncMsg(null), 3500);
|
setTimeout(() => setSyncMsg(null), 3500);
|
||||||
}, [db]);
|
}, [db]);
|
||||||
|
|||||||
+6
-6
@@ -1,14 +1,14 @@
|
|||||||
import type { SQLiteDatabase } from 'expo-sqlite';
|
import type { SQLiteDatabase } from 'expo-sqlite';
|
||||||
import { getSetting, upsertRemoteActivity } from './queries';
|
import { getSetting, upsertRemoteActivity } from './queries';
|
||||||
|
|
||||||
export type SyncResult = { synced: number; error?: string };
|
export type SyncResult = { synced: number; total: number; error?: string };
|
||||||
|
|
||||||
export async function syncFeed(db: SQLiteDatabase): Promise<SyncResult> {
|
export async function syncFeed(db: SQLiteDatabase): Promise<SyncResult> {
|
||||||
const instanceUrl = (await getSetting(db, 'instance_url'))?.replace(/\/$/, '');
|
const instanceUrl = (await getSetting(db, 'instance_url'))?.replace(/\/$/, '');
|
||||||
const token = await getSetting(db, 'api_token');
|
const token = await getSetting(db, 'api_token');
|
||||||
|
|
||||||
if (!instanceUrl || !token) {
|
if (!instanceUrl || !token) {
|
||||||
return { synced: 0, error: 'No instance configured — add one in Settings.' };
|
return { synced: 0, total: 0, error: 'No instance configured — add one in Settings.' };
|
||||||
}
|
}
|
||||||
|
|
||||||
let resp: Response;
|
let resp: Response;
|
||||||
@@ -17,14 +17,14 @@ export async function syncFeed(db: SQLiteDatabase): Promise<SyncResult> {
|
|||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
return { synced: 0, error: 'Could not reach instance — check your connection.' };
|
return { synced: 0, total: 0, error: 'Could not reach instance — check your connection.' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resp.status === 401) {
|
if (resp.status === 401) {
|
||||||
return { synced: 0, error: 'Session expired — reconnect in Settings.' };
|
return { synced: 0, total: 0, error: 'Session expired — reconnect in Settings.' };
|
||||||
}
|
}
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
return { synced: 0, error: `Server error (${resp.status})` };
|
return { synced: 0, total: 0, error: `Server error (${resp.status})` };
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: { activities?: RemoteSummary[] } = await resp.json();
|
const data: { activities?: RemoteSummary[] } = await resp.json();
|
||||||
@@ -48,7 +48,7 @@ export async function syncFeed(db: SQLiteDatabase): Promise<SyncResult> {
|
|||||||
if (changed) synced++;
|
if (changed) synced++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { synced };
|
return { synced, total: activities.length };
|
||||||
}
|
}
|
||||||
|
|
||||||
type RemoteSummary = {
|
type RemoteSummary = {
|
||||||
|
|||||||
Reference in New Issue
Block a user