2 Commits

Author SHA1 Message Date
Dusan Vojacek
25090a9d95 tak predchozi commit byl uprava dasbodu, toto je az fix te migrace
All checks were successful
deploy / deploy (push) Successful in 58s
test / smoke-test (push) Successful in 9s
2026-04-10 20:58:04 +02:00
Dusan Vojacek
b8b3de2b70 fix materialized view 2026-04-10 20:56:42 +02:00
5 changed files with 49 additions and 3 deletions

View File

@@ -0,0 +1,26 @@
---
description: TimescaleDB continuous aggregates komentáře a Flyway (EMS)
globs: db/**/*.sql
alwaysApply: false
---
# Timescale continuous aggregate v EMS
## Komentáře u CA (kritické)
Continuous aggregate vytvořený jako `CREATE MATERIALIZED VIEW … WITH (timescaledb.continuous)` **není** v systémovém katalogu PostgreSQL evidovaný jako běžný **materialized view**.
- **Nepoužívat** `COMMENT ON MATERIALIZED VIEW ems.<název_ca> …` → chyba SQL state **42809** („is not a materialized view“).
- **Použít** `COMMENT ON VIEW ems.<název_ca> …` — stejný vzor jako u `telemetry_inverter_hourly` v migraci **V011**.
Samotné **wrapper view** nad CA (např. `vw_telemetry_15m_7d` v repeatable `R__vw_telemetry_15m_7d.sql`) komentovat standardně `COMMENT ON VIEW`.
## Struktura repa
- **Definice CA + `add_continuous_aggregate_policy`**: verzovaná migrace `db/migration/V0xx__*.sql` (po aplikaci na DB neměnit — nová V migrace).
- **Definice čtecího view nad CA**: raději **repeatable** `db/views/R__vw_*.sql`, aby šla měnit jedna aktuální verze bez nové V migrace.
- **PostgREST**: `GRANT SELECT` na view v `db/views/R__z_postgrest_ems_anon_grants.sql`, ne na samotný CA.
## Odkaz v dokumentaci
Detailněji: `docs/04-modules/telemetry.md` (sekce o continuous aggregates a dashboardu).

View File

@@ -188,6 +188,7 @@ Specifikace z `docs/02-architecture.md`, modulových docs a komentářů v `plan
- Python: `snake_case`, type hints, Pydantic pro API modely. - Python: `snake_case`, type hints, Pydantic pro API modely.
- SQL: `snake_case`, explicitní FK; Flyway pořadí `V###__` / repeatable `R__`. - SQL: `snake_case`, explicitní FK; Flyway pořadí `V###__` / repeatable `R__`.
- Timescale **continuous aggregate** (CA): komentář k objektu CA je **`COMMENT ON VIEW`**, ne `COMMENT ON MATERIALIZED VIEW` (PG hlásí 42809). Viz `.cursor/rules/timescale-continuous-aggregate.mdc`.
- Výkon **W**, energie **Wh**, ceny **Kč/kWh**; čas v DB **`TIMESTAMPTZ` (UTC)**. - Výkon **W**, energie **Wh**, ceny **Kč/kWh**; čas v DB **`TIMESTAMPTZ` (UTC)**.
- NIKDY neupravuj existující V__ migrační soubory po jejich aplikaci na DB. - NIKDY neupravuj existující V__ migrační soubory po jejich aplikaci na DB.
- Pokud je potřeba opravit chybu ve verzované migraci, vytvoř novou V{N+1} migraci. - Pokud je potřeba opravit chybu ve verzované migraci, vytvoř novou V{N+1} migraci.

View File

