Explore: personal GPS heatmap tab under Athlete page

- bincio/explore.py: bake_tracks() simplifies GPS coords (RDP ε=0.0001),
  strips to [lng,lat], groups by sport type, writes per-handle tracks.json
- bake-tracks CLI command; render CLI calls _bake_tracks() after each build;
  strava_zip runs it once at end of batch
- /api/me/tracks endpoint serves the baked file; wipe_user cleans it up
- Explore.svelte: MapLibre full-screen map with sidebar — type pills,
  year/month date filter, Lines / Heatmap (global or by-type) view modes
- AthleteView: Explore tab visible only to profile owner (checks __bincioMe)
- Base.astro: fullscreen prop + Planner nav link
This commit is contained in:
Davide Scaini
2026-05-14 14:31:21 +02:00
parent 2daa66d7b0
commit 5307ae287c
10 changed files with 607 additions and 10 deletions
+16 -4
View File
@@ -4,6 +4,7 @@
import MmpChart from './MmpChart.svelte';
import RecordsView from './RecordsView.svelte';
import AthleteDrawer from './AthleteDrawer.svelte';
import Explore from './Explore.svelte';
import { isUnlisted, formatElapsed, formatDistance, sportIcon } from '../lib/format';
import { loadIndex, loadAthlete } from '../lib/dataloader';
@@ -21,9 +22,10 @@
let error: string | null = null;
let drawerOpen = false;
type Tab = 'power' | 'records' | 'segments' | 'profile';
type Tab = 'power' | 'records' | 'segments' | 'profile' | 'explore';
let activeTab: Tab = 'power';
let mounted = false;
let isOwner = false;
interface SegmentSummaryItem {
segment: { id: string; name: string; sport: string | null; distance_m: number };
@@ -80,9 +82,11 @@
}
onMount(async () => {
const TABS: Tab[] = ['power', 'records', 'segments', 'profile'];
isOwner = (window as any).__bincioMe === handle;
const TABS: Tab[] = ['power', 'records', 'segments', 'profile', 'explore'];
const rawTab = new URLSearchParams(window.location.search).get('tab');
activeTab = TABS.includes(rawTab as Tab) ? (rawTab as Tab) : 'power';
const resolved = TABS.includes(rawTab as Tab) ? (rawTab as Tab) : 'power';
activeTab = (resolved === 'explore' && !isOwner) ? 'power' : resolved;
mounted = true;
// Resolve handle for the segments endpoint
@@ -140,12 +144,14 @@
return hi >= 900 ? `${lo}+ bpm` : `${lo}${hi} bpm`;
}
const TABS: { key: Tab; label: string }[] = [
const ALL_TABS: { key: Tab; label: string; ownerOnly?: boolean }[] = [
{ key: 'power', label: 'Power Curve' },
{ key: 'records', label: 'Records' },
{ key: 'segments', label: 'Segments' },
{ key: 'profile', label: 'Profile' },
{ key: 'explore', label: 'Explore', ownerOnly: true },
];
$: TABS = ALL_TABS.filter(t => !t.ownerOnly || isOwner);
</script>
{#if loading}
@@ -325,6 +331,12 @@
</div>
{/if}
<!-- Explore tab -->
{:else if activeTab === 'explore'}
<div style="height: calc(100vh - 200px); margin: 0 -1rem -1.5rem;">
<Explore {handle} {base} embedded={true} />
</div>
<!-- Profile tab -->
{:else if activeTab === 'profile'}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">