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:
@@ -169,6 +169,7 @@ export function useDashboardData(siteId: number | null) {
|
||||
const [negPrices, setNegPrices] = useState<NegPriceItem[]>([])
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [ready, setReady] = useState(false)
|
||||
const [slotsReady, setSlotsReady] = useState(false)
|
||||
|
||||
const wsRef = useRef<WebSocket | null>(null)
|
||||
const siteIdRef = useRef(siteId)
|
||||
@@ -182,27 +183,40 @@ export function useDashboardData(siteId: number | null) {
|
||||
setLiveMetrics(null)
|
||||
setError(null)
|
||||
setReady(true)
|
||||
setSlotsReady(true)
|
||||
return
|
||||
}
|
||||
|
||||
const windowStart = floorSlotUtcMs(Date.now()) - SLOT_COUNT_BACK * SLOT_MS
|
||||
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 {
|
||||
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 [
|
||||
planMaybe,
|
||||
statusArr,
|
||||
telemetry15m7d,
|
||||
auditHourly,
|
||||
modeLog,
|
||||
hpArr,
|
||||
priceRows,
|
||||
] = await Promise.all([
|
||||
getCurrentPlan(siteId).catch((e: unknown) => {
|
||||
@@ -211,7 +225,6 @@ export function useDashboardData(siteId: number | null) {
|
||||
}
|
||||
throw e
|
||||
}),
|
||||
getJson<SiteStatusRow[]>('/vw_site_status', { site_id: `eq.${siteId}` }),
|
||||
getJson<Telemetry15m7dRow[]>('/vw_telemetry_15m_7d', {
|
||||
site_id: `eq.${siteId}`,
|
||||
slot_start: `gte.${new Date(windowStart).toISOString()}`,
|
||||
@@ -227,7 +240,6 @@ export function useDashboardData(siteId: number | null) {
|
||||
order: 'activated_at.asc',
|
||||
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).
|
||||
getSitePricesSlotsRange(
|
||||
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 planBySlot = new Map<string, PlanningIntervalDto>()
|
||||
for (const iv of plan.intervals) {
|
||||
@@ -403,13 +411,16 @@ export function useDashboardData(siteId: number | null) {
|
||||
setError(null)
|
||||
} catch (e) {
|
||||
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 {
|
||||
setReady(true)
|
||||
setSlotsReady(true)
|
||||
}
|
||||
}, [siteId])
|
||||
|
||||
useEffect(() => {
|
||||
// Změna lokality: data staré site nesmí zůstat zobrazená.
|
||||
setSlots([])
|
||||
setSlotsReady(false)
|
||||
void load()
|
||||
const id = window.setInterval(() => void load(), POLL_FULL_MS)
|
||||
return () => window.clearInterval(id)
|
||||
@@ -526,6 +537,7 @@ export function useDashboardData(siteId: number | null) {
|
||||
negPrices,
|
||||
error,
|
||||
ready,
|
||||
slotsReady,
|
||||
reload: load,
|
||||
liveMetrics,
|
||||
buyNow,
|
||||
|
||||
@@ -315,7 +315,7 @@ export function Dashboard() {
|
||||
</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">
|
||||
Nedostatek dat pro graf (zkontrolujte plán a telemetrii).
|
||||
</div>
|
||||
@@ -348,7 +348,7 @@ export function Dashboard() {
|
||||
)}
|
||||
</section>
|
||||
|
||||
{data.slots.length > 0 && data.ready ? (
|
||||
{data.slots.length > 0 && data.slotsReady ? (
|
||||
<section>
|
||||
<StatePanel slots={data.slots} nowIndex={data.nowIndex} />
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user