@@ -27,7 +27,8 @@ SELECT add_continuous_aggregate_policy(
schedule_interval => INTERVAL '15 minutes' schedule_interval => INTERVAL '15 minutes'
); );
COMMENT ON MATERIALIZED VIEW ems.telemetry_inverter_15m IS -- Timescale CA není v katalogu „materialized view“ stejně jako V011 u telemetry_inverter_hourly.
COMMENT ON VIEW ems.telemetry_inverter_15m IS
'Čtvrthodinové agregáty telemetrie střídače. TimescaleDB continuous aggregate. 'Čtvrthodinové agregáty telemetrie střídače. TimescaleDB continuous aggregate.
Refresh každých 15 minut. Dashboard přehled (sloty 15 min). Refresh každých 15 minut. Dashboard přehled (sloty 15 min).
View vw_telemetry_15m_7d je v repeatable R__vw_telemetry_15m_7d.sql.'; View vw_telemetry_15m_7d je v repeatable R__vw_telemetry_15m_7d.sql.';

View File

@@ -505,6 +505,11 @@ export function useDashboardData(siteId: number | null) {
? slots[liveNowIndex]!.buy_price ? slots[liveNowIndex]!.buy_price
: null : null
const sellNow =
slots.length && liveNowIndex >= 0 && liveNowIndex < slots.length
? slots[liveNowIndex]!.sell_price
: null
return { return {
slots, slots,
nowIndex: liveNowIndex, nowIndex: liveNowIndex,
@@ -515,5 +520,6 @@ export function useDashboardData(siteId: number | null) {
reload: load, reload: load,
liveMetrics, liveMetrics,
buyNow, buyNow,
sellNow,
} }
} }

View File

@@ -99,7 +99,7 @@ export function Dashboard() {
const monitoringHasError = monitoringAlerts.some((a) => a.level === 'error') const monitoringHasError = monitoringAlerts.some((a) => a.level === 'error')
const hbOnline = site?.ems_heartbeat_status === 'ok' const hbOnline = site?.ems_heartbeat_status === 'ok'
/** Horní karty (FVE, síť, SoC, cena): liveMetrics z useDashboardData (5s poll / WS), ne siteRow. */ /** Horní karty (FVE, spotřeba, síť, SoC, ceny): liveMetrics + buyNow/sellNow z useDashboardData (5s poll / WS). */
const lm = data.liveMetrics const lm = data.liveMetrics
const modeName = site?.active_mode ?? fullStatus?.operating_mode.mode_code ?? 'AUTO' const modeName = site?.active_mode ?? fullStatus?.operating_mode.mode_code ?? 'AUTO'
@@ -190,7 +190,7 @@ export function Dashboard() {
) : null} ) : null}
<section> <section>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-5"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
{metricsLoading ? ( {metricsLoading ? (
<> <>
<MetricSkeleton /> <MetricSkeleton />
@@ -198,6 +198,7 @@ export function Dashboard() {
<MetricSkeleton /> <MetricSkeleton />
<MetricSkeleton /> <MetricSkeleton />
<MetricSkeleton /> <MetricSkeleton />
<MetricSkeleton />
</> </>
) : site == null ? ( ) : site == null ? (
<p className="col-span-full text-sm text-slate-500">Žádná aktivní lokalita ve vw_site_status.</p> <p className="col-span-full text-sm text-slate-500">Žádná aktivní lokalita ve vw_site_status.</p>
@@ -233,6 +234,17 @@ export function Dashboard() {
</p> </p>
<p className="mt-1 text-[10px] text-slate-500">Aktuální 15min slot</p> <p className="mt-1 text-[10px] text-slate-500">Aktuální 15min slot</p>
</div> </div>
<div className="rounded-xl border border-slate-800 border-l-4 border-l-teal-500/80 bg-slate-900/70 p-4 pl-3">
<p className="text-[10px] font-semibold uppercase tracking-wide text-slate-500">Cena prodej</p>
<p
className={`mt-1 text-lg font-semibold tabular-nums ${
data.sellNow != null && data.sellNow < 0 ? 'text-red-300' : 'text-teal-200'
}`}
>
{fmtMoney3(data.sellNow)}
</p>
<p className="mt-1 text-[10px] text-slate-500">Aktuální 15min slot</p>
</div>
</> </>
)} )}
</div> </div>