From 753d932a59c0e0b82a0be93a25a90bf3a3816ab9 Mon Sep 17 00:00:00 2001 From: Kevin Cantrell Date: Fri, 17 Apr 2026 23:55:19 +0900 Subject: [PATCH] fixed scrolling vertically, removed junk columns from csv, formatted csv datetimes to be human readable. --- CLAUDE.md | 32 +++++++++++++++++++ src/routes/+page.svelte | 15 +++++++++ .../devices/[dev_eui]/csvExport.ts | 22 +++---------- 3 files changed, 52 insertions(+), 17 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d99d8c04..4cab4c4f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -426,6 +426,38 @@ Pages such as `/reports` and `/rules` that render a `CwDataTable` **must remain --- +## Viewport-fill pages (dashboard exception) + +The `/` dashboard is a **viewport-fill** page: it fills the full viewport height and handles its own scrolling internally (the table uses `CwDataTable fillParent`; the card view has its own `overflow-y: auto` scroll container). It does **not** scroll via `.app-shell__main`. + +Because `AppPage` globally uses `flex: 1 0 auto` (required for list-page scrolling), the dashboard must override that behaviour with a scoped `:global` style so that `AppPage` and its shell are bounded to viewport height and pass a definite height down the flex chain: + +```svelte + + + + +
+ +
+
+``` + +**Why this matters — past regression:** Changing `AppPage`'s CSS from `flex: 1 1 auto; min-height: 0` to `flex: 1 0 auto` (no `min-height`) was the correct fix for list-page scrolling, but it silently broke the dashboard because neither the table's internal scroll nor the card view's scroll container received a definite height. The scoped override keeps both modes working without conflict. + +**Rules:** +- Never remove the `flex: 1 1 auto; min-height: 0` override from `.app-page.dashboard-page` — doing so will break scrolling in both the table and card views on the dashboard. +- Never change the global `AppPage` flex back to `flex: 1 1 auto` to "fix" the dashboard — that re-breaks scrolling on every other page. +- The dashboard's inner wrapper div **must** keep `flex-1 min-h-0 overflow-hidden` so the bounded height is propagated to the child scroll containers. + +--- + ## Checklist for new pages ### UI diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 1f297113..19caf1b3 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -145,6 +145,21 @@ }); + + {m.dashboard_page_title()} diff --git a/src/routes/locations/[location_id]/devices/[dev_eui]/csvExport.ts b/src/routes/locations/[location_id]/devices/[dev_eui]/csvExport.ts index 891fe393..c38f7899 100644 --- a/src/routes/locations/[location_id]/devices/[dev_eui]/csvExport.ts +++ b/src/routes/locations/[location_id]/devices/[dev_eui]/csvExport.ts @@ -22,7 +22,7 @@ interface DownloadCsvOptions { const CREATED_AT_KEY = 'created_at'; const TIMESTAMP_COLUMN_NAMES = new Set(['created_at', 'timestamp', 'traffic_hour']); -const MINUTES_PER_HOUR = 60; +const CSV_ALLOWED_COLUMNS = [CREATED_AT_KEY, 'co2', 'humidity', 'temperature_c', 'moisture', 'ec']; const MILLISECONDS_PER_MINUTE = 60_000; const TIMEZONE_SUFFIX_PATTERN = /(?:[zZ]|[+-]\d{2}(?::?\d{2})?)$/; @@ -155,11 +155,7 @@ function getColumns(rows: CsvRow[]): string[] { } } - const preferredOrder = ['created_at', 'traffic_hour', 'timestamp', 'id', 'dev_eui']; - const ordered = preferredOrder.filter((key) => keys.delete(key) || false); - const remaining = Array.from(keys).sort((a, b) => a.localeCompare(b)); - - return [...ordered, ...remaining]; + return CSV_ALLOWED_COLUMNS.filter((col) => keys.has(col)); } function sanitizeFileName(value: string): string { @@ -290,13 +286,6 @@ function getTimeZoneOffsetMilliseconds(date: Date, timeZone: string): number { return asUtc - date.getTime(); } -function formatOffset(offsetMinutes: number): string { - const sign = offsetMinutes >= 0 ? '+' : '-'; - const absoluteMinutes = Math.abs(offsetMinutes); - const hours = Math.floor(absoluteMinutes / MINUTES_PER_HOUR); - const minutes = absoluteMinutes % MINUTES_PER_HOUR; - return `${sign}${padDatePart(hours)}:${padDatePart(minutes)}`; -} function formatDateForTimeZone(date: Date, timeZone: string): string { const offsetMinutes = Math.round( @@ -304,10 +293,9 @@ function formatDateForTimeZone(date: Date, timeZone: string): string { ); const shiftedDate = new Date(date.getTime() + offsetMinutes * MILLISECONDS_PER_MINUTE); - return [ - `${shiftedDate.getUTCFullYear()}-${padDatePart(shiftedDate.getUTCMonth() + 1)}-${padDatePart(shiftedDate.getUTCDate())}`, - `${padDatePart(shiftedDate.getUTCHours())}:${padDatePart(shiftedDate.getUTCMinutes())}:${padDatePart(shiftedDate.getUTCSeconds())}.${padDatePart(shiftedDate.getUTCMilliseconds(), 3)}${formatOffset(offsetMinutes)}` - ].join('T'); + const datePart = `${shiftedDate.getUTCFullYear()}-${padDatePart(shiftedDate.getUTCMonth() + 1)}-${padDatePart(shiftedDate.getUTCDate())}`; + const timePart = `${padDatePart(shiftedDate.getUTCHours())}:${padDatePart(shiftedDate.getUTCMinutes())}:${padDatePart(shiftedDate.getUTCSeconds())}`; + return `${datePart} ${timePart}`; } function formatFileDateTime(date: Date, timeZone: string): string {