Fix jumping field order, tidy labels/units on Dashboard field list
The unordered plant_asset_latest query could return rows in a different order after every MQTT upsert, making the field list under each asset card appear to shuffle. Added ORDER BY, plus a curated label/unit/order table for the known water-softener fields (Salt Level 42%, Tank A Capacity 1850 L, Regeneration Active Yes/No, ...) with a title-case + alphabetical fallback for anything not yet mapped.
This commit is contained in:
parent
ff145957fc
commit
9bb5645f8d
2 changed files with 48 additions and 7 deletions
|
|
@ -14,9 +14,13 @@ export async function statusRoutes(app) {
|
|||
SELECT * FROM plant_assets WHERE active = TRUE ORDER BY asset_type, name
|
||||
`)
|
||||
|
||||
// ORDER BY matters here — without it, Postgres can return this heavily-
|
||||
// upserted table's rows in a different order after every MQTT message,
|
||||
// which made the Dashboard's field list appear to shuffle on refresh.
|
||||
const { rows: latest } = await pool.query(`
|
||||
SELECT asset_id, field_key, value_numeric, value_text, updated_at
|
||||
FROM plant_asset_latest
|
||||
ORDER BY asset_id, field_key
|
||||
`)
|
||||
const latestByAsset = new Map()
|
||||
for (const l of latest) {
|
||||
|
|
|
|||
|
|
@ -1,20 +1,57 @@
|
|||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { AlertTriangle, WifiOff } from 'lucide-react'
|
||||
import { fetchStatus } from '../api'
|
||||
import type { AssetStatus, AssetType } from '../types'
|
||||
import type { AssetStatus, AssetType, TelemetryField } from '../types'
|
||||
import { ASSET_TYPES, ASSET_TYPE_LABELS } from '../types'
|
||||
import WaterSoftenerDiagram from '../components/WaterSoftenerDiagram'
|
||||
|
||||
const POLL_MS = 30000
|
||||
|
||||
function fieldLabel(key: string): string {
|
||||
return key.replace(/_/g, ' ')
|
||||
// Curated presentation for known MQTT field_keys — proper label, unit, a
|
||||
// fixed display order, and whether the value is boolean-ish. Anything not
|
||||
// listed here (other asset types, fields not yet known) falls back to a
|
||||
// title-cased label and sorts alphabetically after all known fields, so the
|
||||
// list never breaks for an unmapped field, it just looks less polished.
|
||||
interface FieldMeta { label: string; order: number; unit?: string; boolean?: boolean }
|
||||
const FIELD_META: Record<string, FieldMeta> = {
|
||||
salt_level_pct: { label: 'Salt Level', order: 1, unit: '%' },
|
||||
salt_level_status: { label: 'Salt Status', order: 2 },
|
||||
tank_a_capacity_l: { label: 'Tank A Capacity', order: 3, unit: 'L' },
|
||||
tank_b_capacity_l: { label: 'Tank B Capacity', order: 4, unit: 'L' },
|
||||
tank_in_service: { label: 'Tank In Service', order: 5, boolean: true },
|
||||
regen_active: { label: 'Regeneration Active', order: 6, boolean: true },
|
||||
}
|
||||
|
||||
function fieldValue(f: { value_numeric: string | null; value_text: string | null }): string {
|
||||
function titleCase(key: string): string {
|
||||
return key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
|
||||
}
|
||||
|
||||
function isTruthyField(f: TelemetryField): boolean {
|
||||
if (f.value_numeric !== null) return Number(f.value_numeric) === 1
|
||||
const t = (f.value_text || '').trim().toLowerCase()
|
||||
return t === 'true' || t === 'on' || t === '1' || t === 'yes'
|
||||
}
|
||||
|
||||
function fieldLabel(key: string): string {
|
||||
return FIELD_META[key]?.label ?? titleCase(key)
|
||||
}
|
||||
|
||||
function fieldValue(f: TelemetryField): string {
|
||||
const meta = FIELD_META[f.field_key]
|
||||
if (meta?.boolean) return isTruthyField(f) ? 'Yes' : 'No'
|
||||
if (f.value_text !== null && f.value_text !== '' && isNaN(Number(f.value_text))) return f.value_text
|
||||
if (f.value_numeric !== null) return f.value_numeric
|
||||
return f.value_text ?? '—'
|
||||
if (f.value_numeric === null) return f.value_text ?? '—'
|
||||
const rounded = Math.round(Number(f.value_numeric) * 100) / 100
|
||||
if (!meta?.unit) return String(rounded)
|
||||
return meta.unit === '%' ? `${rounded}%` : `${rounded} ${meta.unit}`
|
||||
}
|
||||
|
||||
function sortedFields(latest: TelemetryField[]): TelemetryField[] {
|
||||
return [...latest].sort((a, b) => {
|
||||
const oa = FIELD_META[a.field_key]?.order ?? 999
|
||||
const ob = FIELD_META[b.field_key]?.order ?? 999
|
||||
return oa !== ob ? oa - ob : a.field_key.localeCompare(b.field_key)
|
||||
})
|
||||
}
|
||||
|
||||
function AssetCard({ asset }: { asset: AssetStatus }) {
|
||||
|
|
@ -38,7 +75,7 @@ function AssetCard({ asset }: { asset: AssetStatus }) {
|
|||
<div className="muted" style={{ fontSize: 12.5 }}>No telemetry received yet.</div>
|
||||
) : (
|
||||
<div className="asset-field-list">
|
||||
{asset.latest.map(f => (
|
||||
{sortedFields(asset.latest).map(f => (
|
||||
<div key={f.field_key} className="asset-field-row">
|
||||
<span className="asset-field-key">{fieldLabel(f.field_key)}</span>
|
||||
<span className="asset-field-value">{fieldValue(f)}</span>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue