# Runbook: pgdb health (TimescaleDB + pgweb + pg-backup)
## Purpose
Read-only health check of the pgdb TimescaleDB compose stack (PG18 + pgweb GUI + nightly custom-format backups). Confirms the stack is serving Home Assistant (hass/scribe) and that backups are current and restorable.
- Read-only: never mutates containers, databases, backups, or secrets.
- Not applicable: restoring data (use [pgdb-restore](pgdb-restore.md)), upgrading images (use [pgdb-update](pgdb-update.md)), HA-side changes (see `hosts/hass.windy.lan.md`).
## Ownership
- Owner: personal ops (Windy)
- Last reviewed: 2026-08-29
- Related systems: pgdb (`/opt/database`, TimescaleDB 18.6 / TS 2.29.2), HA `192.168.55.11` (hass/scribe clients)
## Access
SSH to pgdb (key auth works from the WSL agent shell as of 2026-08-29):
From the agent sandbox, always use `-F /dev/null` (system ssh config is unreadable there) and prefer IPv4. The compose project lives at `/opt/database` — prefix every `docker compose` call with `cd /opt/database`. Never print `.env` values or bookmarks (they contain DB passwords); compare or use them only inside commands that output non-secret signals (status codes, counts, names).
## Safety
### Non-negotiable rules
- Read-only diagnosis only; never "fix while checking".
- Never print passwords or secrets — redact/consume them inside commands.
- If live state conflicts with an expected value, `STOP` and record evidence; do not invent parameters or bypass a failed check.
- Restore/update work belongs to the change runbooks, not this one.
### Stop conditions
- Any container `Exited`, `Restarting`, or not `healthy` where expected.
- Expected database/table/hypertable missing or a key query errors.
- Write-activity sample does not increase (scribe `states_raw` static).
- Latest daily backup older than today, not custom format, or `pg_restore -l` fails.
- Disk usage near full on `/srv/pgdata` or `/`.
- New `ERROR`/`FATAL` lines in the timescaledb log or backup failures in the pg-backup log.
- pgweb: no credentials → HTTP 401; with credentials → HTTP 200; `/api/bookmarks` → `["hass","scribe"]`.
-`daily/*-latest.dump` symlinks point to today's dumps; `file -L` reports `PostgreSQL custom database dump`.
-`/srv/pgdata` (`/dev/sdb1`, 32G) and `/` not near full; fstab mounts `/srv/pgdata` by `UUID=c9e12e79-1f66-404c-ab7f-b8809be81d86` with `defaults,noatime`.
- timescaledb log: no new `ERROR`/`FATAL`; pg-backup log: recent successful backup.
ssh -F /dev/null -o BatchMode=yes windy@192.168.55.15 'cd /opt/database && docker compose exec -T timescaledb psql -U postgres -Atc "select datname from pg_database where datistemplate=false order by 1;"'
ssh -F /dev/null -o BatchMode=yes windy@192.168.55.15 'cd /opt/database && docker compose exec -T timescaledb psql -U postgres -Atc "select datname, usename, client_addr from pg_stat_activity where client_addr is not null group by 1,2,3 order by 1;"'
```
**Expected**
-`PostgreSQL 18.x` (verified: 18.6).
- Databases: `hass`, `postgres`, `scribe`.
- HA sessions: `hass|hass|192.168.55.11` and `scribe|hass|192.168.55.11` (the HAOS recorder/scribe clients from `192.168.55.11`).
**Stop** if a database is missing, the version is not 18.x, or HA has no live sessions (scribe connectivity is part of the HA pipeline).
Also sample `hass.states` once (recorder table, bulk-writes on HA restart): `docker compose exec -T timescaledb psql -U postgres -d hass -Atc "select count(*) from states;"`.
**Expected** — `states_raw` increases between samples (verified: 2665 → 2693 in 30 s). `states` count is sane (thousands).
**Stop** if `states_raw` is static across samples while HA is up — writes have stalled.
- Scribe hypertables: `states_raw` and `events`, both `1` dimension.
- Compression configured for `states_raw` (segmentby `metadata_id` idx 1, orderby `time` idx 1) and `events` (segmentby `event_type`, orderby `time`). Note: TimescaleDB 2.29.x has **no**`compression_enabled` column in this view — row presence is the enabled signal.
-`entities` resolves (scribe registry table).
**Stop** if the extension version differs from the pinned 2.29.x line, a hypertable is missing, compression rows vanish, or `entities` is absent (scribe schema broke — see Known issues in [hosts/pgdb.md](../hosts/pgdb.md)).
-`daily/*-latest.dump` symlinks point to **today's**`*-YYYYMMDD.dump` (nightly 02:00 local `Asia/Shanghai`; a fresh container also fires `BACKUP_ON_START`).
-`pg_restore -l` from the **pg-backup** container lists the archive TOC without error (timescaledb does not mount `/backups`).
**Stop** if the latest dump is not from today, is not custom format, or `pg_restore -l` fails. Stray non-`daily/` dumps at the `/opt/database/backups/` root are pre-compose leftovers — ignore for health, flag for cleanup.
- timescaledb: no new `ERROR`/`FATAL`. Known benign history: an old `relation "hass.states" does not exist` from a wrong-schema probe and `compression_enabled` column errors from an outdated query — neither recurs with the commands above.
- pg-backup: recent successful run (`SQL backup created successfully` for each database, no restore/cleanup errors).
**Stop** if repeated `ERROR`/`FATAL` appear or a backup run failed.
ssh -F /dev/null -o BatchMode=yes windy@192.168.55.15 'cd /opt/database && docker compose exec -T timescaledb psql -U postgres -d scribe -Atc "select max(period) as last_day, current_date - 2 as min_expected from csg_history where kind='\''day'\'';"'
# b) period 上是否有缺口(期望 0 行)
ssh -F /dev/null -o BatchMode=yes windy@192.168.55.15 'cd /opt/database && docker compose exec -T timescaledb psql -U postgres -d scribe -Atc "select period from generate_series('\''2026-07-01'\''::date, (select max(period) from csg_history where kind='\''day'\''), '\''1 day'\'') period except select period from csg_history where kind='\''day'\'';"'
# c) 两个 CSG job 的状态(期望均为 Success / 0 failures)
ssh -F /dev/null -o BatchMode=yes windy@192.168.55.15 'cd /opt/database && docker compose exec -T timescaledb psql -U postgres -d scribe -Atc "select j.job_id, j.proc_name, s.last_run_status, s.total_runs, s.total_failures from timescaledb_information.jobs j join timescaledb_information.job_stats s on s.job_id = j.job_id where j.proc_name like '\''csg%'\'' order by j.job_id;"'
# d) 日费用与月费用的口径一致性(期望 |diff| <= 0.05)
ssh -F /dev/null -o BatchMode=yes windy@192.168.55.15 'cd /opt/database && docker compose exec -T timescaledb psql -U postgres -d scribe -Atc "select m.period, m.usage_kwh, m.cost, round(d.s,2) as day_sum, round(d.s - m.cost,2) as diff from csg_history m left join (select date_trunc('\''month'\'',period)::date as mon, sum(cost) as s from csg_history where kind='\''day'\'' group by 1) d on d.mon = m.period where m.kind='\''month'\'' and m.period >= '\''2026-07-01'\'' order by m.period;"'
```
**Expected**
- a) `last_day >= current_date - 2`。
- b) 0 行。
- c) `csg_daily_snapshot`(job 1010,14:30 UTC = 22:30 CST)与
`csg_backfill_missing_days`(job 1011,15:10 UTC = 23:10 CST)均 `Success`、`total_failures = 0`。