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
42 changes: 35 additions & 7 deletions src/lib/components/dashboard/DashboardDeviceCards.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,15 @@
});
}

// On mount and whenever locationCards or the access token become available,
// check localStorage for any cards that were already expanded and pre-fetch their data.
// Pre-fetch data for any cards that were already expanded in localStorage.
// Scoped to visibleLocationCards so off-screen cards don't fan out fetches at
// page load — as the user scrolls and visibleLocationCount grows, this effect
// re-runs and picks up the newly-visible expanded cards. Already-fetched ones
// are dedup'd via the marker in expandedDetailRowsByDevEui.
$effect(() => {
if (!app.accessToken || locationCards.length === 0) return;
if (!app.accessToken || visibleLocationCards.length === 0) return;

for (const card of locationCards) {
for (const card of visibleLocationCards) {
for (const sensor of card.sensors) {
if (isSensorExpandedInStorage(card, sensor)) {
void handleSensorExpand(sensor);
Expand Down Expand Up @@ -436,22 +439,46 @@

const freshData = await fetchAndMergeLatestDeviceData(devEui);
if (!freshData) {
// Remove loading marker so the user can retry by collapsing and re-expanding
delete expandedDetailRowsByDevEui[devEui];
// Leave the marker in place. Deleting it would let the auto-expand $effect
// (which re-runs whenever locationCards re-derives — every 30s from reactiveNow,
// plus on every app.devices mutation) re-fire handleSensorExpand on every tick,
// hammering the API with retries that compound when the server returns 429s.
// The marker stays null (showing "Loading…" rows); the periodic 10-min refresh
// uses a different endpoint and will replace these rows when it next fires.
return;
}

backfilledDevEuis.add(devEui);

const liveDevice = resolveLiveDashboardDevice(sensor.sourceDevice) ?? sensor.sourceDevice;
const typeConfig = resolveDeviceTypeConfig(liveDevice, app.deviceTypeLookup);
// /latest-data can return an older created_at than what's already in app.devices
// (the two endpoints query different tables for relay-style devices). The merge
// in fetchAndMergeLatestDeviceData prefers the newer one, so use the live device's
// created_at for the "Last Update" row — but fall back to the response's value
// if liveDevice somehow lacks a valid date, so the row builder always has input.
const liveCreatedAt = liveDevice?.created_at;
const liveCreatedAtMs =
liveCreatedAt instanceof Date ? liveCreatedAt.getTime() : NaN;
const createdAtForRow = Number.isFinite(liveCreatedAtMs)
? liveCreatedAt
: freshData.created_at;
expandedDetailRowsByDevEui[devEui] = buildDeviceExpandedDetailRows(
freshData,
{ ...freshData, created_at: createdAtForRow },
typeConfig,
sensor.sensor.label
);
}

function handleSensorCollapse(sensor: DashboardSensorCardEntry) {
// Allow the user to retry a failed/stuck expansion by collapsing and
// re-expanding. We only clear the marker when it's null (loading/failed) —
// successful fetches (rows array) stay cached so re-expanding doesn't refetch.
if (expandedDetailRowsByDevEui[sensor.devEui] === null) {
delete expandedDetailRowsByDevEui[sensor.devEui];
}
}

async function refreshSingleDevice(device: IDevice | null | undefined) {
const targetDevice = resolveLiveDashboardDevice(device);
const devEui = targetDevice?.dev_eui;
Expand Down Expand Up @@ -541,6 +568,7 @@
expireAfterMinutes={sensor?.sourceDevice?.raw_data?.default_upload_interval ?? 10}
class="dashboard-device-cards__sensor-card"
onExpand={() => handleSensorExpand(sensor)}
onCollapse={() => handleSensorCollapse(sensor)}
>
<div class="dashboard-device-cards__sensor-details">
<CwDataList rows={resolveSensorDetailRows(sensor)} />
Expand Down
9 changes: 9 additions & 0 deletions src/lib/components/dashboard/dashboard-device-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ function preferIncomingLocationId(incoming: number, current: number): number {
return incoming > 0 ? incoming : current;
}

function preferNewerCreatedAt(incoming: Date, current: Date): Date {
const incomingMs = incoming instanceof Date ? incoming.getTime() : NaN;
const currentMs = current instanceof Date ? current.getTime() : NaN;
if (!Number.isFinite(incomingMs)) return current;
if (!Number.isFinite(currentMs)) return incoming;
return incomingMs > currentMs ? incoming : current;
}

function getDeviceDataTable(
device: DeviceDto | DevicePrimaryDataDto | Record<string, unknown>
): string {
Expand Down Expand Up @@ -244,6 +252,7 @@ export function mergeDashboardDevices(
alert_count: latestDevice.alert_count ?? device.alert_count ?? 0,
// Preserve device_type_id (metadata device has it, primary data refresh may not)
device_type_id: device.device_type_id ?? latestDevice.device_type_id,
created_at: preferNewerCreatedAt(latestDevice.created_at, device.created_at),
raw_data: latestDevice.raw_data
? { ...device.raw_data, ...latestDevice.raw_data }
: device.raw_data
Expand Down
1 change: 1 addition & 0 deletions src/lib/components/dashboard/device-cards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ function buildDashboardSensorCardEntry(
const rawPrimary = device.raw_data?.[typeConfig?.primary_data_key];
const rawSecondary = device.raw_data?.[typeConfig?.secondary_data_key] || null;

console.log(device);
return {
id: `sensor:${device.dev_eui}`,
storageKey: getSensorStorageKey(device),
Expand Down
2 changes: 1 addition & 1 deletion src/lib/devices/relay-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type {
RelayTelemetryRow
} from './relay-types';

export const RELAY_VERIFICATION_DELAY_MS = 15_000;
export const RELAY_VERIFICATION_DELAY_MS = 10_000;
export const RELAY_REFRESH_INTERVAL_MS = 30_000;
export const MIN_RELAY_PULSE_DURATION_SECONDS = 15;
export const MAX_RELAY_PULSE_DURATION_SECONDS = 4_294_967;
Expand Down
Loading