Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/lib/components/dashboard/DashboardCards.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
} from '$lib/sensor-labels';
import { m } from '$lib/paraglide/messages.js';
import { goto } from '$app/navigation';
import { onAppForeground } from '$lib/utils/onAppForeground';

interface Filters {
group: string;
Expand Down Expand Up @@ -216,12 +217,21 @@
});
});

let removeForegroundListener: (() => void) | null = null;

onMount(() => {
pollTimer = setInterval(refreshLoaded, REFRESH_INTERVAL_MS);
// Pull fresh data whenever the user returns to the tab/app, so devices
// that went "stale" while the page sat in the background don't linger as
// false offline indicators.
removeForegroundListener = onAppForeground(() => {
void refreshLoaded();
});
});

onDestroy(() => {
if (pollTimer) clearInterval(pollTimer);
removeForegroundListener?.();
});
</script>

Expand Down
12 changes: 10 additions & 2 deletions src/lib/components/dashboard/DashboardTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { getAppContext } from '$lib/appContext.svelte';
import { formatSensorValue, labelFor } from '$lib/sensor-labels';
import { m } from '$lib/paraglide/messages.js';
import { onAppForeground } from '$lib/utils/onAppForeground';

interface Filters {
group: string;
Expand Down Expand Up @@ -82,14 +83,21 @@
return { rows: page.rows, total: page.total };
}

// Bumped whenever the user returns to the tab/app, to force a fresh pull so
// devices that went stale in the background don't show as false offline.
let refreshToken = $state(0);
$effect(() => onAppForeground(() => (refreshToken += 1)));

// Mapped into CwDataTable's `Record<string, string[]>` filter shape so the
// table re-runs `loadData` whenever the dashboard filters (incl. the search
// box) change. The actual values are read from `filters` inside `loadData`.
// box) or the refresh token change. The actual filter values are read from
// `filters` inside `loadData`; `_refresh` only exists to trigger a reload.
const tableFilters = $derived({
group: filters.group ? [filters.group] : [],
locationGroup: filters.locationGroup ? [filters.locationGroup] : [],
location: filters.location ? [filters.location] : [],
name: filters.name ? [filters.name] : []
name: filters.name ? [filters.name] : [],
_refresh: [String(refreshToken)]
});
</script>

Expand Down
34 changes: 34 additions & 0 deletions src/lib/utils/onAppForeground.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Invoke `callback` when the app returns to the foreground — i.e. the browser
* tab becomes visible again, or the window regains focus after the user
* switched to another tab or application.
*
* Both `visibilitychange` and `focus` are observed because switching *apps*
* (rather than browser tabs) does not reliably fire `visibilitychange` on every
* platform. A short cooldown collapses the burst that happens when both events
* fire together on return, so `callback` runs at most once per return.
*
* Returns a cleanup function. Safe to call during SSR (it becomes a no-op).
*/
export function onAppForeground(callback: () => void): () => void {
if (typeof document === 'undefined') return () => {};

const COOLDOWN_MS = 1000;
let lastFired = 0;

const trigger = () => {
if (document.visibilityState !== 'visible') return;
const now = Date.now();
if (now - lastFired < COOLDOWN_MS) return;
lastFired = now;
callback();
};

document.addEventListener('visibilitychange', trigger);
window.addEventListener('focus', trigger);

return () => {
document.removeEventListener('visibilitychange', trigger);
window.removeEventListener('focus', trigger);
};
}
13 changes: 13 additions & 0 deletions src/routes/locations/[location_id]/devices/[dev_eui]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import { resolveExportTimeZone } from './csvExport';
import type { PageProps } from './$types';
import { onDestroy } from 'svelte';
import { onAppForeground } from '$lib/utils/onAppForeground';
import DeviceDashboardHeader from './DeviceDashboardHeader.svelte';
import {
DEFAULT_RANGE_SELECTION,
Expand Down Expand Up @@ -323,6 +324,18 @@
return isRelayDevice ? await refreshRelayDisplayData() : await fetchLatestData();
}

// Full pull whenever the user returns to the tab/app. A detail page left in
// the background goes stale — the device can look offline purely because the
// page sat idle past its upload interval. Refresh the active history range
// first, then the latest reading so it prepends onto fresh history.
async function refreshOnForeground(): Promise<void> {
if (!authToken || !devEui) return;
await selectRange(activeRange ?? DEFAULT_RANGE_SELECTION);
await refreshDisplayedData();
}

$effect(() => onAppForeground(() => void refreshOnForeground()));

async function queueRelayCommand(
relay: RelayNumber,
targetState: RelayTargetState,
Expand Down
Loading