Compare commits
2 Commits
eb8dd0368f
...
25090a9d95
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25090a9d95 | ||
|
|
b8b3de2b70 |
26
.cursor/rules/timescale-continuous-aggregate.mdc
Normal file
26
.cursor/rules/timescale-continuous-aggregate.mdc
Normal 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).
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.';
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user