1 — Timeseries split
- writer.py: timeseries is now written to {id}.timeseries.json as a separate file. The detail JSON gets a timeseries_url field instead. finalize_pending and cleanup_pending handle the extra file.
- merge.py (merge_one): symlinks the .timeseries.json file alongside the detail JSON. merge_all already handles it transparently (the .timeseries.json stem doesn't match any activity
ID in to_merge, so it falls through to the symlink branch).
- types.ts: timeseries is now timeseries?: Timeseries | null, and timeseries_url?: string | null added.
- dataloader.ts: new loadTimeseries(url, detailUrl, base) function that resolves paths correctly in both single- and multi-user modes (uses the fetched detail URL's directory as the base).
- ActivityDetail.svelte: loads timeseries separately after detail loads; uses detail.timeseries for IDB activities (embedded) or fetches via detail.timeseries_url for server activities. Charts show a pulse placeholder while loading.
2 — GZip
- GZipMiddleware (min 1 KB) added to both bincio/serve/server.py and bincio/edit/server.py — all API JSON responses are now gzip-compressed.
- For static files (the big timeseries JSONs), nginx should be configured with gzip on; gzip_types application/json application/geo+json; — no code change needed on the server side.
Net effect: opening an activity page now fetches ~1.4 KB (detail) instead of ~586 KB. The timeseries fetches ~60–150 KB gzip-compressed shortly after (it loads concurrently with the map rendering).
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
* for anything the user recorded or converted on this device).
|
||||
*/
|
||||
|
||||
import type { ActivityDetail, ActivitySummary, BASIndex } from './types';
|
||||
import type { ActivityDetail, ActivitySummary, BASIndex, Timeseries } from './types';
|
||||
import { listLocalActivities } from './localstore';
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
@@ -170,6 +170,40 @@ export async function loadActivity(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the timeseries for an activity. Called lazily when the charts section
|
||||
* is shown, so the initial detail load stays small (~1 KB instead of ~600 KB).
|
||||
*
|
||||
* @param timeseriesUrl Relative path from the detail JSON (e.g. "activities/id.timeseries.json")
|
||||
* @param detailUrl The URL from which the detail JSON was fetched — used to resolve
|
||||
* relative paths correctly in both single- and multi-user modes.
|
||||
* @param baseUrl Site base URL — fallback when detailUrl is empty
|
||||
*/
|
||||
export async function loadTimeseries(
|
||||
timeseriesUrl: string,
|
||||
detailUrl: string,
|
||||
baseUrl: string,
|
||||
): Promise<Timeseries | null> {
|
||||
try {
|
||||
let url: string;
|
||||
if (timeseriesUrl.startsWith('http')) {
|
||||
url = timeseriesUrl;
|
||||
} else if (detailUrl.startsWith('http')) {
|
||||
// detailUrl is absolute — resolve timeseries relative to its directory
|
||||
const dir = detailUrl.substring(0, detailUrl.lastIndexOf('/') + 1);
|
||||
// timeseriesUrl is "activities/id.timeseries.json" — strip leading "activities/"
|
||||
// because dir already ends with "activities/"
|
||||
const filename = timeseriesUrl.replace(/^activities\//, '');
|
||||
url = `${dir}${filename}`;
|
||||
} else {
|
||||
url = `${baseUrl}data/${timeseriesUrl}`;
|
||||
}
|
||||
return await fetchJSON<Timeseries>(url);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load athlete profile. Athlete data is not stored locally yet, so this is
|
||||
* always a network fetch with a graceful null on failure.
|
||||
|
||||
Reference in New Issue
Block a user