From ae3f3a00150d187f397cbfc761fb437a10f07a36 Mon Sep 17 00:00:00 2001 From: Kevin Cantrell Date: Thu, 23 Apr 2026 22:36:25 +0900 Subject: [PATCH 1/2] fixed issue with sensors requiring opening before loading primary card data. --- .../dashboard/DashboardDeviceCards.svelte | 92 ++++++++++++++----- .../DashboardDeviceCards.svelte.spec.ts | 8 +- 2 files changed, 74 insertions(+), 26 deletions(-) diff --git a/src/lib/components/dashboard/DashboardDeviceCards.svelte b/src/lib/components/dashboard/DashboardDeviceCards.svelte index 70abf778..c45ef056 100644 --- a/src/lib/components/dashboard/DashboardDeviceCards.svelte +++ b/src/lib/components/dashboard/DashboardDeviceCards.svelte @@ -26,7 +26,11 @@ getDashboardDeviceNextRefreshDelayMs, refreshDashboardDevice } from './dashboard-device-refresh'; - import { resolveDeviceTypeConfig } from './dashboard-device-data'; + import { + mapDashboardPrimaryDataToDevice, + mergeDashboardDevices, + resolveDeviceTypeConfig + } from './dashboard-device-data'; import { ApiService } from '$lib/api/api.service'; import { reactiveNow } from '$lib/utils/reactive-now.svelte'; import { listDashboardDevices, type DashboardDeviceFilters } from './device-table'; @@ -55,6 +59,9 @@ let expandedDetailRowsByDevEui = $state>({}); let isExpandingLocationWindow = $state(false); let monitoredCardRefreshIds: string[] = []; + // Dedupe guard for the missing-primary-data backfill effect below; a plain + // Set is fine because we never want to render from it. + const backfilledDevEuis = new Set(); let search = $derived(page.url.searchParams.get('search') ?? ''); let filterKey = $derived( @@ -149,6 +156,23 @@ } }); + // Backfill visible cards whose device came back from the bulk primary-data + // endpoint without any reading. Scoped to visible cards so we don't spray + // requests across hundreds of never-seen devices. + $effect(() => { + if (!app.accessToken) return; + + for (const card of visibleLocationCards) { + for (const sensor of card.sensors) { + if (backfilledDevEuis.has(sensor.devEui)) continue; + if (sensor.sourceDevice.has_primary_data !== false) continue; + + backfilledDevEuis.add(sensor.devEui); + void fetchAndMergeLatestDeviceData(sensor.devEui); + } + } + }); + $effect(() => { if (!app.accessToken) { cardRefreshAlarms.clear(); @@ -365,20 +389,10 @@ } } - async function handleSensorExpand(sensor: DashboardSensorCardEntry) { - if (!app.accessToken || sensor.sourceDevice.has_primary_data === false) { - return; - } - - const { devEui, sourceDevice } = sensor; - - // Skip if already fetched or currently loading - if (devEui in expandedDetailRowsByDevEui) { - return; - } - - // Mark as loading - expandedDetailRowsByDevEui[devEui] = null; + async function fetchAndMergeLatestDeviceData( + devEui: string + ): Promise | null> { + if (!app.accessToken) return null; try { const api = new ApiService({ authToken: app.accessToken }); @@ -391,17 +405,51 @@ typeof v === 'string' && v.trim() !== '' && Number.isFinite(Number(v)) ? Number(v) : v ]) ); - const typeConfig = resolveDeviceTypeConfig(sourceDevice, app.deviceTypeLookup); - expandedDetailRowsByDevEui[devEui] = buildDeviceExpandedDetailRows( - freshData, - typeConfig, - sensor.sensor.label - ); + + // Merge into app.devices so the card header's primary/secondary + // values re-derive — otherwise the card keeps whatever (possibly + // empty) reading the bulk primary-data endpoint returned at page load. + const freshDevice = mapDashboardPrimaryDataToDevice(freshData); + app.devices = mergeDashboardDevices(app.devices, [freshDevice]); + + return freshData; } catch (err) { - console.error(`Failed to load expanded data for ${devEui}:`, err); + console.error(`Failed to load latest data for ${devEui}:`, err); + return null; + } + } + + async function handleSensorExpand(sensor: DashboardSensorCardEntry) { + if (!app.accessToken) { + return; + } + + const { devEui } = sensor; + + // Skip if already fetched or currently loading + if (devEui in expandedDetailRowsByDevEui) { + return; + } + + // Mark as loading + expandedDetailRowsByDevEui[devEui] = null; + + const freshData = await fetchAndMergeLatestDeviceData(devEui); + if (!freshData) { // Remove loading marker so the user can retry by collapsing and re-expanding delete expandedDetailRowsByDevEui[devEui]; + return; } + + backfilledDevEuis.add(devEui); + + const liveDevice = resolveLiveDashboardDevice(sensor.sourceDevice) ?? sensor.sourceDevice; + const typeConfig = resolveDeviceTypeConfig(liveDevice, app.deviceTypeLookup); + expandedDetailRowsByDevEui[devEui] = buildDeviceExpandedDetailRows( + freshData, + typeConfig, + sensor.sensor.label + ); } async function refreshSingleDevice(device: IDevice | null | undefined) { diff --git a/src/lib/components/dashboard/DashboardDeviceCards.svelte.spec.ts b/src/lib/components/dashboard/DashboardDeviceCards.svelte.spec.ts index 67eef1c7..a7b518a8 100644 --- a/src/lib/components/dashboard/DashboardDeviceCards.svelte.spec.ts +++ b/src/lib/components/dashboard/DashboardDeviceCards.svelte.spec.ts @@ -153,10 +153,10 @@ describe('DashboardDeviceCards', () => { name: 'Canopy', location_name: 'Grow Room', group: 'air', - created_at: '2026-04-09T10:00:18.000Z', - co2: 901, - humidity: 60.1, - temperature_c: 25.4, + created_at: '2026-04-09T09:50:00.000Z', + co2: 820, + humidity: 56, + temperature_c: 24.5, location_id: 1 })); const { component } = renderDashboardDeviceCards(); From c1d2d84f29f6bf831e2e5fc198c1a9769b583406 Mon Sep 17 00:00:00 2001 From: Kevin Cantrell Date: Sat, 25 Apr 2026 21:10:24 +0900 Subject: [PATCH 2/2] fixed issue where reports schedule blew away older schedules --- src/routes/reports/[report_id]/edit/+page.server.ts | 13 +++++++++---- src/routes/reports/[report_id]/edit/+page.svelte | 10 +++------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/routes/reports/[report_id]/edit/+page.server.ts b/src/routes/reports/[report_id]/edit/+page.server.ts index 1aad4e2e..c04d0707 100644 --- a/src/routes/reports/[report_id]/edit/+page.server.ts +++ b/src/routes/reports/[report_id]/edit/+page.server.ts @@ -308,15 +308,20 @@ function sanitizeDataProcessingScheduleEntries( ): CreateReportDataProcessingScheduleRequest[] | undefined { if (!Array.isArray(value)) return undefined; - // Validate HH:mm format - const isValidTime = (t: unknown): t is string => typeof t === 'string' && /^\d{2}:\d{2}$/.test(t); + // Accept both HH:mm and HH:mm:ss; normalize to HH:mm. + const TIME_PATTERN = /^(\d{2}:\d{2})(?::\d{2})?$/; + const normalizeTime = (t: unknown): string | undefined => { + if (typeof t !== 'string') return undefined; + const match = TIME_PATTERN.exec(t.trim()); + return match ? match[1] : undefined; + }; const entries = value .filter(isRecord) .map((entry) => { const dayOfWeek = readOptionalInteger(entry.day_of_week); - const startTime = isValidTime(entry.start_time) ? entry.start_time : undefined; - const endTime = isValidTime(entry.end_time) ? entry.end_time : undefined; + const startTime = normalizeTime(entry.start_time); + const endTime = normalizeTime(entry.end_time); if (dayOfWeek === undefined || !startTime || !endTime) { return null; diff --git a/src/routes/reports/[report_id]/edit/+page.svelte b/src/routes/reports/[report_id]/edit/+page.svelte index 70f0e67c..d7908ac4 100644 --- a/src/routes/reports/[report_id]/edit/+page.svelte +++ b/src/routes/reports/[report_id]/edit/+page.svelte @@ -594,16 +594,12 @@ } function addDataProcessingSchedule() { - fields.report_data_processing_schedules = [ - ...fields.report_data_processing_schedules, - createEmptyDataProcessingSchedule() - ]; + fields.report_data_processing_schedules.push(createEmptyDataProcessingSchedule()); } function removeDataProcessingSchedule(key: string) { - fields.report_data_processing_schedules = fields.report_data_processing_schedules.filter( - (s) => s.key !== key - ); + const idx = fields.report_data_processing_schedules.findIndex((s) => s.key === key); + if (idx >= 0) fields.report_data_processing_schedules.splice(idx, 1); } function addRecipient() {