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 [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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user