refactor(fitness): use:action → {@attach}, harden streamed-data error paths

Two custom Leaflet actions converted to attachments: renderMap is now
a factory returning an attachment, mountMap is the attachment itself.
Four call sites updated. use:enhance left alone — still the canonical
SvelteKit form-action API.

The stats page's three streamed Promise.resolve(...).then(...) chains
now log on rejection instead of silently swallowing errors. The muscle
heatmap {#await} block gained pending and catch branches with a
lang-aware error message.
This commit is contained in:
2026-04-30 19:13:06 +02:00
parent d8abcbf74b
commit 936c59debc
4 changed files with 45 additions and 30 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "homepage",
"version": "1.52.2",
"version": "1.52.3",
"private": true,
"type": "module",
"scripts": {
@@ -291,20 +291,20 @@
}
/**
* Svelte use:action — renders a Leaflet map for a GPS track
* @param {HTMLElement} node
* @param {any} params
* Attachment factory — renders a Leaflet map for a GPS track.
* @param {any[]} track
* @param {number} idx
* @returns {import('svelte/attachments').Attachment<HTMLElement>}
*/
function renderMap(node, params) {
const { track, idx } = params;
initMapForTrack(node, track, idx);
return {
destroy() {
function renderMap(track, idx) {
return (node) => {
initMapForTrack(node, track, idx);
return () => {
if (maps[idx]) {
maps[idx].remove();
delete maps[idx];
}
}
};
};
}
@@ -707,7 +707,7 @@
</button>
</div>
{#if exData.gpsTrack?.length >= 2}
<div class="track-map" use:renderMap={{ track: exData.gpsTrack, idx: exIdx }}></div>
<div class="track-map" {@attach renderMap(exData.gpsTrack, exIdx)}></div>
{/if}
{/if}
@@ -787,7 +787,7 @@
<span class="gps-stat elev-loss">-{elevStats.loss}{t('elevation_unit', lang)}</span>
{/if}
</div>
<div class="track-map" use:renderMap={{ track: ex.gpsTrack, idx: exIdx }}></div>
<div class="track-map" {@attach renderMap(ex.gpsTrack, exIdx)}></div>
{#if ex.gpsTrack.length >= 2}
{@const samples = computePaceSamples(ex.gpsTrack)}
@@ -131,9 +131,21 @@
let periodsData = $state([]);
/** @type {any[]} */
let sharedPeriodsData = $state([]);
$effect(() => { Promise.resolve(data.nutritionStats).then(v => { ns = v ?? {}; }); });
$effect(() => { Promise.resolve(data.periods).then(v => { periodsData = v ?? []; }); });
$effect(() => { Promise.resolve(data.sharedPeriods).then(v => { sharedPeriodsData = v ?? []; }); });
$effect(() => {
Promise.resolve(data.nutritionStats)
.then(v => { ns = v ?? {}; })
.catch(err => console.error('nutritionStats stream failed:', err));
});
$effect(() => {
Promise.resolve(data.periods)
.then(v => { periodsData = v ?? []; })
.catch(err => console.error('periods stream failed:', err));
});
$effect(() => {
Promise.resolve(data.sharedPeriods)
.then(v => { sharedPeriodsData = v ?? []; })
.catch(err => console.error('sharedPeriods stream failed:', err));
});
const hasSma = $derived(stats.weightChart?.sma?.some((/** @type {any} */ v) => v !== null));
@@ -498,8 +510,12 @@
<div class="section-block muscle-heatmap-block">
<h2 class="section-title">{t('muscle_balance', lang)}</h2>
{#await data.muscleHeatmap then muscleHeatmap}
{#await data.muscleHeatmap}
<div class="muscle-heatmap-pending" aria-hidden="true"></div>
{:then muscleHeatmap}
<MuscleHeatmap data={muscleHeatmap} />
{:catch}
<div class="muscle-heatmap-failed">{lang === 'de' ? 'Fehler beim Laden' : 'Failed to load'}</div>
{/await}
</div>
</div>
@@ -398,20 +398,19 @@
let leafletLib = null;
let prevTrackLen = 0;
/** Svelte use:action — called when the map div enters the DOM */
function mountMap(/** @type {HTMLElement} */ node) {
/** Attachment — initialises the Leaflet map when the div mounts. */
/** @type {import('svelte/attachments').Attachment<HTMLElement>} */
function mountMap(node) {
initMap(node);
return {
destroy() {
if (liveMap) {
liveMap.remove();
}
liveMap = null;
livePolyline = null;
liveMarker = null;
leafletLib = null;
prevTrackLen = 0;
return () => {
if (liveMap) {
liveMap.remove();
}
liveMap = null;
livePolyline = null;
liveMarker = null;
leafletLib = null;
prevTrackLen = 0;
};
}
@@ -1136,7 +1135,7 @@
{:else if workout.active && workout.mode === 'gps'}
<div class="gps-workout">
<div class="gps-workout-map" use:mountMap></div>
<div class="gps-workout-map" {@attach mountMap}></div>
<!-- Overlay: sits on top of the map at the bottom -->
<div class="gps-overlay" class:gps-overlay-prestart={!gpsStarted}>
@@ -1631,7 +1630,7 @@
<span>Voice: every {vgTriggerValue} {vgTriggerType === 'distance' ? 'km' : 'min'}</span>
</div>
{/if}
<div class="live-map" use:mountMap></div>
<div class="live-map" {@attach mountMap}></div>
{/if}
</div>
{/if}