výkon: dashboard ve 2 vlnách (status hned, plán/telemetrie async), stale data bez blikání

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dusan Vojacek
2026-06-11 14:23:23 +02:00
parent 7c2669def6
commit 293f32cff1
2 changed files with 29 additions and 17 deletions

View File

@@ -169,6 +169,7 @@ export function useDashboardData(siteId: number | null) {
const [negPrices, setNegPrices] = useState<NegPriceItem[]>([]) const [negPrices, setNegPrices] = useState<NegPriceItem[]>([])
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [ready, setReady] = useState(false) const [ready, setReady] = useState(false)
const [slotsReady, setSlotsReady] = useState(false)
const wsRef = useRef<WebSocket | null>(null) const wsRef = useRef<WebSocket | null>(null)
const siteIdRef = useRef(siteId) const siteIdRef = useRef(siteId)
@@ -182,27 +183,40 @@ export function useDashboardData(siteId: number | null) {
setLiveMetrics(null) setLiveMetrics(null)
setError(null) setError(null)
setReady(true) setReady(true)
setSlotsReady(true)
return return
} }
const windowStart = floorSlotUtcMs(Date.now()) - SLOT_COUNT_BACK * SLOT_MS const windowStart = floorSlotUtcMs(Date.now()) - SLOT_COUNT_BACK * SLOT_MS
const nIdx = currentSlotIndexInWindow(windowStart) const nIdx = currentSlotIndexInWindow(windowStart)
// Vlna 1 — kritická: vw_site_status (+ TČ) je rychlé, UI se odemkne hned.
let status: SiteStatusRow | null = null
try {
const [statusArr, hpArr] = await Promise.all([
getJson<SiteStatusRow[]>('/vw_site_status', { site_id: `eq.${siteId}` }),
getJson<HeatPumpLatestRow[]>('/vw_latest_heat_pump', { site_id: `eq.${siteId}` }),
])
status = Array.isArray(statusArr) && statusArr[0] ? statusArr[0]! : null
const hp = Array.isArray(hpArr) && hpArr[0] ? hpArr[0]! : null
setLiveMetrics(buildLiveMetrics(status, hp))
setError(null)
} catch (e) {
setError(e instanceof Error ? e.message : 'Chyba načítání dashboardu')
} finally {
setReady(true)
}
// Vlna 2 — extended: plán, telemetrie, audit, ceny. Při refetchi zůstávají
// zobrazená stale data (sloty se přepíšou až novými daty, žádné blikání).
try { try {
const todayPrague = pragueCalendarDay() const todayPrague = pragueCalendarDay()
const dates = new Set<string>()
for (let i = 0; i < TOTAL_SLOTS; i++) {
const ms = windowStart + i * SLOT_MS
dates.add(pragueCalendarDay(new Date(ms)))
}
const [ const [
planMaybe, planMaybe,
statusArr,
telemetry15m7d, telemetry15m7d,
auditHourly, auditHourly,
modeLog, modeLog,
hpArr,
priceRows, priceRows,
] = await Promise.all([ ] = await Promise.all([
getCurrentPlan(siteId).catch((e: unknown) => { getCurrentPlan(siteId).catch((e: unknown) => {
@@ -211,7 +225,6 @@ export function useDashboardData(siteId: number | null) {
} }
throw e throw e
}), }),
getJson<SiteStatusRow[]>('/vw_site_status', { site_id: `eq.${siteId}` }),
getJson<Telemetry15m7dRow[]>('/vw_telemetry_15m_7d', { getJson<Telemetry15m7dRow[]>('/vw_telemetry_15m_7d', {
site_id: `eq.${siteId}`, site_id: `eq.${siteId}`,
slot_start: `gte.${new Date(windowStart).toISOString()}`, slot_start: `gte.${new Date(windowStart).toISOString()}`,
@@ -227,7 +240,6 @@ export function useDashboardData(siteId: number | null) {
order: 'activated_at.asc', order: 'activated_at.asc',
limit: '200', limit: '200',
}), }),
getJson<HeatPumpLatestRow[]>('/vw_latest_heat_pump', { site_id: `eq.${siteId}` }),
// Ceny bereme přes FastAPI range endpoint (PostgREST /rest je u vás chráněné → 401). // Ceny bereme přes FastAPI range endpoint (PostgREST /rest je u vás chráněné → 401).
getSitePricesSlotsRange( getSitePricesSlotsRange(
siteId, siteId,
@@ -236,10 +248,6 @@ export function useDashboardData(siteId: number | null) {
), ),
]) ])
const status = Array.isArray(statusArr) && statusArr[0] ? statusArr[0]! : null
const hp = Array.isArray(hpArr) && hpArr[0] ? hpArr[0]! : null
setLiveMetrics(buildLiveMetrics(status, hp))
const plan = planMaybe as { intervals: PlanningIntervalDto[] } const plan = planMaybe as { intervals: PlanningIntervalDto[] }
const planBySlot = new Map<string, PlanningIntervalDto>() const planBySlot = new Map<string, PlanningIntervalDto>()
for (const iv of plan.intervals) { for (const iv of plan.intervals) {
@@ -403,13 +411,16 @@ export function useDashboardData(siteId: number | null) {
setError(null) setError(null)
} catch (e) { } catch (e) {
setError(e instanceof Error ? e.message : 'Chyba načítání dashboardu') setError(e instanceof Error ? e.message : 'Chyba načítání dashboardu')
setSlots([]) // Sloty neměnit — během chyby refetche zůstávají zobrazená poslední data.
} finally { } finally {
setReady(true) setSlotsReady(true)
} }
}, [siteId]) }, [siteId])
useEffect(() => { useEffect(() => {
// Změna lokality: data staré site nesmí zůstat zobrazená.
setSlots([])
setSlotsReady(false)
void load() void load()
const id = window.setInterval(() => void load(), POLL_FULL_MS) const id = window.setInterval(() => void load(), POLL_FULL_MS)
return () => window.clearInterval(id) return () => window.clearInterval(id)
@@ -526,6 +537,7 @@ export function useDashboardData(siteId: number | null) {
negPrices, negPrices,
error, error,
ready, ready,
slotsReady,
reload: load, reload: load,
liveMetrics, liveMetrics,
buyNow, buyNow,

View File

@@ -315,7 +315,7 @@ export function Dashboard() {
</section> </section>
<section> <section>
{data.slots.length === 0 && data.ready ? ( {data.slots.length === 0 && data.slotsReady ? (
<div className="rounded-xl border border-slate-800 bg-slate-900/40 px-4 py-8 text-center text-sm text-slate-500"> <div className="rounded-xl border border-slate-800 bg-slate-900/40 px-4 py-8 text-center text-sm text-slate-500">
Nedostatek dat pro graf (zkontrolujte plán a telemetrii). Nedostatek dat pro graf (zkontrolujte plán a telemetrii).
</div> </div>
@@ -348,7 +348,7 @@ export function Dashboard() {
)} )}
</section> </section>
{data.slots.length > 0 && data.ready ? ( {data.slots.length > 0 && data.slotsReady ? (
<section> <section>
<StatePanel slots={data.slots} nowIndex={data.nowIndex} /> <StatePanel slots={data.slots} nowIndex={data.nowIndex} />
</section> </section>