feat(hikes): anonymize GPX timestamps to 08:00 today
Re-base every track + image timestamp so each hike starts at 08:00 on the build date, preserving all relative timing (total duration, per-stage gaps, photo "nach X"). The per-hike track JSON is the single source for the page metrics and the client-built GPX download, so both come out anonymized; the real recording times stay only in the private source track.gpx. Also close two stale-data leaks that would otherwise still expose real times: sweep prior-build track.*.json (keep only the current hash) and remove orphan slug dirs from static/ (renamed/deleted hikes).
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homepage",
|
"name": "homepage",
|
||||||
"version": "1.85.3",
|
"version": "1.86.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1064,6 +1064,34 @@ async function buildHike(slug: string, cache: GeocodeCache): Promise<HikeManifes
|
|||||||
const gpxImageCount = Object.keys(gpxImageRefs).length;
|
const gpxImageCount = Object.keys(gpxImageRefs).length;
|
||||||
console.log(`[build-hikes:${slug}] parsed GPX (${track.length} track pts, ${gpxStages.length} stage(s), ${gpxImageCount} image refs)`);
|
console.log(`[build-hikes:${slug}] parsed GPX (${track.length} track pts, ${gpxStages.length} stage(s), ${gpxImageCount} image refs)`);
|
||||||
|
|
||||||
|
// Privacy: anonymise absolute clock times. Re-base every timestamp so the
|
||||||
|
// hike starts at 08:00 "today" while preserving all relative offsets
|
||||||
|
// (total duration, per-stage gaps, photo "nach X"). This single shift flows
|
||||||
|
// into the published track JSON, the page metrics, and the client-built GPX
|
||||||
|
// download — all of which read these timestamps — so the real recording
|
||||||
|
// times never leave the private source GPX. Track points are shared with
|
||||||
|
// `gpxStages` (flatMap keeps object identity), so stages rebase too.
|
||||||
|
{
|
||||||
|
let firstTs: number | null = null;
|
||||||
|
for (const p of track) {
|
||||||
|
if (typeof p.timestamp === 'number') {
|
||||||
|
firstTs = p.timestamp;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (firstTs !== null) {
|
||||||
|
const anchor = new Date();
|
||||||
|
anchor.setHours(8, 0, 0, 0);
|
||||||
|
const offset = anchor.getTime() - firstTs;
|
||||||
|
for (const p of track) {
|
||||||
|
if (typeof p.timestamp === 'number') p.timestamp += offset;
|
||||||
|
}
|
||||||
|
for (const ref of Object.values(gpxImageRefs)) {
|
||||||
|
if (typeof ref.timestamp === 'number') ref.timestamp += offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Per-stage stats + flat-track index ranges. Indices are contiguous and
|
// Per-stage stats + flat-track index ranges. Indices are contiguous and
|
||||||
// disjoint (endIdx + 1 === next.startIdx).
|
// disjoint (endIdx + 1 === next.startIdx).
|
||||||
const stageEntries: HikeStage[] = [];
|
const stageEntries: HikeStage[] = [];
|
||||||
@@ -1294,6 +1322,22 @@ async function buildHike(slug: string, cache: GeocodeCache): Promise<HikeManifes
|
|||||||
await fs.writeFile(trackFile, trackJson);
|
await fs.writeFile(trackFile, trackJson);
|
||||||
console.log(`[build-hikes:${slug}] wrote track.${trackHash}.json (${trackJson.length} bytes)`);
|
console.log(`[build-hikes:${slug}] wrote track.${trackHash}.json (${trackJson.length} bytes)`);
|
||||||
|
|
||||||
|
// Sweep stale track.*.json from earlier builds. Without this the previous
|
||||||
|
// file lingers in static/ and ships on deploy — and since timestamps are
|
||||||
|
// now anonymised, an old file would still expose the real recording times
|
||||||
|
// at its (guessable) URL.
|
||||||
|
{
|
||||||
|
const dir = path.dirname(trackFile);
|
||||||
|
const keep = path.basename(trackFile);
|
||||||
|
const stale = (await fs.readdir(dir)).filter(
|
||||||
|
(f) => /^track\..*\.json$/.test(f) && f !== keep
|
||||||
|
);
|
||||||
|
await Promise.all(stale.map((f) => fs.unlink(path.join(dir, f)).catch(() => {})));
|
||||||
|
if (stale.length > 0) {
|
||||||
|
console.log(`[build-hikes:${slug}] removed ${stale.length} stale track JSON(s)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const date = typeof fm.date === 'string'
|
const date = typeof fm.date === 'string'
|
||||||
? fm.date
|
? fm.date
|
||||||
: (typeof fm.date === 'number' ? new Date(fm.date).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10));
|
: (typeof fm.date === 'number' ? new Date(fm.date).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10));
|
||||||
@@ -1378,6 +1422,31 @@ async function main() {
|
|||||||
if (entry) hikes.push(entry);
|
if (entry) hikes.push(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sweep whole orphan slug dirs from static/ — e.g. a renamed or deleted
|
||||||
|
// hike. Otherwise its old per-slug track JSON (with the real, un-anonymised
|
||||||
|
// recording times) keeps shipping at a guessable URL. Keep current content
|
||||||
|
// slugs and any special "_*" entry (e.g. the overview hero). Guarded by a
|
||||||
|
// non-empty slug list so a failed content read never wipes everything.
|
||||||
|
if (slugs.length > 0) {
|
||||||
|
try {
|
||||||
|
const keep = new Set(slugs);
|
||||||
|
const present = await fs.readdir(STATIC_DIR, { withFileTypes: true });
|
||||||
|
const orphans = present.filter(
|
||||||
|
(e) => e.isDirectory() && !e.name.startsWith('_') && !keep.has(e.name)
|
||||||
|
);
|
||||||
|
await Promise.all(
|
||||||
|
orphans.map((e) => fs.rm(path.join(STATIC_DIR, e.name), { recursive: true, force: true }))
|
||||||
|
);
|
||||||
|
if (orphans.length > 0) {
|
||||||
|
console.log(
|
||||||
|
`[build-hikes] removed ${orphans.length} orphan slug dir(s) from static/: ${orphans.map((o) => o.name).join(', ')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// static/hikes may not exist yet on a clean checkout — nothing to sweep.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await saveGeocodeCache(cache);
|
await saveGeocodeCache(cache);
|
||||||
|
|
||||||
hikes.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0));
|
hikes.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0));
|
||||||
|
|||||||
Reference in New Issue
Block a user