Compare commits
21
Commits
de52cb8b57
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6fbbe13c5 | ||
|
|
799c175996 | ||
|
|
eefc803d5f | ||
|
|
5a5eba8014 | ||
|
|
906fb4a759 | ||
|
|
2c5d5e3d45 | ||
|
|
3745c7573c | ||
|
|
812a1eb77d | ||
|
|
e08e5d50de | ||
|
|
ff1a92110c | ||
|
|
cc3fb99c14 | ||
|
|
6879d79cc6 | ||
|
|
c445c5f512 | ||
|
|
3c83246f24 | ||
|
|
c0c975584a | ||
|
|
0034cec925 | ||
|
|
c9dcde1274 | ||
|
|
037c4ccaa5 | ||
|
|
17bb171578 | ||
|
|
cecf7e6331 | ||
|
|
e957bc2bb1 |
@@ -32,3 +32,4 @@ skills-lock.json
|
|||||||
*~
|
*~
|
||||||
# Agent working scratch (not repo content).
|
# Agent working scratch (not repo content).
|
||||||
.agent-work/
|
.agent-work/
|
||||||
|
.tmp-*
|
||||||
|
|||||||
@@ -23,16 +23,20 @@ This repo is the **agent ops handbook + fact source** for maintaining personal V
|
|||||||
> so plain `ssh` aborts with `Bad owner or permissions on ...`. Always use
|
> so plain `ssh` aborts with `Bad owner or permissions on ...`. Always use
|
||||||
> `ssh -F /dev/null` from the agent shell and pass options explicitly
|
> `ssh -F /dev/null` from the agent shell and pass options explicitly
|
||||||
> (`~/.ssh/config` is skipped; e.g. `ssh -F /dev/null -p 2222
|
> (`~/.ssh/config` is skipped; e.g. `ssh -F /dev/null -p 2222
|
||||||
> -i ~/.ssh/id_ed25519 windy@repo.windy.me`). `sudo` never works in the
|
> -i ~/.ssh/id_ed25519 git@repo.windy.me`). `sudo` never works in the
|
||||||
> sandbox (`NoNewPrivs`, no capabilities, `/` read-only). The host itself is
|
> sandbox (`NoNewPrivs`, no capabilities, `/` read-only). The host itself is
|
||||||
> healthy — to inspect or act on the real host from the sandbox use
|
> healthy — to inspect or act on the real host from the sandbox use
|
||||||
> `/mnt/c/WINDOWS/system32/wsl.exe -u root -- <cmd>` (real root: keep
|
> `/mnt/c/WINDOWS/system32/wsl.exe -u root -- <cmd>` (real root: keep
|
||||||
> read-only unless a change is approved).
|
> read-only unless a change is approved).
|
||||||
|
|
||||||
7. Record each material VPS operation, incident, configuration change, or
|
7. Record each material VPS operation, incident, configuration change, or
|
||||||
verification outcome in the corresponding **Linear `vps` project**. Include
|
verification outcome in the corresponding **Plane `vps` project**
|
||||||
scope, action, verification, and remaining follow-up; never put passwords,
|
(self-hosted `plane.chans.xyz`, Plane MCP `mcp__plane__*`, following the
|
||||||
tokens, private keys, recovery keys, or private room IDs in Linear.
|
`plane-workflow` skill). **Linear is retired as a record source (2026-09-03)
|
||||||
|
— do not create Linear issues;** existing W1N-* entries are read-only
|
||||||
|
history. Include scope, action, verification, and remaining follow-up; never
|
||||||
|
put passwords, tokens, private keys, recovery keys, or private room IDs in
|
||||||
|
Plane or Linear.
|
||||||
|
|
||||||
### Runbook execution rules
|
### Runbook execution rules
|
||||||
|
|
||||||
@@ -59,7 +63,7 @@ not maintain a second copy of the machine table here.
|
|||||||
| SMTP `mx2.windy.me:587` (STARTTLS) or `:465` | mx2 | client submission; full email + mailbox password — [runbook](runbooks/mailcow-smtp-client.md) |
|
| SMTP `mx2.windy.me:587` (STARTTLS) or `:465` | mx2 | client submission; full email + mailbox password — [runbook](runbooks/mailcow-smtp-client.md) |
|
||||||
| IMAP `mx2.windy.me:993` | mx2 | same mailbox credentials |
|
| IMAP `mx2.windy.me:993` | mx2 | same mailbox credentials |
|
||||||
| https://auth.wsvc.info | us2 (`/opt/vaultwarden`) | Vaultwarden (Postgres, **operational**) — client Server URL |
|
| https://auth.wsvc.info | us2 (`/opt/vaultwarden`) | Vaultwarden (Postgres, **operational**) — client Server URL |
|
||||||
| `repo.windy.me:2222` | us2 (`/opt/soft-serve`) | Soft Serve git — v0.12.2 pinned, backup sidecar; details in [hosts/us2.wsvc.info.md](hosts/us2.wsvc.info.md) |
|
| `repo.windy.me` (git SSH `:2222` / web HTTPS) | us2 (`/opt/gitea`) | Gitea — 1.27.3-rootless pinned, backup sidecar; details in [hosts/us2.wsvc.info.md](hosts/us2.wsvc.info.md) |
|
||||||
| DNS `ns1.wsvc.info:53` | hk2 (`/opt/pdns`, Auth **5.0.6**) | PowerDNS auth — zones `windy.me`, `wsvc.info`, `chans.xyz` |
|
| DNS `ns1.wsvc.info:53` | hk2 (`/opt/pdns`, Auth **5.0.6**) | PowerDNS auth — zones `windy.me`, `wsvc.info`, `chans.xyz` |
|
||||||
| https://pdns.wsvc.info | hk2 (`poweradmin`) | Poweradmin UI |
|
| https://pdns.wsvc.info | hk2 (`poweradmin`) | Poweradmin UI |
|
||||||
| https://pgweb.wsvc.info | hk2 (`pgweb`) | PowerDNS Postgres browser |
|
| https://pgweb.wsvc.info | hk2 (`pgweb`) | PowerDNS Postgres browser |
|
||||||
@@ -67,6 +71,7 @@ not maintain a second copy of the machine table here.
|
|||||||
| https://synapse.chans.xyz | synapse | Synapse Client-Server + Federation API |
|
| https://synapse.chans.xyz | synapse | Synapse Client-Server + Federation API |
|
||||||
| https://account.chans.xyz | synapse | Matrix Authentication Service (local passwords) |
|
| https://account.chans.xyz | synapse | Matrix Authentication Service (local passwords) |
|
||||||
| https://admin.chans.xyz | synapse | Element Admin console (MAS admin auth) |
|
| https://admin.chans.xyz | synapse | Element Admin console (MAS admin auth) |
|
||||||
|
| https://plane.chans.xyz | synapse (`plane`, Helm `plane-ce` 1.8.0 / v1.4.1) | Plane project management (self-hosted, K3s) |
|
||||||
|
|
||||||
### Upstream docs
|
### Upstream docs
|
||||||
|
|
||||||
@@ -76,8 +81,12 @@ not maintain a second copy of the machine table here.
|
|||||||
|
|
||||||
**Matrix (ESS on synapse):** Matrix homeserver running on `synapse.chans.xyz` via the official ESS (Element Server Suite) Helm chart with Synapse + MAS + Element Web + Admin. DNS zone `chans.xyz` managed by hk2 PowerDNS. Before changing config, read [docs/matrix-upstream.md](docs/matrix-upstream.md) and [hosts/synapse.chans.xyz.md](hosts/synapse.chans.xyz.md). K3s cluster on this node has hostPort 80/443 for Traefik (no ServiceLB). Health: [matrix-health](runbooks/matrix-health.md).
|
**Matrix (ESS on synapse):** Matrix homeserver running on `synapse.chans.xyz` via the official ESS (Element Server Suite) Helm chart with Synapse + MAS + Element Web + Admin. DNS zone `chans.xyz` managed by hk2 PowerDNS. Before changing config, read [docs/matrix-upstream.md](docs/matrix-upstream.md) and [hosts/synapse.chans.xyz.md](hosts/synapse.chans.xyz.md). K3s cluster on this node has hostPort 80/443 for Traefik (no ServiceLB). Health: [matrix-health](runbooks/matrix-health.md).
|
||||||
|
|
||||||
|
**Plane (on synapse):** Self-hosted Plane project management at `plane.chans.xyz`, Helm release `plane-app` (chart `plane-ce-1.8.0`, app `v1.4.1`) in ns `plane` on the same K3s node as Matrix. Config from `/home/windy/plane-k3s/values.yaml`; workload/cert/ingress details in [hosts/synapse.chans.xyz.md](hosts/synapse.chans.xyz.md). Its Postgres/MinIO PVCs are **not** backed up.
|
||||||
|
|
||||||
**RustDesk:** Self-hosted RustDesk server on `hk2.chans.xyz` (`/opt/rustdesk`, containers `hbbs`/`hbbr`, image pinned `1.1.14`). The `hbbs -r` relay hostname must resolve to the host's public IP `154.36.174.161` — use `hk2.chans.xyz` (never `hk2.wsvc.info`, which has no DNS record). Health: [rustdesk-health](runbooks/rustdesk-health.md).
|
**RustDesk:** Self-hosted RustDesk server on `hk2.chans.xyz` (`/opt/rustdesk`, containers `hbbs`/`hbbr`, image pinned `1.1.14`). The `hbbs -r` relay hostname must resolve to the host's public IP `154.36.174.161` — use `hk2.chans.xyz` (never `hk2.wsvc.info`, which has no DNS record). Health: [rustdesk-health](runbooks/rustdesk-health.md).
|
||||||
|
|
||||||
|
**Gitea (on us2):** Self-hosted Gitea at `repo.windy.me` (web HTTPS + git SSH `:2222`; clone `ssh://git@repo.windy.me:2222/windy/<repo>.git` — user must be `git@`, path needs the owner segment). Replaced Soft Serve 2026-09-18 (Plane VPS-94; `/opt/soft-serve` kept stopped as rollback). Before changing `/opt/gitea`, read [hosts/us2.wsvc.info.md](hosts/us2.wsvc.info.md). Image digest-pinned (`1.27.3-rootless`); upgrade = new digest in `compose.yml` + `docker compose up -d` + re-run health. `config/app.ini` is mode 600 and holds SECRET_KEY/INTERNAL_TOKEN — never commit; the backup sidecar includes it because restores need it. Health: [gitea-health](runbooks/gitea-health.md).
|
||||||
|
|
||||||
## Runbooks & scripts
|
## Runbooks & scripts
|
||||||
|
|
||||||
| Task | Path |
|
| Task | Path |
|
||||||
@@ -91,8 +100,11 @@ not maintain a second copy of the machine table here.
|
|||||||
| PowerDNS health (hk2) | [runbooks/pdns-health.md](runbooks/pdns-health.md) |
|
| PowerDNS health (hk2) | [runbooks/pdns-health.md](runbooks/pdns-health.md) |
|
||||||
| PowerDNS upstream refs | [docs/pdns-upstream.md](docs/pdns-upstream.md) |
|
| PowerDNS upstream refs | [docs/pdns-upstream.md](docs/pdns-upstream.md) |
|
||||||
| Matrix health | [runbooks/matrix-health.md](runbooks/matrix-health.md) |
|
| Matrix health | [runbooks/matrix-health.md](runbooks/matrix-health.md) |
|
||||||
|
| Plane health | [runbooks/plane-health.md](runbooks/plane-health.md) |
|
||||||
| RustDesk health (hk2) | [runbooks/rustdesk-health.md](runbooks/rustdesk-health.md) |
|
| RustDesk health (hk2) | [runbooks/rustdesk-health.md](runbooks/rustdesk-health.md) |
|
||||||
|
| Gitea health (us2) | [runbooks/gitea-health.md](runbooks/gitea-health.md) |
|
||||||
| AdGuard Home health | [runbooks/adguard-home-health.md](runbooks/adguard-home-health.md) |
|
| AdGuard Home health | [runbooks/adguard-home-health.md](runbooks/adguard-home-health.md) |
|
||||||
|
| Host disk cleanup | [runbooks/host-disk-cleanup.md](runbooks/host-disk-cleanup.md) |
|
||||||
| Home Assistant maintenance | [runbooks/home-assistant-maintenance.md](runbooks/home-assistant-maintenance.md) + [scripts/ha-maintenance.sh](runbooks/scripts/ha-maintenance.sh) |
|
| Home Assistant maintenance | [runbooks/home-assistant-maintenance.md](runbooks/home-assistant-maintenance.md) + [scripts/ha-maintenance.sh](runbooks/scripts/ha-maintenance.sh) |
|
||||||
| matrix_e2ee update (hass.windy.lan) | [runbooks/matrix-e2ee-update.md](runbooks/matrix-e2ee-update.md) |
|
| matrix_e2ee update (hass.windy.lan) | [runbooks/matrix-e2ee-update.md](runbooks/matrix-e2ee-update.md) |
|
||||||
| Matrix upstream refs | [docs/matrix-upstream.md](docs/matrix-upstream.md) |
|
| Matrix upstream refs | [docs/matrix-upstream.md](docs/matrix-upstream.md) |
|
||||||
@@ -116,7 +128,12 @@ Routine mailcow health: `cd ansible && ansible-playbook playbooks/health-report.
|
|||||||
|
|
||||||
### Issue tracker
|
### Issue tracker
|
||||||
|
|
||||||
Issues are tracked in Linear and created/updated via the Linear MCP (`vps` project). See `docs/agents/issue-tracker.md`.
|
Issues are tracked in **Plane** — self-hosted at `plane.chans.xyz`, project
|
||||||
|
`vps` — and created/updated via the Plane MCP (`mcp__plane__*`), following the
|
||||||
|
`plane-workflow` skill. **Linear is retired as a record source (2026-09-03); do
|
||||||
|
not create Linear issues.** Existing W1N-* entries are read-only history.
|
||||||
|
`docs/agents/issue-tracker.md` documents the retired Linear workflow and is
|
||||||
|
stale; treat this section as authoritative.
|
||||||
|
|
||||||
### Triage labels
|
### Triage labels
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -11,7 +11,8 @@ files: every secret is a `${VAR}` reference resolved from the **server-local
|
|||||||
| `vaultwarden` | us2 (`/opt/vaultwarden`) | `compose/vaultwarden/compose.yml` | static file + `compose-deploy.yml` |
|
| `vaultwarden` | us2 (`/opt/vaultwarden`) | `compose/vaultwarden/compose.yml` | static file + `compose-deploy.yml` |
|
||||||
| `pdns` | hk2 (`/opt/pdns`) | `compose/pdns/compose.yml` | static file + `compose-deploy.yml` |
|
| `pdns` | hk2 (`/opt/pdns`) | `compose/pdns/compose.yml` | static file + `compose-deploy.yml` |
|
||||||
| `pgdb` | pgdb (`/opt/database`, 无 ansible) | `compose/pgdb/compose.yml` | static file(手动部署:scp → `docker compose config -q` → `up -d`;服务器文件名 `docker-compose.yml`) |
|
| `pgdb` | pgdb (`/opt/database`, 无 ansible) | `compose/pgdb/compose.yml` | static file(手动部署:scp → `docker compose config -q` → `up -d`;服务器文件名 `docker-compose.yml`) |
|
||||||
| `soft-serve` | us2 (`/opt/soft-serve`) | `compose/soft-serve/compose.yml` (+ `Dockerfile.backup`, `scripts/`) | static file(参考镜像, 未接入 compose-deploy; 服务器文件为准) |
|
| `soft-serve` | us2 (`/opt/soft-serve`, 已退役停用) | `compose/soft-serve/compose.yml` (+ `Dockerfile.backup`, `scripts/`) | static file(参考镜像; 2026-09-18 被 gitea 替换 VPS-94, 数据保留作回滚) |
|
||||||
|
| `gitea` | us2 (`/opt/gitea`) | `compose/gitea/compose.yml` (+ `Dockerfile.backup`, `scripts/`) | static file(参考镜像, 未接入 compose-deploy; 服务器文件为准; 2026-09-18 替换 soft-serve, VPS-94) |
|
||||||
| `adguardhome` | dns.windy.lan (`/opt/adguardhome`) | — (待从 LAN 提取) | static file (pending) |
|
| `adguardhome` | dns.windy.lan (`/opt/adguardhome`) | — (待从 LAN 提取) | static file (pending) |
|
||||||
| `unifi` | ubnt (`/home/windy/unifi-9`) | — (待从 LAN 提取) | static file (pending) |
|
| `unifi` | ubnt (`/home/windy/unifi-9`) | — (待从 LAN 提取) | static file (pending) |
|
||||||
| `wireguard` | us4 (`/opt/wireguard`) | `ansible/templates/wireguard-compose.yml.j2` | role-rendered (inventory vars) |
|
| `wireguard` | us4 (`/opt/wireguard`) | `ansible/templates/wireguard-compose.yml.j2` | role-rendered (inventory vars) |
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# compose/gitea — 秘密一律走服务器本地 .env, 不入库
|
||||||
|
# 迁移期一次性: Gitea 管理员生成的 token (mirror-migrate.sh 读取, 用后撤销)
|
||||||
|
GITEA_MIGRATE_USER=
|
||||||
|
GITEA_MIGRATE_TOKEN=
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
FROM alpine:3.20
|
||||||
|
RUN apk add --no-cache sqlite rsync tzdata
|
||||||
|
WORKDIR /scripts
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Gitea on us2 — reference compose (Plane VPS-94, 迁移完成 2026-09-18)
|
||||||
|
# 参考镜像, 服务器 /opt/gitea 文件为准 (同 soft-serve 约定, 未接入 compose-deploy)
|
||||||
|
# rootless 镜像: uid 1000 原生非 root; 数据 /var/lib/gitea (宿主 ./data), 配置 /etc/gitea (宿主 ./config)
|
||||||
|
# SSH: 容器内监听 2322 (非特权, SSH_LISTEN_PORT), 对外 repo.windy.me:2222 经 Traefik TCP entrypoint `ssh`
|
||||||
|
services:
|
||||||
|
gitea:
|
||||||
|
image: gitea/gitea@sha256:1c17ecaead42eb3b5391553d8708103a4beb0e86edf5b9ebc1eb269c318845f2 # 1.27.3-rootless
|
||||||
|
container_name: gitea
|
||||||
|
restart: unless-stopped
|
||||||
|
user: "1000:1000"
|
||||||
|
environment:
|
||||||
|
TZ: Asia/Shanghai
|
||||||
|
volumes:
|
||||||
|
- ./data:/var/lib/gitea
|
||||||
|
- ./config:/etc/gitea
|
||||||
|
- ./secrets:/secrets:ro # 复用的 soft-serve host key (SSH_SERVER_HOST_KEYS)
|
||||||
|
networks:
|
||||||
|
- traefik
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
# Web UI: repo.windy.me (2026-09-18 操作者决定复用现有域名, 免 DNS 变更)
|
||||||
|
- traefik.http.routers.gitea-web.rule=Host(`repo.windy.me`)
|
||||||
|
- traefik.http.routers.gitea-web.entrypoints=websecure
|
||||||
|
- traefik.http.routers.gitea-web.tls.certresolver=letsencrypt
|
||||||
|
- traefik.http.services.gitea-web.loadbalancer.server.port=3000
|
||||||
|
# SSH: 接管 :2222 (entrypoint 已存在, router 动态生效, 无需重启 Traefik)
|
||||||
|
- traefik.tcp.routers.gitea-ssh.entrypoints=ssh
|
||||||
|
- traefik.tcp.routers.gitea-ssh.rule=HostSNI(`*`)
|
||||||
|
- traefik.tcp.routers.gitea-ssh.tls=false
|
||||||
|
- traefik.tcp.services.gitea-ssh.loadbalancer.server.port=2322
|
||||||
|
|
||||||
|
gitea-backup:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.backup
|
||||||
|
container_name: gitea-backup
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./data:/data:ro
|
||||||
|
- ./config:/config:ro
|
||||||
|
- ./backups:/backup
|
||||||
|
- ./scripts:/scripts
|
||||||
|
environment:
|
||||||
|
TZ: Asia/Shanghai
|
||||||
|
BACKUP_UID: 1000
|
||||||
|
BACKUP_GID: 1000
|
||||||
|
entrypoint: >
|
||||||
|
/bin/sh -ec "
|
||||||
|
umask 077 &&
|
||||||
|
touch /backup/backup.log &&
|
||||||
|
crontab /scripts/crontab.txt &&
|
||||||
|
echo '[INFO] gitea backup cron installed' &&
|
||||||
|
crond -f -l 8
|
||||||
|
"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik:
|
||||||
|
external: true
|
||||||
|
name: vw-net
|
||||||
Executable
+20
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
umask 077
|
||||||
|
D() { date "+%Y-%m-%d %H:%M:%S"; }
|
||||||
|
TS=$(date +%Y%m%d_%H%M%S)
|
||||||
|
OUT="/backup/gitea_${TS}"
|
||||||
|
mkdir -p "$OUT"
|
||||||
|
echo "[$(D)] Starting gitea backup -> $OUT"
|
||||||
|
# rootless 布局: app.ini=/etc/gitea(宿主 ./config), db+repos=/var/lib/gitea/data(宿主 ./data/data)
|
||||||
|
# app.ini 含 SECRET_KEY/INTERNAL_TOKEN — 恢复 2FA/session/mirror 凭据必需
|
||||||
|
tar czf "$OUT/app.ini.tar.gz" -C /config app.ini
|
||||||
|
sqlite3 /data/data/gitea.db ".backup '$OUT/gitea.db'"
|
||||||
|
rsync -a /data/data/git/repositories/ "$OUT/repos/"
|
||||||
|
tar czf "$OUT/repos.tar.gz" -C "$OUT" repos
|
||||||
|
rm -rf "$OUT/repos"
|
||||||
|
chmod 600 "$OUT"/*
|
||||||
|
if [ -n "${BACKUP_UID:-}" ] && [ -n "${BACKUP_GID:-}" ]; then
|
||||||
|
chown -R "$BACKUP_UID:$BACKUP_GID" "$OUT" /backup/backup.log
|
||||||
|
fi
|
||||||
|
echo "[$(D)] Backup OK: $(du -sh "$OUT" | cut -f1)"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Run gitea backup daily at 02:00
|
||||||
|
0 2 * * * /bin/sh /scripts/backup.sh >> /backup/backup.log 2>&1
|
||||||
|
# Prune backups older than 14 days daily at 03:00
|
||||||
|
0 3 * * * /bin/sh /scripts/prune.sh >> /backup/backup.log 2>&1
|
||||||
Executable
+41
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# 一次性迁移辅助 (Plane VPS-94 Phase 2): 在 gitea 容器内执行。
|
||||||
|
# 已于 2026-09-18 执行完成 (16 仓), 留档备查; 复用时按 VPS-94 流程重生成一次性 token。
|
||||||
|
# 用法:
|
||||||
|
# GITEA_MIGRATE_USER=<user> GITEA_MIGRATE_TOKEN=<token> \
|
||||||
|
# docker exec -e GITEA_MIGRATE_USER -e GITEA_MIGRATE_TOKEN gitea \
|
||||||
|
# /scripts/mirror-migrate.sh [public_repo ...]
|
||||||
|
# 每仓: API 建仓 (默认 private, 参数中列出的为 public) -> push --mirror。
|
||||||
|
# default_branch 按源仓 symbolic-ref HEAD 设置, 避免非 main 源仓在 Gitea 显示为空。
|
||||||
|
# 结束后按 VPS-94 Phase 3 逐仓核对 git ls-remote ref 全集。
|
||||||
|
set -eu
|
||||||
|
MUSER="${GITEA_MIGRATE_USER:?need GITEA_MIGRATE_USER}"
|
||||||
|
TOKEN="${GITEA_MIGRATE_TOKEN:?need GITEA_MIGRATE_TOKEN}"
|
||||||
|
SRC="/migration-src"
|
||||||
|
API="http://localhost:3000/api/v1"
|
||||||
|
PUBLIC_REPOS=" $* "
|
||||||
|
|
||||||
|
migrate_one() {
|
||||||
|
dir="$1"
|
||||||
|
git -C "$dir" rev-parse --git-dir >/dev/null 2>&1 || { echo "[SKIP] $dir (not a git repo)"; return 0; }
|
||||||
|
name=$(basename "$dir"); name=${name%.git}
|
||||||
|
def_branch=$(git -C "$dir" symbolic-ref --short HEAD)
|
||||||
|
case "$PUBLIC_REPOS" in *" $name "*) private=false ;; *) private=true ;; esac
|
||||||
|
echo "[MIGRATE] $name (default=$def_branch private=$private)"
|
||||||
|
code=$(curl -s -o /dev/null -w '%{http_code}' -X POST "$API/user/repos" \
|
||||||
|
-H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
||||||
|
-d "{\"name\":\"$name\",\"private\":$private,\"default_branch\":\"$def_branch\",\"auto_init\":false}")
|
||||||
|
case "$code" in
|
||||||
|
201) : ;;
|
||||||
|
409) echo " [WARN] $name 已存在, 直接补推" ;;
|
||||||
|
*) echo " [FAIL] create HTTP $code"; return 1 ;;
|
||||||
|
esac
|
||||||
|
git -C "$dir" push --mirror "http://$MUSER:$TOKEN@localhost:3000/$MUSER/$name.git"
|
||||||
|
echo " [OK] $name pushed"
|
||||||
|
}
|
||||||
|
|
||||||
|
for dir in "$SRC"/*.git "$SRC"/cdia; do
|
||||||
|
[ -d "$dir" ] || continue
|
||||||
|
migrate_one "$dir"
|
||||||
|
done
|
||||||
|
echo "[DONE] 全部处理完毕; 迁移后记得撤销一次性 token"
|
||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
D() { date "+%Y-%m-%d %H:%M:%S"; }
|
||||||
|
ls -dt /backup/gitea_* 2>/dev/null | tail -n +15 | xargs -r rm -rf
|
||||||
|
echo "[$(D)] Pruned. Kept $(ls -d /backup/gitea_* 2>/dev/null | wc -l) backups (max 14)"
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
-- CSG 长期归档修复 (2026-09-21)
|
||||||
|
-- 背景:TimescaleDB 任务 1008 `csg_daily_snapshot()` 自 2026-08-29 创建起 0 成功 / 294 失败。
|
||||||
|
-- 原因:函数签名是零参数 `csg_daily_snapshot()`,而 TimescaleDB 自定义 job 动作
|
||||||
|
-- 按名字调用 `schema.proc(job_id integer, config jsonb)`,解析不到 → 每次报
|
||||||
|
-- `function or procedure "public.csg_daily_snapshot(integer, jsonb)" does not exist`。
|
||||||
|
-- 后果:csg_history 的 day 行停在 2026-08-28、month 行停在 2026-08-01(且 2026-08 的
|
||||||
|
-- month 行是 08-29 当时的「本月至今」302.47 kWh / 180.28 元,不是月终值 331.22 / 198.65)。
|
||||||
|
--
|
||||||
|
-- 本文件的三个动作(幂等,可重复执行):
|
||||||
|
-- 1. `csg_ladder_cost(kwh, month)` —— 阶梯电价助手(广州:夏季 5-10 月 260/600,
|
||||||
|
-- 非夏季 200/400;0.589 / 0.639 / 0.889 元每 kWh)。归档侧的唯一计价来源。
|
||||||
|
-- 2. `csg_daily_snapshot(job_id integer, config jsonb)` v3 —— 兼容 TimescaleDB job 签名;
|
||||||
|
-- 并把 month 行按「数据日期 d 所属月份」归属:
|
||||||
|
-- · d 属于当前月 → usage 取集成本月累计,cost 用阶梯助手重算
|
||||||
|
-- · d 属于上月 → usage 由该月 day 行汇总(不读集成 last_month,避开翻月瞬态:
|
||||||
|
-- 2026-08-31 16:32 实测 last_month_total_usage 曾读到 323.49 这种误值)
|
||||||
|
-- 并加「不降级」保护:day 行汇总结果小于已记录的 month 行时不动。
|
||||||
|
-- 3. 回填 2026-08-29 → 2026-09-19 的 day 行(数据源:08-29..08-31 取 scribe
|
||||||
|
-- states_raw 里 `yesterday_kwh` 观测值减一天;09-01..09-19 取 CSG 集成
|
||||||
|
-- `this_month_by_day`,其中 09-06 = 9.29 是 scribe 漏采的观测)。日费用一律用
|
||||||
|
-- 「当日用电 × 当日所处的边际档位」计算,使整月 day 费用之和 == 阶梯月费用。
|
||||||
|
--
|
||||||
|
-- 回滚(脚本内已自动建快照表 csg_history_bak_20260921):
|
||||||
|
-- BEGIN;
|
||||||
|
-- SELECT delete_job((SELECT job_id FROM timescaledb_information.jobs WHERE proc_name='csg_daily_snapshot'));
|
||||||
|
-- DROP FUNCTION IF EXISTS public.csg_daily_snapshot(integer, jsonb);
|
||||||
|
-- CREATE FUNCTION public.csg_daily_snapshot() RETURNS void LANGUAGE plpgsql AS $f$ ...原函数体... $f$;
|
||||||
|
-- SELECT add_job('public.csg_daily_snapshot'::regproc, INTERVAL '1 day');
|
||||||
|
-- DROP FUNCTION IF EXISTS public.csg_ladder_cost(numeric, date);
|
||||||
|
-- DELETE FROM csg_history; INSERT INTO csg_history SELECT * FROM csg_history_bak_20260921;
|
||||||
|
-- COMMIT;
|
||||||
|
-- (或整体放弃归档:DROP TABLE csg_history + 删任务,见 hosts/pgdb.md 回滚段)
|
||||||
|
|
||||||
|
\set ON_ERROR_STOP on
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- ---------- 0. 回滚快照(只建一次) ----------
|
||||||
|
CREATE TABLE IF NOT EXISTS public.csg_history_bak_20260921 AS TABLE public.csg_history;
|
||||||
|
|
||||||
|
-- ---------- 1. 阶梯电价助手 ----------
|
||||||
|
CREATE OR REPLACE FUNCTION public.csg_ladder_cost(kwh numeric, month_date date)
|
||||||
|
RETURNS numeric
|
||||||
|
LANGUAGE sql
|
||||||
|
IMMUTABLE
|
||||||
|
AS $function$
|
||||||
|
SELECT CASE
|
||||||
|
WHEN kwh IS NULL OR kwh <= 0 THEN 0::numeric
|
||||||
|
WHEN kwh <= (CASE WHEN extract(month FROM month_date) BETWEEN 5 AND 10 THEN 260 ELSE 200 END)
|
||||||
|
THEN round(kwh * 0.589, 2)
|
||||||
|
WHEN kwh <= (CASE WHEN extract(month FROM month_date) BETWEEN 5 AND 10 THEN 600 ELSE 400 END)
|
||||||
|
THEN round((CASE WHEN extract(month FROM month_date) BETWEEN 5 AND 10 THEN 260 ELSE 200 END) * 0.589
|
||||||
|
+ (kwh - (CASE WHEN extract(month FROM month_date) BETWEEN 5 AND 10 THEN 260 ELSE 200 END)) * 0.639, 2)
|
||||||
|
ELSE round((CASE WHEN extract(month FROM month_date) BETWEEN 5 AND 10 THEN 260 ELSE 200 END) * 0.589
|
||||||
|
+ ((CASE WHEN extract(month FROM month_date) BETWEEN 5 AND 10 THEN 600 ELSE 400 END)
|
||||||
|
- (CASE WHEN extract(month FROM month_date) BETWEEN 5 AND 10 THEN 260 ELSE 200 END)) * 0.639
|
||||||
|
+ (kwh - (CASE WHEN extract(month FROM month_date) BETWEEN 5 AND 10 THEN 600 ELSE 400 END)) * 0.889, 2)
|
||||||
|
END;
|
||||||
|
$function$;
|
||||||
|
|
||||||
|
-- ---------- 2. 重建为 TimescaleDB 兼容签名的 v3 + 重挂 job ----------
|
||||||
|
-- 注意:不能直接 DROP 旧函数——TimescaleDB job 1008 持有依赖
|
||||||
|
-- (ERROR: cannot drop public.csg_daily_snapshot because background job 1008 depends on it)。
|
||||||
|
-- 因此:delete_job → 换签名重建函数 → add_job 重挂,并把排程对齐文档口径
|
||||||
|
-- (22:30 Asia/Shanghai = 14:30 UTC;原来实际跑在 22:00 UTC = 06:00 CST,
|
||||||
|
-- 那个时点集成还没发布前一日数据,d 会晚一天)。
|
||||||
|
DO $do$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM timescaledb_information.jobs WHERE job_id = 1008) THEN
|
||||||
|
PERFORM delete_job(1008);
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$do$;
|
||||||
|
|
||||||
|
DROP FUNCTION IF EXISTS public.csg_daily_snapshot();
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION public.csg_daily_snapshot(
|
||||||
|
job_id integer DEFAULT NULL,
|
||||||
|
config jsonb DEFAULT NULL
|
||||||
|
)
|
||||||
|
RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
d date;
|
||||||
|
v_usage numeric; v_cost numeric; v_ladder text; v_balance numeric;
|
||||||
|
m_usage numeric; v_tariff numeric;
|
||||||
|
p_month date; p_usage numeric; prev_usage numeric;
|
||||||
|
BEGIN
|
||||||
|
-- 数据日期 = CSG 集成「最新一天」(数据滞后一天);COALESCE 兜底昨天
|
||||||
|
SELECT COALESCE(
|
||||||
|
(SELECT (attributes->>'latest_day_date')::date
|
||||||
|
FROM states_raw s JOIN entities e ON e.id = s.metadata_id
|
||||||
|
WHERE e.entity_id = 'sensor.0800041935246530_latest_day_kwh'
|
||||||
|
ORDER BY s.time DESC LIMIT 1),
|
||||||
|
CURRENT_DATE - 1) INTO d;
|
||||||
|
|
||||||
|
-- 一律取「最新有值行」:轮询先写瞬态 unknown、~4s 后才写有值行
|
||||||
|
SELECT s.value INTO v_usage FROM states_raw s JOIN entities e ON e.id=s.metadata_id
|
||||||
|
WHERE e.entity_id='sensor.0800041935246530_yesterday_kwh' AND s.value IS NOT NULL ORDER BY s.time DESC LIMIT 1;
|
||||||
|
SELECT s.value INTO v_cost FROM states_raw s JOIN entities e ON e.id=s.metadata_id
|
||||||
|
WHERE e.entity_id='sensor.0800041935246530_latest_day_cost' AND s.value IS NOT NULL ORDER BY s.time DESC LIMIT 1;
|
||||||
|
SELECT s.state INTO v_ladder FROM states_raw s JOIN entities e ON e.id=s.metadata_id
|
||||||
|
WHERE e.entity_id='sensor.csg_current_ladder' AND s.state IS NOT NULL AND s.state NOT IN ('unknown','unavailable') ORDER BY s.time DESC LIMIT 1;
|
||||||
|
SELECT s.value INTO v_balance FROM states_raw s JOIN entities e ON e.id=s.metadata_id
|
||||||
|
WHERE e.entity_id='sensor.0800041935246530_balance' AND s.value IS NOT NULL ORDER BY s.time DESC LIMIT 1;
|
||||||
|
SELECT s.value INTO m_usage FROM states_raw s JOIN entities e ON e.id=s.metadata_id
|
||||||
|
WHERE e.entity_id='sensor.0800041935246530_this_month_total_usage' AND s.value IS NOT NULL ORDER BY s.time DESC LIMIT 1;
|
||||||
|
-- 日费用回退:原生 latest_day_cost 从未有值(集成侧该传感器恒为 unknown),
|
||||||
|
-- 用「昨日用电 × 当前档费率」估计;账单口径可用时优先原生
|
||||||
|
SELECT s.value INTO v_tariff FROM states_raw s JOIN entities e ON e.id=s.metadata_id
|
||||||
|
WHERE e.entity_id='sensor.csg_current_ladder_tariff' AND s.value IS NOT NULL ORDER BY s.time DESC LIMIT 1;
|
||||||
|
IF v_cost IS NULL AND v_usage IS NOT NULL AND v_tariff IS NOT NULL THEN
|
||||||
|
v_cost := v_usage * v_tariff;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
INSERT INTO csg_history (period, kind, usage_kwh, cost, ladder, balance)
|
||||||
|
VALUES (d, 'day', v_usage, v_cost, v_ladder, v_balance)
|
||||||
|
ON CONFLICT (period, kind) DO UPDATE SET
|
||||||
|
usage_kwh = COALESCE(EXCLUDED.usage_kwh, csg_history.usage_kwh),
|
||||||
|
cost = COALESCE(EXCLUDED.cost, csg_history.cost),
|
||||||
|
ladder = COALESCE(EXCLUDED.ladder, csg_history.ladder),
|
||||||
|
balance = COALESCE(EXCLUDED.balance, csg_history.balance),
|
||||||
|
updated_at = now();
|
||||||
|
|
||||||
|
-- month 行按 d 所属月份归属(v3:修掉翻月时用错月份累计的 bug)
|
||||||
|
p_month := date_trunc('month', d)::date;
|
||||||
|
|
||||||
|
IF p_month = date_trunc('month', CURRENT_DATE)::date THEN
|
||||||
|
-- 当月:集成本月累计优先,缺失则退回 day 行汇总;费用一律阶梯重算
|
||||||
|
SELECT COALESCE(m_usage, sum(h.usage_kwh)) INTO p_usage
|
||||||
|
FROM csg_history h
|
||||||
|
WHERE h.kind='day' AND h.period >= p_month AND h.period < (p_month + interval '1 month');
|
||||||
|
ELSE
|
||||||
|
-- 上月/更早:只信该月 day 行(集成翻月时 last_month_* 有瞬态误值)
|
||||||
|
SELECT sum(h.usage_kwh) INTO p_usage
|
||||||
|
FROM csg_history h
|
||||||
|
WHERE h.kind='day' AND h.period >= p_month AND h.period < (p_month + interval '1 month');
|
||||||
|
SELECT h.usage_kwh INTO prev_usage FROM csg_history h
|
||||||
|
WHERE h.kind='month' AND h.period = p_month;
|
||||||
|
IF p_usage IS NULL OR (prev_usage IS NOT NULL AND prev_usage > p_usage) THEN
|
||||||
|
RETURN; -- day 行不完整,保留已有 month 行,不降级
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_usage IS NOT NULL AND p_usage > 0 THEN
|
||||||
|
INSERT INTO csg_history (period, kind, usage_kwh, cost)
|
||||||
|
VALUES (p_month, 'month', p_usage, public.csg_ladder_cost(p_usage, p_month))
|
||||||
|
ON CONFLICT (period, kind) DO UPDATE SET
|
||||||
|
usage_kwh = EXCLUDED.usage_kwh,
|
||||||
|
cost = EXCLUDED.cost,
|
||||||
|
updated_at = now();
|
||||||
|
END IF;
|
||||||
|
END $function$;
|
||||||
|
|
||||||
|
-- 重挂每日任务:22:30 Asia/Shanghai(14:30 UTC)
|
||||||
|
DO $do$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM timescaledb_information.jobs WHERE proc_name = 'csg_daily_snapshot') THEN
|
||||||
|
PERFORM add_job(proc => 'public.csg_daily_snapshot'::regproc,
|
||||||
|
schedule_interval => INTERVAL '1 day',
|
||||||
|
initial_start => date_trunc('day', now() AT TIME ZONE 'UTC')
|
||||||
|
+ INTERVAL '14 hours 30 minutes',
|
||||||
|
timezone => 'UTC');
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$do$;
|
||||||
|
|
||||||
|
-- ---------- 3. 回填 day 行 2026-08-29 .. 2026-09-19 + 重算月行 ----------
|
||||||
|
-- 现有 day 行(07-01..08-28 来自首批回填)与新行一起参与「月内累计」窗口,
|
||||||
|
-- 使边际档位计算正确;已有非空 cost 不覆盖。
|
||||||
|
WITH newrows(period, usage_kwh) AS (
|
||||||
|
VALUES
|
||||||
|
(DATE '2026-08-29', 10.99::numeric),
|
||||||
|
(DATE '2026-08-30', 10.03::numeric),
|
||||||
|
(DATE '2026-08-31', 7.73::numeric),
|
||||||
|
(DATE '2026-09-01', 8.50::numeric),
|
||||||
|
(DATE '2026-09-02', 6.68::numeric),
|
||||||
|
(DATE '2026-09-03', 8.74::numeric),
|
||||||
|
(DATE '2026-09-04', 6.76::numeric),
|
||||||
|
(DATE '2026-09-05', 7.76::numeric),
|
||||||
|
(DATE '2026-09-06', 9.29::numeric),
|
||||||
|
(DATE '2026-09-07', 7.24::numeric),
|
||||||
|
(DATE '2026-09-08', 13.00::numeric),
|
||||||
|
(DATE '2026-09-09', 7.98::numeric),
|
||||||
|
(DATE '2026-09-10', 8.16::numeric),
|
||||||
|
(DATE '2026-09-11', 8.75::numeric),
|
||||||
|
(DATE '2026-09-12', 10.48::numeric),
|
||||||
|
(DATE '2026-09-13', 11.11::numeric),
|
||||||
|
(DATE '2026-09-14', 7.22::numeric),
|
||||||
|
(DATE '2026-09-15', 9.94::numeric),
|
||||||
|
(DATE '2026-09-16', 7.19::numeric),
|
||||||
|
(DATE '2026-09-17', 10.32::numeric),
|
||||||
|
(DATE '2026-09-18', 7.77::numeric),
|
||||||
|
(DATE '2026-09-19', 11.79::numeric)
|
||||||
|
),
|
||||||
|
allrows AS (
|
||||||
|
SELECT h.period, h.usage_kwh FROM csg_history h
|
||||||
|
WHERE h.kind='day' AND h.period BETWEEN DATE '2026-07-01' AND DATE '2026-09-19'
|
||||||
|
UNION ALL
|
||||||
|
SELECT n.period, n.usage_kwh FROM newrows n
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM csg_history h WHERE h.kind='day' AND h.period = n.period)
|
||||||
|
),
|
||||||
|
calc AS (
|
||||||
|
SELECT period, usage_kwh,
|
||||||
|
CASE WHEN extract(month FROM period) BETWEEN 5 AND 10 THEN 260 ELSE 200 END AS t1,
|
||||||
|
CASE WHEN extract(month FROM period) BETWEEN 5 AND 10 THEN 600 ELSE 400 END AS t2,
|
||||||
|
sum(usage_kwh) OVER (PARTITION BY date_trunc('month', period)
|
||||||
|
ORDER BY period ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS c1
|
||||||
|
FROM allrows
|
||||||
|
),
|
||||||
|
slab AS (
|
||||||
|
SELECT period, usage_kwh, t1, t2, c1, c1 - usage_kwh AS c0 FROM calc
|
||||||
|
),
|
||||||
|
priced AS (
|
||||||
|
SELECT period, usage_kwh,
|
||||||
|
round(
|
||||||
|
(CASE WHEN c1 <= t1 THEN c1*0.589
|
||||||
|
WHEN c1 <= t2 THEN t1*0.589 + (c1-t1)*0.639
|
||||||
|
ELSE t1*0.589 + (t2-t1)*0.639 + (c1-t2)*0.889 END)
|
||||||
|
- (CASE WHEN c0 <= t1 THEN c0*0.589
|
||||||
|
WHEN c0 <= t2 THEN t1*0.589 + (c0-t1)*0.639
|
||||||
|
ELSE t1*0.589 + (t2-t1)*0.639 + (c0-t2)*0.889 END), 2) AS cost,
|
||||||
|
CASE WHEN c1 <= t1 THEN '一档' WHEN c1 <= t2 THEN '二档' ELSE '三档' END AS ladder
|
||||||
|
FROM slab
|
||||||
|
)
|
||||||
|
INSERT INTO csg_history (period, kind, usage_kwh, cost, ladder)
|
||||||
|
SELECT period, 'day', usage_kwh, cost, ladder FROM priced
|
||||||
|
ON CONFLICT (period, kind) DO UPDATE SET
|
||||||
|
usage_kwh = EXCLUDED.usage_kwh,
|
||||||
|
cost = COALESCE(csg_history.cost, EXCLUDED.cost),
|
||||||
|
ladder = COALESCE(csg_history.ladder, EXCLUDED.ladder),
|
||||||
|
updated_at = now();
|
||||||
|
|
||||||
|
-- 月行按 day 行汇总重算(只动 2026-08 / 2026-09;2026-07 及更早的月行来自集成
|
||||||
|
-- by_month,day 行并不覆盖整月,重算会把它算小,故不触碰)
|
||||||
|
INSERT INTO csg_history (period, kind, usage_kwh, cost)
|
||||||
|
SELECT date_trunc('month', h.period)::date, 'month', round(sum(h.usage_kwh),2),
|
||||||
|
public.csg_ladder_cost(round(sum(h.usage_kwh),2), date_trunc('month', h.period)::date)
|
||||||
|
FROM csg_history h
|
||||||
|
WHERE h.kind='day' AND h.period BETWEEN DATE '2026-08-01' AND DATE '2026-09-30'
|
||||||
|
GROUP BY date_trunc('month', h.period)
|
||||||
|
ON CONFLICT (period, kind) DO UPDATE SET
|
||||||
|
usage_kwh = EXCLUDED.usage_kwh,
|
||||||
|
cost = EXCLUDED.cost,
|
||||||
|
updated_at = now();
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
-- CSG 长期归档 v5 (2026-09-22, VPS-78)
|
||||||
|
--
|
||||||
|
-- 背景:v3 修好了 job(1008 → 1010)并回填,但 csg_daily_snapshot() 写 day 行时用的是
|
||||||
|
-- 「昨日用电 × 当前档费率」**单一费率**且未舍入,而 v3 的回填(07-01..09-19)用的是
|
||||||
|
-- 「当月累积的边际差分」并 round(...,2)。两套口径混在同一列:
|
||||||
|
-- · 实测今天只差**精度**(3 行:2026-08-28 / 09-19 / 09-20,总差 -0.01 元);
|
||||||
|
-- · 但一旦某个**跨档日**由夜间任务写出,就会与历史口径不一致,并破坏
|
||||||
|
-- 「day 费用之和 ≈ 阶梯月费用」这条 v3 建立的验收不变量。
|
||||||
|
-- · 时限:2026-09-22 时 csg_current_ladder_remaining_kwh = 80.6、本月日均 8.97
|
||||||
|
-- → 约 2026-09-29 跨入二档。
|
||||||
|
--
|
||||||
|
-- 本文件的四个动作(幂等,可重复执行):
|
||||||
|
-- 1. csg_ladder_cost_raw(kwh, month) —— **无舍入**的阶梯费用助手。
|
||||||
|
-- v3 回填的算法是 round(精确c1 − 精确c0, 2)。若直接用
|
||||||
|
-- csg_ladder_cost(c1) − csg_ladder_cost(c0),会**二次舍入**(7 月实测 1 天差 0.01),
|
||||||
|
-- 故新增 raw 版,只在最后舍入一次。
|
||||||
|
-- 2. csg_daily_snapshot() v5 —— day cost 改为「当月累积边际差分 + round(...,2)」;
|
||||||
|
-- 并把 p_month 的计算**上移到计费之前**,加**跨月守卫**
|
||||||
|
-- (每月 1 日的 d 落在上月,此时 m_usage 是「新月份」累计,不能作起点,否则为负)。
|
||||||
|
-- 同时把「数据日期」与「当日用量」改为取**同一个实体同一行**
|
||||||
|
-- (sensor.0800041935246530_latest_day_kwh):v3 用 latest_day_kwh 取日期、
|
||||||
|
-- 却用 yesterday_kwh 取数值,两个实体可能错配;且 latest_day_kwh 的归档更完整
|
||||||
|
-- (含 2026-09-06 = 9.29 这条 yesterday_kwh 没有的观测)。
|
||||||
|
-- 3. 一次性归一:把历史 day 行的 cost 按新口径重算(实测仅 3 行变化,总差 -0.01)。
|
||||||
|
-- 4. csg_backfill_missing_days() + job 1011 —— **断档自愈**:只 INSERT 缺失日期,
|
||||||
|
-- ON CONFLICT DO NOTHING,**绝不覆盖既有行**;数据源是 scribe.states_raw 里
|
||||||
|
-- latest_day_kwh 的 (latest_day_date 属性, value) 观测对(该属性 v3 起就有归档)。
|
||||||
|
--
|
||||||
|
-- 回滚(脚本已自动建快照表 csg_history_bak_20260922):
|
||||||
|
-- BEGIN;
|
||||||
|
-- SELECT delete_job((SELECT job_id FROM timescaledb_information.jobs
|
||||||
|
-- WHERE proc_name='csg_backfill_missing_days'));
|
||||||
|
-- DROP FUNCTION IF EXISTS public.csg_backfill_missing_days(integer, jsonb);
|
||||||
|
-- DROP FUNCTION IF EXISTS public.csg_ladder_cost_raw(numeric, date);
|
||||||
|
-- -- v3 的 csg_daily_snapshot 用 compose/pgdb/csg-snapshot-v3.sql 的原文重建即可
|
||||||
|
-- DELETE FROM csg_history; INSERT INTO csg_history SELECT * FROM csg_history_bak_20260922;
|
||||||
|
-- COMMIT;
|
||||||
|
-- (或整体放弃归档:DROP TABLE csg_history + 删 job,见 hosts/pgdb.md 回滚段)
|
||||||
|
|
||||||
|
\set ON_ERROR_STOP on
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- ---------- 0. 回滚快照(只建一次) ----------
|
||||||
|
CREATE TABLE IF NOT EXISTS public.csg_history_bak_20260922 AS TABLE public.csg_history;
|
||||||
|
|
||||||
|
-- ---------- 1. 无舍入阶梯费用助手(差分用) ----------
|
||||||
|
CREATE OR REPLACE FUNCTION public.csg_ladder_cost_raw(kwh numeric, month_date date)
|
||||||
|
RETURNS numeric
|
||||||
|
LANGUAGE sql
|
||||||
|
IMMUTABLE
|
||||||
|
AS $function$
|
||||||
|
SELECT CASE
|
||||||
|
WHEN kwh IS NULL OR kwh <= 0 THEN 0::numeric
|
||||||
|
WHEN kwh <= (CASE WHEN extract(month FROM month_date) BETWEEN 5 AND 10 THEN 260 ELSE 200 END)
|
||||||
|
THEN kwh * 0.589
|
||||||
|
WHEN kwh <= (CASE WHEN extract(month FROM month_date) BETWEEN 5 AND 10 THEN 600 ELSE 400 END)
|
||||||
|
THEN (CASE WHEN extract(month FROM month_date) BETWEEN 5 AND 10 THEN 260 ELSE 200 END) * 0.589
|
||||||
|
+ (kwh - (CASE WHEN extract(month FROM month_date) BETWEEN 5 AND 10 THEN 260 ELSE 200 END)) * 0.639
|
||||||
|
ELSE (CASE WHEN extract(month FROM month_date) BETWEEN 5 AND 10 THEN 260 ELSE 200 END) * 0.589
|
||||||
|
+ ((CASE WHEN extract(month FROM month_date) BETWEEN 5 AND 10 THEN 600 ELSE 400 END)
|
||||||
|
- (CASE WHEN extract(month FROM month_date) BETWEEN 5 AND 10 THEN 260 ELSE 200 END)) * 0.639
|
||||||
|
+ (kwh - (CASE WHEN extract(month FROM month_date) BETWEEN 5 AND 10 THEN 600 ELSE 400 END)) * 0.889
|
||||||
|
END;
|
||||||
|
$function$;
|
||||||
|
|
||||||
|
-- ---------- 2. csg_daily_snapshot() v5 ----------
|
||||||
|
CREATE OR REPLACE FUNCTION public.csg_daily_snapshot(
|
||||||
|
job_id integer DEFAULT NULL,
|
||||||
|
config jsonb DEFAULT NULL
|
||||||
|
)
|
||||||
|
RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
d date;
|
||||||
|
v_usage numeric; v_cost numeric; v_ladder text; v_balance numeric;
|
||||||
|
m_usage numeric;
|
||||||
|
p_month date; p_usage numeric; prev_usage numeric; c0 numeric; c1 numeric;
|
||||||
|
BEGIN
|
||||||
|
-- 数据日期与当日用量取【同一实体的同一行】:latest_day_kwh
|
||||||
|
-- (v3 的 d 来自它的 latest_day_date,但 v_usage 来自另一个实体 yesterday_kwh)
|
||||||
|
SELECT (s.attributes->>'latest_day_date')::date, s.value
|
||||||
|
INTO d, v_usage
|
||||||
|
FROM states_raw s JOIN entities e ON e.id = s.metadata_id
|
||||||
|
WHERE e.entity_id = 'sensor.0800041935246530_latest_day_kwh'
|
||||||
|
AND s.value IS NOT NULL
|
||||||
|
AND s.attributes->>'latest_day_date' IS NOT NULL
|
||||||
|
ORDER BY s.time DESC LIMIT 1;
|
||||||
|
d := COALESCE(d, CURRENT_DATE - 1);
|
||||||
|
|
||||||
|
-- 一律取「最新有值行」:轮询先写瞬态 unknown、~4s 后才写有值行
|
||||||
|
SELECT s.value INTO v_cost FROM states_raw s JOIN entities e ON e.id=s.metadata_id
|
||||||
|
WHERE e.entity_id='sensor.0800041935246530_latest_day_cost' AND s.value IS NOT NULL ORDER BY s.time DESC LIMIT 1;
|
||||||
|
SELECT s.state INTO v_ladder FROM states_raw s JOIN entities e ON e.id=s.metadata_id
|
||||||
|
WHERE e.entity_id='sensor.csg_current_ladder' AND s.state IS NOT NULL AND s.state NOT IN ('unknown','unavailable') ORDER BY s.time DESC LIMIT 1;
|
||||||
|
SELECT s.value INTO v_balance FROM states_raw s JOIN entities e ON e.id=s.metadata_id
|
||||||
|
WHERE e.entity_id='sensor.0800041935246530_balance' AND s.value IS NOT NULL ORDER BY s.time DESC LIMIT 1;
|
||||||
|
SELECT s.value INTO m_usage FROM states_raw s JOIN entities e ON e.id=s.metadata_id
|
||||||
|
WHERE e.entity_id='sensor.0800041935246530_this_month_total_usage' AND s.value IS NOT NULL ORDER BY s.time DESC LIMIT 1;
|
||||||
|
|
||||||
|
-- 月归属必须在计费之前算:跨月守卫要用它
|
||||||
|
p_month := date_trunc('month', d)::date;
|
||||||
|
|
||||||
|
-- 日费用:与 v3 回填同口径 = 当月累积的边际差分,只在最后舍入一次。
|
||||||
|
-- 原生 latest_day_cost 从未有值(集成侧恒 unknown),故这里是唯一路径。
|
||||||
|
IF v_cost IS NULL AND v_usage IS NOT NULL THEN
|
||||||
|
IF p_month = date_trunc('month', CURRENT_DATE)::date THEN
|
||||||
|
-- 当月日:起点 = 本月累计 − 当日用量
|
||||||
|
IF m_usage IS NOT NULL THEN
|
||||||
|
c1 := m_usage;
|
||||||
|
c0 := m_usage - v_usage;
|
||||||
|
END IF;
|
||||||
|
ELSE
|
||||||
|
-- 上月日(每月 1 日必现,因数据滞后一天):起点 = 该月已归档 day 行之和
|
||||||
|
SELECT COALESCE(sum(h.usage_kwh), 0) INTO c0
|
||||||
|
FROM csg_history h
|
||||||
|
WHERE h.kind='day' AND h.period >= p_month AND h.period < d;
|
||||||
|
c1 := c0 + v_usage;
|
||||||
|
END IF;
|
||||||
|
IF c0 IS NOT NULL AND c1 IS NOT NULL AND c0 >= 0 THEN
|
||||||
|
v_cost := round(public.csg_ladder_cost_raw(c1, p_month)
|
||||||
|
- public.csg_ladder_cost_raw(c0, p_month), 2);
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
INSERT INTO csg_history (period, kind, usage_kwh, cost, ladder, balance)
|
||||||
|
VALUES (d, 'day', v_usage, v_cost, v_ladder, v_balance)
|
||||||
|
ON CONFLICT (period, kind) DO UPDATE SET
|
||||||
|
usage_kwh = COALESCE(EXCLUDED.usage_kwh, csg_history.usage_kwh),
|
||||||
|
cost = COALESCE(EXCLUDED.cost, csg_history.cost),
|
||||||
|
ladder = COALESCE(EXCLUDED.ladder, csg_history.ladder),
|
||||||
|
balance = COALESCE(EXCLUDED.balance, csg_history.balance),
|
||||||
|
updated_at = now();
|
||||||
|
|
||||||
|
-- month 行按 d 所属月份归属(v3:修掉翻月时用错月份累计的 bug)
|
||||||
|
IF p_month = date_trunc('month', CURRENT_DATE)::date THEN
|
||||||
|
-- 当月:集成本月累计优先,缺失则退回 day 行汇总;费用一律阶梯重算
|
||||||
|
SELECT COALESCE(m_usage, sum(h.usage_kwh)) INTO p_usage
|
||||||
|
FROM csg_history h
|
||||||
|
WHERE h.kind='day' AND h.period >= p_month AND h.period < (p_month + interval '1 month');
|
||||||
|
ELSE
|
||||||
|
-- 上月/更早:只信该月 day 行(集成翻月时 last_month_* 有瞬态误值)
|
||||||
|
SELECT sum(h.usage_kwh) INTO p_usage
|
||||||
|
FROM csg_history h
|
||||||
|
WHERE h.kind='day' AND h.period >= p_month AND h.period < (p_month + interval '1 month');
|
||||||
|
SELECT h.usage_kwh INTO prev_usage FROM csg_history h
|
||||||
|
WHERE h.kind='month' AND h.period = p_month;
|
||||||
|
IF p_usage IS NULL OR (prev_usage IS NOT NULL AND prev_usage > p_usage) THEN
|
||||||
|
RETURN; -- day 行不完整,保留已有 month 行,不降级
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_usage IS NOT NULL AND p_usage > 0 THEN
|
||||||
|
INSERT INTO csg_history (period, kind, usage_kwh, cost)
|
||||||
|
VALUES (p_month, 'month', p_usage, public.csg_ladder_cost(p_usage, p_month))
|
||||||
|
ON CONFLICT (period, kind) DO UPDATE SET
|
||||||
|
usage_kwh = EXCLUDED.usage_kwh,
|
||||||
|
cost = EXCLUDED.cost,
|
||||||
|
updated_at = now();
|
||||||
|
END IF;
|
||||||
|
END $function$;
|
||||||
|
|
||||||
|
-- ---------- 3. 一次性归一:历史 day 行 cost 按新口径重算 ----------
|
||||||
|
-- 幂等:只更新与新口径不同的行。实测(写入前 dry-run)仅 3 行变化、总差 -0.01。
|
||||||
|
WITH cum AS (
|
||||||
|
SELECT period, usage_kwh, cost,
|
||||||
|
sum(usage_kwh) OVER (PARTITION BY date_trunc('month', period)
|
||||||
|
ORDER BY period ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS c1
|
||||||
|
FROM csg_history
|
||||||
|
WHERE kind='day' AND usage_kwh IS NOT NULL
|
||||||
|
), priced AS (
|
||||||
|
SELECT period,
|
||||||
|
round(public.csg_ladder_cost_raw(c1, period)
|
||||||
|
- public.csg_ladder_cost_raw(c1 - usage_kwh, period), 2) AS new_cost
|
||||||
|
FROM cum
|
||||||
|
)
|
||||||
|
UPDATE csg_history h
|
||||||
|
SET cost = p.new_cost, updated_at = now()
|
||||||
|
FROM priced p
|
||||||
|
WHERE h.kind='day' AND h.period = p.period
|
||||||
|
AND h.cost IS DISTINCT FROM p.new_cost;
|
||||||
|
|
||||||
|
-- ---------- 4. 断档自愈(只补缺失,不覆盖既有) ----------
|
||||||
|
CREATE OR REPLACE FUNCTION public.csg_backfill_missing_days(
|
||||||
|
job_id integer DEFAULT NULL,
|
||||||
|
config jsonb DEFAULT NULL
|
||||||
|
)
|
||||||
|
RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
lookback int := COALESCE((config->>'lookback_days')::int, 45);
|
||||||
|
dd date;
|
||||||
|
v_usage numeric;
|
||||||
|
c0 numeric; c1 numeric;
|
||||||
|
v_ladder text;
|
||||||
|
mm date;
|
||||||
|
BEGIN
|
||||||
|
-- 4a. 补缺失的 day 行(只 INSERT,绝不覆盖)
|
||||||
|
FOR dd IN
|
||||||
|
SELECT g::date
|
||||||
|
FROM generate_series(CURRENT_DATE - lookback, CURRENT_DATE - 1, INTERVAL '1 day') g
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM csg_history h WHERE h.kind='day' AND h.period = g::date)
|
||||||
|
ORDER BY 1
|
||||||
|
LOOP
|
||||||
|
-- 该数据日的观测:同一实体同时给出日期与数值
|
||||||
|
SELECT s.value INTO v_usage
|
||||||
|
FROM states_raw s JOIN entities e ON e.id = s.metadata_id
|
||||||
|
WHERE e.entity_id = 'sensor.0800041935246530_latest_day_kwh'
|
||||||
|
AND s.value IS NOT NULL
|
||||||
|
AND s.attributes->>'latest_day_date' = dd::text
|
||||||
|
ORDER BY s.time DESC LIMIT 1;
|
||||||
|
CONTINUE WHEN v_usage IS NULL; -- 上游没有这天的数据,不写空行
|
||||||
|
|
||||||
|
SELECT COALESCE(sum(h.usage_kwh), 0) INTO c0
|
||||||
|
FROM csg_history h
|
||||||
|
WHERE h.kind='day'
|
||||||
|
AND h.period >= date_trunc('month', dd)::date
|
||||||
|
AND h.period < dd;
|
||||||
|
c1 := c0 + v_usage;
|
||||||
|
v_ladder := CASE
|
||||||
|
WHEN c1 <= (CASE WHEN extract(month FROM dd) BETWEEN 5 AND 10 THEN 260 ELSE 200 END) THEN '一档'
|
||||||
|
WHEN c1 <= (CASE WHEN extract(month FROM dd) BETWEEN 5 AND 10 THEN 600 ELSE 400 END) THEN '二档'
|
||||||
|
ELSE '三档' END;
|
||||||
|
|
||||||
|
INSERT INTO csg_history (period, kind, usage_kwh, cost, ladder)
|
||||||
|
VALUES (dd, 'day', v_usage,
|
||||||
|
round(public.csg_ladder_cost_raw(c1, dd) - public.csg_ladder_cost_raw(c0, dd), 2),
|
||||||
|
v_ladder)
|
||||||
|
ON CONFLICT (period, kind) DO NOTHING;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
-- 4b. 受影响月份的 month 行重算(只升不降,与主函数同语义)
|
||||||
|
FOR mm IN
|
||||||
|
SELECT DISTINCT date_trunc('month', h.period)::date
|
||||||
|
FROM csg_history h
|
||||||
|
WHERE h.kind='day' AND h.period >= CURRENT_DATE - lookback
|
||||||
|
ORDER BY 1
|
||||||
|
LOOP
|
||||||
|
SELECT sum(h.usage_kwh) INTO c1
|
||||||
|
FROM csg_history h
|
||||||
|
WHERE h.kind='day' AND h.period >= mm AND h.period < (mm + interval '1 month');
|
||||||
|
SELECT h.usage_kwh INTO c0 FROM csg_history h WHERE h.kind='month' AND h.period = mm;
|
||||||
|
IF c1 IS NOT NULL AND c1 > 0 AND (c0 IS NULL OR c1 > c0) THEN
|
||||||
|
INSERT INTO csg_history (period, kind, usage_kwh, cost)
|
||||||
|
VALUES (mm, 'month', c1, public.csg_ladder_cost(c1, mm))
|
||||||
|
ON CONFLICT (period, kind) DO UPDATE SET
|
||||||
|
usage_kwh = EXCLUDED.usage_kwh,
|
||||||
|
cost = EXCLUDED.cost,
|
||||||
|
updated_at = now();
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
END $function$;
|
||||||
|
|
||||||
|
-- 重挂自愈任务:每天 15:10 UTC = 23:10 Asia/Shanghai(排在 14:30 UTC 的主任务之后)
|
||||||
|
DO $do$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM timescaledb_information.jobs WHERE proc_name = 'csg_backfill_missing_days') THEN
|
||||||
|
PERFORM add_job(proc => 'public.csg_backfill_missing_days'::regproc,
|
||||||
|
schedule_interval => INTERVAL '1 day',
|
||||||
|
initial_start => date_trunc('day', now() AT TIME ZONE 'UTC')
|
||||||
|
+ INTERVAL '1 day 15 hours 10 minutes',
|
||||||
|
timezone => 'UTC');
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$do$;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -60,7 +60,9 @@
|
|||||||
> [runbooks/matter-packet-capture.md](../runbooks/matter-packet-capture.md)。
|
> [runbooks/matter-packet-capture.md](../runbooks/matter-packet-capture.md)。
|
||||||
> 下面是最常用的两条。
|
> 下面是最常用的两条。
|
||||||
|
|
||||||
**视角必须在 LAN55**。**HA matter-server 作配对方时推荐直接在 hass `end0` 抓**——配对方
|
hass 上网卡名是 `end1`(2026-09-23)。2026-08 笔记里的 `end0` 已经没有了。
|
||||||
|
|
||||||
|
**视角必须在 LAN55**。**HA matter-server 作配对方时推荐直接在 hass `end1` 抓**——配对方
|
||||||
必然参与配对流程的每一条通讯(mDNS 本段组播 + 自己的 TCP 5540 全程),覆盖最全;AP `br0`
|
必然参与配对流程的每一条通讯(mDNS 本段组播 + 自己的 TCP 5540 全程),覆盖最全;AP `br0`
|
||||||
能看到全部 mDNS 组播 + 无线客户端单播,但**看不到有线↔有线单播**(如 Thread 设备经有线 M3
|
能看到全部 mDNS 组播 + 无线客户端单播,但**看不到有线↔有线单播**(如 Thread 设备经有线 M3
|
||||||
配对时 HA↔M3 的 5540 在 AP 侧不可见)。66 网段电脑看不到 55 的组播。BusyBox 注意点仅适用
|
配对时 HA↔M3 的 5540 在 AP 侧不可见)。66 网段电脑看不到 55 的组播。BusyBox 注意点仅适用
|
||||||
@@ -75,7 +77,7 @@ ssh zhiqiangf@192.168.55.5 "tcpdump -ni br0 -s 0 -vvv -tt 'udp port 5353 or tcp
|
|||||||
hass 侧(HA matter-server 作配对方,推荐;非交互 ssh 需显式 `sudo -n -i`):
|
hass 侧(HA matter-server 作配对方,推荐;非交互 ssh 需显式 `sudo -n -i`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh hassio@hass.windy.lan "sudo -n -i tcpdump -ni end0 -s 0 -vvv -tt 'udp port 5353 or tcp port 5540 or tcp port 5552'"
|
ssh hassio@hass.windy.lan "sudo -n -i tcpdump -ni end1 -s 0 -vvv -tt 'udp port 5353 or tcp port 5540 or tcp port 5552'"
|
||||||
```
|
```
|
||||||
|
|
||||||
精简过滤(只看 Matter 信号):
|
精简过滤(只看 Matter 信号):
|
||||||
@@ -193,7 +195,7 @@ ssh zhiqiangf@192.168.55.5 "tcpdump -ni br0 -s 0 -vvv -tt 'udp port 5353 or tcp
|
|||||||
| 新盏 `34:98:7a:27:10:bc`(`.148`,hostname `matter`) | DHCP 04:40 续租;gw ARP 完整;ping 通(93–122ms,ESP32 省电时延);08-22 16:40 起稳定关联 `wifi0ap1`,关联时 `avg_rssi -70`。mDNS 宣告 3 实例:`4DF2B1455D19402D-02EF079EEB480D07`(**新 node ID——08-22 之后被重新配网过**)、`2F6E56020E1996E7-137147AF27BE4EB6`、`DCE86145C137AF0E-0000000000000011`(HA fabric);host 记录 A `.148` + fe80 + **当前前缀** GUA `240e:3bd:238:4812:*`。支持单播 legacy mDNS 查询(`dig -p 5353 @.148 _matter._tcp.local PTR` 可用) |
|
| 新盏 `34:98:7a:27:10:bc`(`.148`,hostname `matter`) | DHCP 04:40 续租;gw ARP 完整;ping 通(93–122ms,ESP32 省电时延);08-22 16:40 起稳定关联 `wifi0ap1`,关联时 `avg_rssi -70`。mDNS 宣告 3 实例:`4DF2B1455D19402D-02EF079EEB480D07`(**新 node ID——08-22 之后被重新配网过**)、`2F6E56020E1996E7-137147AF27BE4EB6`、`DCE86145C137AF0E-0000000000000011`(HA fabric);host 记录 A `.148` + fe80 + **当前前缀** GUA `240e:3bd:238:4812:*`。支持单播 legacy mDNS 查询(`dig -p 5353 @.148 _matter._tcp.local PTR` 可用) |
|
||||||
| 工作盏(MAC 已变)`fc:e8:c0:25:a1:f0`(`.146`,hostname `espressif`) | DHCP 07:17 续租;ping 通 v4/v6(v6 fe80 38–61ms)。mDNS 宣告 3 实例:`4DF2B1455D19402D-02EF4CA3F856B615`、`2F6E56020E1996E7-EE8F2E4F1A77BF05`、`DCE86145C137AF0E-000000000000000B`。原 MAC `34:98:7a:25:a1:f0` 全网消失(无租约/ARP/AP 日志)而新 MAC 末 3 字节相同 → 疑固件更新后改 MAC。**拒绝单播 5353**(ICMP port unreachable),只应答组播查询——同族固件行为差异。hass 残留其旧前缀 GUA `240e:3bd:235:1fb2:fee8:c0ff:fe25:a1f0` 的 **FAILED** 邻居项 |
|
| 工作盏(MAC 已变)`fc:e8:c0:25:a1:f0`(`.146`,hostname `espressif`) | DHCP 07:17 续租;ping 通 v4/v6(v6 fe80 38–61ms)。mDNS 宣告 3 实例:`4DF2B1455D19402D-02EF4CA3F856B615`、`2F6E56020E1996E7-EE8F2E4F1A77BF05`、`DCE86145C137AF0E-000000000000000B`。原 MAC `34:98:7a:25:a1:f0` 全网消失(无租约/ARP/AP 日志)而新 MAC 末 3 字节相同 → 疑固件更新后改 MAC。**拒绝单播 5353**(ICMP port unreachable),只应答组播查询——同族固件行为差异。hass 残留其旧前缀 GUA `240e:3bd:235:1fb2:fee8:c0ff:fe25:a1f0` 的 **FAILED** 邻居项 |
|
||||||
| 故障盏 `34:98:7a:27:7f:08`(曾 `.145`) | 无租约、ARP incomplete、AP 日志零事件 = 已离网 |
|
| 故障盏 `34:98:7a:27:7f:08`(曾 `.145`) | 无租约、ARP incomplete、AP 日志零事件 = 已离网 |
|
||||||
| **TCP 5540 探测(两盏)** | IPv4(LAN66 与 hass 本段)、IPv6(fe80%end0 + 当前 GUA)全部 **RST(Connection refused)** —— SRV 宣告 :5540 且 TXT `T=1`,但实际无监听 |
|
| **TCP 5540 探测(两盏)** | IPv4(LAN66 与 hass 本段)、IPv6(fe80%end1 + 当前 GUA)全部 **RST(Connection refused)** —— SRV 宣告 :5540 且 TXT `T=1`,但实际无监听 |
|
||||||
| hass matter-server | `started`,v9.0.4,无更新;宣告自身运营实例 `DCE86145C137AF0E-…1B669`(v4+v6,当前 GUA);**无任何 established :5540 会话**;core/add-on 日志无 matter 错误 |
|
| hass matter-server | `started`,v9.0.4,无更新;宣告自身运营实例 `DCE86145C137AF0E-…1B669`(v4+v6,当前 GUA);**无任何 established :5540 会话**;core/add-on 日志无 matter 错误 |
|
||||||
| 其他 Matter 控制器 | Aqara M3 `.248` 在线(有线 0.8ms),宣告含自身 fabric 节点 `4DF2B1455D19402D-11E158E46D24A000`;SmartThings `.48` 在线并周期查询 `_matter._tcp.local`;手机(当前前缀 GUA)也在浏览。LAN55 共见 **5 个 fabric**:`4DF2B1455D19402D`(M3)、`DCE86145C137AF0E`(HA)、`2F6E56020E1996E7`、`03BCFAEDD6153944`、`6A6FF80C2DB84DEE` |
|
| 其他 Matter 控制器 | Aqara M3 `.248` 在线(有线 0.8ms),宣告含自身 fabric 节点 `4DF2B1455D19402D-11E158E46D24A000`;SmartThings `.48` 在线并周期查询 `_matter._tcp.local`;手机(当前前缀 GUA)也在浏览。LAN55 共见 **5 个 fabric**:`4DF2B1455D19402D`(M3)、`DCE86145C137AF0E`(HA)、`2F6E56020E1996E7`、`03BCFAEDD6153944`、`6A6FF80C2DB84DEE` |
|
||||||
|
|
||||||
@@ -207,9 +209,9 @@ TCP 5540 恢复监听即可确认。
|
|||||||
|
|
||||||
- gw:`show dhcp leases` / `show arp`(经 `/opt/vyatta/bin/vyatta-op-cmd-wrapper`)。
|
- gw:`show dhcp leases` / `show arp`(经 `/opt/vyatta/bin/vyatta-op-cmd-wrapper`)。
|
||||||
- AP:`grep -i <mac> /var/log/messages`(hostapd 关联事件 + stahtd RSSI/soft failure)。
|
- AP:`grep -i <mac> /var/log/messages`(hostapd 关联事件 + stahtd RSSI/soft failure)。
|
||||||
- hass:`sudo -n -i ha apps info core_matter_server`;`ip -6 neigh show dev end0`
|
- hass:`sudo -n -i ha apps info core_matter_server`;`ip -6 neigh show dev end1`
|
||||||
(看灯泡 fe80/旧新前缀 GUA 与 FAILED 项);被动抓包
|
(看灯泡 fe80/旧新前缀 GUA 与 FAILED 项);被动抓包
|
||||||
`sudo -n -i timeout 65 tcpdump -ni end0 -s 0 -tt 'udp port 5353'`——配对方周期查询
|
`sudo -n -i timeout 65 tcpdump -ni end1 -s 0 -tt 'udp port 5353'`——配对方周期查询
|
||||||
会自然引出灯泡宣告,无需主动发包。
|
会自然引出灯泡宣告,无需主动发包。
|
||||||
- 5540 探测:hass 上 python3 对 v4 / fe80%end0 / GUA 各 connect 一次;RST=无监听,
|
- 5540 探测:hass 上 python3 对 v4 / fe80%end1 / GUA 各 connect 一次;RST=无监听,
|
||||||
超时=不可达(两者含义不同)。
|
超时=不可达(两者含义不同)。
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Plane CE 加固草稿(docs/plane-hardening/)
|
||||||
|
|
||||||
|
> **状态:草稿,未应用、未提交。** 对应追踪:Plane vps 项目条目(2026-09-03,**记录源**;Linear W1N-277 已取消,Linear 自 2026-09-03 起不再作为记录源)。
|
||||||
|
> 线上实例:`plane.chans.xyz`(synapse K3s,ns `plane`,release `plane-app` = chart `plane-ce-1.8.0` / app `v1.4.1`)。
|
||||||
|
> 依据:2026-09-03 只读核查(13 条审查意见中 11 条属实、#3 基本属实、#9 指标归属错误)+ 上游 chart 模板逐条核对。
|
||||||
|
|
||||||
|
## 文件
|
||||||
|
|
||||||
|
| 文件 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| `values.hardened.yaml` | 可选硬化 values(external secrets 引用、requireExplicitSecrets、minio pin、上传限额对齐);含 HTTP→HTTPS `extraObjects` 示例 |
|
||||||
|
| `secrets.yaml.example` | 6 组外部 Secret 结构占位(只含 key 名,真实值仅存宿主机) |
|
||||||
|
| `backup/plane-backup.yaml` | **PostgreSQL 备份 CronJob**(pg_dump `-Fc`,hostPath `/var/backups/plane`;MinIO 已按实际用量剔除) |
|
||||||
|
| `backup/README.md` | 备份方案说明(排程/容量/保留/还原/阻塞) |
|
||||||
|
|
||||||
|
## 应用顺序(每步先 diff 后执行,全部需用户逐项确认)
|
||||||
|
|
||||||
|
### 现在就值得做:DB 备份(P0,见 backup/)
|
||||||
|
`plane-backup.yaml` 部署 + 手动触发验证一次即可;88 MB 库每日快照几乎零成本。
|
||||||
|
|
||||||
|
### 可选(顺手做一次,不是必须)
|
||||||
|
- **Phase A 密钥外部化**(零行为变化、无停机,约 15 分钟):按 `secrets.yaml.example`
|
||||||
|
在宿主机建 6 个 Secret(值先复制当前集群),用 `values.hardened.yaml` 跑
|
||||||
|
`helm diff upgrade` → `helm upgrade`;验证后删除 chart 生成的旧 Secret。
|
||||||
|
价值:默认密钥不再落在 chart 公开常量上,作为保险。
|
||||||
|
- **MCP API Key 轮换**:若审查对话出过你的环境,Plane 后台重生成 + 更新
|
||||||
|
`/home/windy/plane-k3s/mcp/mcp.env`(0600)+ 重启 Cursor MCP。
|
||||||
|
- **/god-mode IP 白名单**:若在意管理后台被公网爆破。chart 1.8.0 的 IngressRoute
|
||||||
|
不支持给单条路由追加 middleware → 需 post-renderer 或 upgrade 后 `kubectl patch`
|
||||||
|
(升级会覆盖,需固化);源 IP 清单待提供。
|
||||||
|
|
||||||
|
### 明确暂缓/跳过(个人单节点,等出现症状再处理)
|
||||||
|
- SECRET_KEY 等轮换(Phase B):等真要配 SMTP/OAuth 前再做(避免旧密文不可解)。
|
||||||
|
- NetworkPolicy、有状态组件 resources limits(chart 无 values 开关,需 post-render/patch)、
|
||||||
|
HTTP→HTTPS(草稿已给 `extraObjects` 示例)、metrics-server/Sentry。
|
||||||
|
|
||||||
|
## 关键限制(chart 1.8.0 模板已核对)
|
||||||
|
- `external_secrets.*_existingSecret` 设置后,对应 Secret **必须**包含模板所需全部 key
|
||||||
|
(缺失不自动补),见 `secrets.yaml.example` 注释。
|
||||||
|
- `app_keys_existingSecret` 的 envFrom 在所有 workload 上**最后注入**(后置生效),
|
||||||
|
保证 app/live 共享密钥一致——不要在其后再放同名 key 的 Secret。
|
||||||
|
- `DATABASE_URL`/`AMQP_URL`/`REDIS_URL` 是 chart 生成的派生 URL,内嵌明文密码;
|
||||||
|
外部化后轮换 DB/队列密码时必须同步更新 `plane-app-env`。
|
||||||
|
- minio 的 `MINIO_ROOT_*` 与 `AWS_*` 同源于一个 Secret;升级时 bucket Job 会重跑
|
||||||
|
(需 admin 权限凭据)——换 svcacct 前先确认权限覆盖该 Job。
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Plane CE 备份方案(DB-only)— DRAFT (2026-09-03), 未应用
|
||||||
|
|
||||||
|
> 关联:`plane-backup.yaml`(CronJob);追踪:Plane vps 项目条目(记录源,2026-09-03 起不用 Linear)。
|
||||||
|
> 现状(实测):pg 全库 **88 MB**(310 issues / 1 user);MinIO uploads **264 KB**(几乎空)。
|
||||||
|
|
||||||
|
## 范围决策(2026-09-03,实际角度)
|
||||||
|
|
||||||
|
- **做:PostgreSQL 逻辑备份** —— 覆盖现实故障(误删、升级失败、磁盘坏、重装),成本≈0。
|
||||||
|
- **不做:MinIO/附件备份** —— 桶仅 264 KB,个人实例附件可接受丢失;不为它付日常维护。
|
||||||
|
日后附件明显变多再按原完整版思路加 `mc mirror`(历史版本见本目录 git 历史/Plane 条目评论)。
|
||||||
|
- 异机同步暂不启用(见下"局限/阻塞")。
|
||||||
|
|
||||||
|
## 方案
|
||||||
|
|
||||||
|
集群内 CronJob(ns `plane`,每天 **01:30 UTC = 03:30 本地**,控制器按 UTC 跑):
|
||||||
|
|
||||||
|
1. 单容器 `postgres:15.7-alpine`:`pg_dump -Fc`(自定义压缩格式)打 `plane` 库
|
||||||
|
→ `/var/backups/plane/pg/plane-<UTC时间戳>.dump`(hostPath `DirectoryOrCreate`)
|
||||||
|
2. 保留 7 天(`find -mtime +7 -delete`),成功/失败历史各留 3/2
|
||||||
|
3. 凭据:现 chart Secret `plane-app-pgdb-secrets`(Phase A 外部化后改 `plane-pgdb-credentials`)
|
||||||
|
|
||||||
|
## 容量
|
||||||
|
|
||||||
|
- 库 88 MB → `-Fc` 快照约 10–40 MB/天 × 7 天 ≈ **<300 MB**,对 83 G 可用盘可忽略。
|
||||||
|
|
||||||
|
## 还原(未演练;应用前先做一次隔离测试)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 目标 PG15 实例(临时起一个 postgres:15.7-alpine 容器或另一台机):
|
||||||
|
# 先建空库: createdb plane (user=plane)
|
||||||
|
pg_restore -h <target> -U plane -d plane --clean --if-exists /var/backups/plane/pg/plane-<TS>.dump
|
||||||
|
# 还原后确认 310 issues 量级一致;附件为空属预期(未备份 MinIO)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 验收(应用前逐项过)
|
||||||
|
|
||||||
|
- [ ] CronJob 建立后手动触发一次:`kubectl -n plane create job --from=cronjob/plane-backup plane-backup-manual-1`,Job `Completed`
|
||||||
|
- [ ] `/var/backups/plane/pg/plane-*.dump` 可被 `pg_restore -l` 列出
|
||||||
|
- [ ] 备份 Job 只依赖 pgdb 服务,不依赖 Plane 应用 Pod(应用故障期间也能出备份)
|
||||||
|
- [ ] 保留清理 dry-run(`find ... -print`)正确;`df -h /` 前后对比记录
|
||||||
|
|
||||||
|
## 局限 / 阻塞
|
||||||
|
|
||||||
|
- **本地方案不是离机备份**:单节点磁盘/整机故障即丢。如日后要离机,纳入
|
||||||
|
[Restic 异机 repository 决策与存取隔离](https://plane.chans.xyz/space/projects/56874283-7e1d-43a8-afa4-631cf1c4ad5b/issues/7825d564-ae15-446b-bced-be26b648346b/)
|
||||||
|
(与 Matrix 备份同一决策);恢复演练纪律见
|
||||||
|
[服务级 restore runbook 与隔离复元演练](https://plane.chans.xyz/space/projects/56874283-7e1d-43a8-afa4-631cf1c4ad5b/issues/a9bea3ba-c958-4a74-b2f1-6bbb653f21d3/)。
|
||||||
|
- 提醒:同一节点 **Matrix 数据价值远高于 Plane 且同样无备份** —— 若投入备份精力,顺序上 Matrix 优先。
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Plane CE PostgreSQL backup CronJob — DRAFT (2026-09-03), NOT applied.
|
||||||
|
# ns: plane (synapse K3s single node). Output: hostPath /var/backups/plane (root disk, auto-created).
|
||||||
|
#
|
||||||
|
# Scope decision (2026-09-03, practical): DB-only. MinIO dropped — uploads bucket
|
||||||
|
# measured at 264 KB / 444 KB total; attachments are acceptable loss for this
|
||||||
|
# personal 1-user instance (310 issues / 88 MB DB). Revisit only if usage grows.
|
||||||
|
#
|
||||||
|
# Credentials: read from the CURRENT chart-generated Secret (works today). After the
|
||||||
|
# optional external-secrets migration (docs/plane-hardening/README.md Phase A) switch
|
||||||
|
# the secretKeyRef name to plane-pgdb-credentials.
|
||||||
|
#
|
||||||
|
# Apply:
|
||||||
|
# ssh windy@synapse.chans.xyz 'sudo k3s kubectl apply -n plane -f -' < plane-backup.yaml
|
||||||
|
# Manual run + verify:
|
||||||
|
# sudo k3s kubectl -n plane create job --from=cronjob/plane-backup plane-backup-manual-1
|
||||||
|
# sudo k3s kubectl -n plane get cronjob,job,pods | grep plane-backup
|
||||||
|
# sudo ls -lh /var/backups/plane/pg
|
||||||
|
# Restore steps + tuning: see backup/README.md
|
||||||
|
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: CronJob
|
||||||
|
metadata:
|
||||||
|
name: plane-backup
|
||||||
|
namespace: plane
|
||||||
|
spec:
|
||||||
|
# 01:30 UTC daily = 03:30 local (CEST). CronJob controller runs in UTC.
|
||||||
|
schedule: "30 1 * * *"
|
||||||
|
concurrencyPolicy: Forbid
|
||||||
|
successfulJobsHistoryLimit: 3
|
||||||
|
failedJobsHistoryLimit: 2
|
||||||
|
jobTemplate:
|
||||||
|
spec:
|
||||||
|
backoffLimit: 2
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
restartPolicy: OnFailure
|
||||||
|
volumes:
|
||||||
|
- name: backup
|
||||||
|
hostPath:
|
||||||
|
path: /var/backups/plane
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
containers:
|
||||||
|
- name: pg-dump
|
||||||
|
image: postgres:15.7-alpine
|
||||||
|
env:
|
||||||
|
- name: PGPASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: plane-app-pgdb-secrets # -> plane-pgdb-credentials after Phase A
|
||||||
|
key: POSTGRES_PASSWORD
|
||||||
|
command: ["/bin/sh", "-c"]
|
||||||
|
args:
|
||||||
|
- |
|
||||||
|
set -euo pipefail
|
||||||
|
TS=$(date -u +%Y%m%dT%H%M%SZ)
|
||||||
|
mkdir -p /backup/pg
|
||||||
|
pg_dump -h plane-app-pgdb.plane.svc.cluster.local -U plane -d plane \
|
||||||
|
-Fc -f "/backup/pg/plane-${TS}.dump"
|
||||||
|
find /backup/pg -type f -name 'plane-*.dump' -mtime +7 -delete
|
||||||
|
echo "pg_dump done: /backup/pg/plane-${TS}.dump ($(du -h /backup/pg/plane-${TS}.dump | cut -f1))"
|
||||||
|
volumeMounts:
|
||||||
|
- name: backup
|
||||||
|
mountPath: /backup
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# External Secret structure for Plane CE hardening — EXAMPLE ONLY.
|
||||||
|
# No real values here; this file is safe to commit. Real values live only on the
|
||||||
|
# host (/home/windy/plane-k3s, 0600/0700) and in the cluster.
|
||||||
|
#
|
||||||
|
# Phase A — create each Secret with the CURRENT cluster values first (zero change):
|
||||||
|
# # current source Secrets (chart-generated):
|
||||||
|
# kubectl -n plane get secret plane-app-app-secrets -o jsonpath='{.data.SECRET_KEY}' | base64 -d
|
||||||
|
# kubectl -n plane get secret plane-app-live-secrets -o jsonpath='{.data.REDIS_URL}' | base64 -d
|
||||||
|
# kubectl -n plane get secret plane-app-pgdb-secrets -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d
|
||||||
|
# kubectl -n plane get secret plane-app-rabbitmq-secrets -o jsonpath='{.data.RABBITMQ_DEFAULT_PASS}' | base64 -d
|
||||||
|
# kubectl -n plane get secret plane-app-doc-store-secrets -o jsonpath='{.data}' | base64 -d
|
||||||
|
#
|
||||||
|
# e.g. kubectl -n plane create secret generic plane-app-keys \
|
||||||
|
# --from-literal=SECRET_KEY="$(<copy from above>)" \
|
||||||
|
# --from-literal=LIVE_SERVER_SECRET_KEY="$(<copy from above>)"
|
||||||
|
#
|
||||||
|
# All keys below are REQUIRED by chart templates/plane-ce-1.8.0 (verified 2026-09-03):
|
||||||
|
# missing keys are NOT auto-filled once an existingSecret is referenced.
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: plane-app-keys # external_secrets.app_keys_existingSecret
|
||||||
|
namespace: plane
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
SECRET_KEY: "" # current: copy from plane-app-app-secrets; rotate only in Phase B
|
||||||
|
LIVE_SERVER_SECRET_KEY: "" # current: same value as above / plane-app-live-secrets
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: plane-app-env # external_secrets.app_env_existingSecret
|
||||||
|
namespace: plane
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
REDIS_URL: "" # redis://plane-app-redis.plane.svc.cluster.local:6379/
|
||||||
|
DATABASE_URL: "" # postgresql://plane:plane@plane-app-pgdb.plane.svc.cluster.local/plane
|
||||||
|
AMQP_URL: "" # amqp://plane:plane@plane-app-rabbitmq.plane.svc.cluster.local/
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: plane-live-env # external_secrets.live_env_existingSecret
|
||||||
|
namespace: plane
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
REDIS_URL: "" # redis://plane-app-redis.plane.svc.cluster.local:6379/
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: plane-pgdb-credentials # external_secrets.pgdb_existingSecret
|
||||||
|
namespace: plane
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
POSTGRES_PASSWORD: "" # Phase A: keep current ('plane'); Phase B: ALTER USER first, then sync
|
||||||
|
POSTGRES_DB: "plane"
|
||||||
|
POSTGRES_USER: "plane"
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: plane-rabbitmq-credentials # external_secrets.rabbitmq_existingSecret
|
||||||
|
namespace: plane
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
RABBITMQ_DEFAULT_USER: "plane"
|
||||||
|
RABBITMQ_DEFAULT_PASS: "" # Phase A: keep current; Phase B: rabbitmqctl change_password first
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: plane-minio-credentials # external_secrets.doc_store_existingSecret
|
||||||
|
namespace: plane
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
FILE_SIZE_LIMIT: "20971520" # must match env.doc_upload_size_limit
|
||||||
|
AWS_S3_BUCKET_NAME: "uploads"
|
||||||
|
USE_MINIO: "1"
|
||||||
|
MINIO_ROOT_USER: "admin"
|
||||||
|
MINIO_ROOT_PASSWORD: "" # root creds take effect on first init only
|
||||||
|
AWS_ACCESS_KEY_ID: "admin"
|
||||||
|
AWS_SECRET_ACCESS_KEY: "" # == MINIO_ROOT_PASSWORD while minio.local_setup
|
||||||
|
AWS_S3_ENDPOINT_URL: "http://plane-app-minio:9000"
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# Plane CE hardened values — DRAFT (2026-09-03), NOT applied.
|
||||||
|
# Target file on host: /home/windy/plane-k3s/values.yaml (synapse.chans.xyz)
|
||||||
|
# Reference release: plane-app, chart plane-ce-1.8.0 (values.yaml L1-362 + templates verified 2026-09-03).
|
||||||
|
# No secrets in this file. Secret *values* live only in k8s Secrets (see secrets.yaml.example).
|
||||||
|
#
|
||||||
|
# Two phases:
|
||||||
|
# Phase A: externalize secrets (reference names below) with CURRENT values copied -> zero change.
|
||||||
|
# Phase B: rotate credentials one by one (see README.md). SECRET_KEY rotation is cheap only while
|
||||||
|
# SMTP/OAuth are unconfigured (no encrypted config rows yet).
|
||||||
|
|
||||||
|
planeVersion: v1.4.1
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
appHost: plane.chans.xyz
|
||||||
|
ingressClass: traefik
|
||||||
|
traefik:
|
||||||
|
# 20 MiB (chart default). Keep aligned with env.doc_upload_size_limit below.
|
||||||
|
maxRequestBodyBytes: 20971520
|
||||||
|
|
||||||
|
ssl:
|
||||||
|
createIssuer: true
|
||||||
|
issuer: http # HTTP-01; ssl_token_existingSecret not needed
|
||||||
|
email: admin@chans.xyz
|
||||||
|
generateCerts: true
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
storageClass: local-path
|
||||||
|
volumeSize: 5Gi
|
||||||
|
# NOTE: chart 1.8.0 exposes NO resources knob for the bundled datastores
|
||||||
|
# (stateful templates render no resources block). Add limits via
|
||||||
|
# --post-renderer/kustomize or `kubectl -n plane patch sts ...` re-applied on
|
||||||
|
# every upgrade (P2 task; see README.md).
|
||||||
|
|
||||||
|
redis:
|
||||||
|
storageClass: local-path
|
||||||
|
# image: valkey/valkey:7.2.11-alpine # already pinned by chart default; uncomment to make explicit
|
||||||
|
|
||||||
|
minio:
|
||||||
|
# P2: pin. Digest of the currently running :latest (2026-09-03, pod plane-app-minio-wl-0).
|
||||||
|
image: minio/minio@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e
|
||||||
|
# image_mc: minio/mc@sha256:... # optional: pin one-shot bucket-init client the same way
|
||||||
|
storageClass: local-path
|
||||||
|
volumeSize: 5Gi
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
storageClass: local-path
|
||||||
|
|
||||||
|
env:
|
||||||
|
# Fail the render instead of ever falling back to the chart's PUBLIC constants
|
||||||
|
# (values.yaml L340-341 in chart 1.8.0). Requires external_secrets below.
|
||||||
|
requireExplicitSecrets: true
|
||||||
|
|
||||||
|
# SECRET_KEY / LIVE_SERVER_SECRET_KEY are deliberately OMITTED here.
|
||||||
|
# They live in k8s Secret `plane-app-keys` (referenced below). With
|
||||||
|
# requireExplicitSecrets=true and app_keys_existingSecret set, the chart renders
|
||||||
|
# neither key itself and app+live workloads both envFrom `plane-app-keys` LAST
|
||||||
|
# (later envFrom wins), which keeps the shared signing key consistent.
|
||||||
|
|
||||||
|
pgdb_name: plane
|
||||||
|
docstore_bucket: uploads
|
||||||
|
# Align app-side upload cap with the Traefik body limit (was 5242880/5MiB).
|
||||||
|
# Keep both at 20MiB, or lower both together.
|
||||||
|
doc_upload_size_limit: "20971520"
|
||||||
|
|
||||||
|
external_secrets:
|
||||||
|
# Shared signing keys (used by app + live). REQUIRED keys: SECRET_KEY, LIVE_SERVER_SECRET_KEY.
|
||||||
|
app_keys_existingSecret: plane-app-keys
|
||||||
|
# REQUIRED keys: REDIS_URL, DATABASE_URL, AMQP_URL (chart-derived URLs; update on DB/queue rotation).
|
||||||
|
app_env_existingSecret: plane-app-env
|
||||||
|
# REQUIRED keys: REDIS_URL.
|
||||||
|
live_env_existingSecret: plane-live-env
|
||||||
|
# REQUIRED keys: POSTGRES_PASSWORD, POSTGRES_DB, POSTGRES_USER.
|
||||||
|
pgdb_existingSecret: plane-pgdb-credentials
|
||||||
|
# REQUIRED keys: RABBITMQ_DEFAULT_USER, RABBITMQ_DEFAULT_PASS.
|
||||||
|
rabbitmq_existingSecret: plane-rabbitmq-credentials
|
||||||
|
# REQUIRED keys: FILE_SIZE_LIMIT, AWS_S3_BUCKET_NAME, USE_MINIO, MINIO_ROOT_USER,
|
||||||
|
# MINIO_ROOT_PASSWORD, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_S3_ENDPOINT_URL.
|
||||||
|
doc_store_existingSecret: plane-minio-credentials
|
||||||
|
# ssl_token_existingSecret: '' # DNS-01 only (cloudflare/digitalocean); unused with HTTP-01
|
||||||
|
|
||||||
|
# Optional, P2: HTTP -> HTTPS 301. The chart's own IngressRoute binds only
|
||||||
|
# 'websecure' (http:// currently 404s). extraObjects is rendered verbatim (toYaml).
|
||||||
|
# Uncomment and `helm upgrade` once reviewed:
|
||||||
|
# extraObjects:
|
||||||
|
# - apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: Middleware
|
||||||
|
# metadata:
|
||||||
|
# name: plane-https-redirect
|
||||||
|
# namespace: plane
|
||||||
|
# spec:
|
||||||
|
# redirectScheme:
|
||||||
|
# scheme: https
|
||||||
|
# permanent: true
|
||||||
|
# - apiVersion: traefik.io/v1alpha1
|
||||||
|
# kind: IngressRoute
|
||||||
|
# metadata:
|
||||||
|
# name: plane-http-to-https
|
||||||
|
# namespace: plane
|
||||||
|
# spec:
|
||||||
|
# entryPoints: [web]
|
||||||
|
# routes:
|
||||||
|
# - match: Host(`plane.chans.xyz`)
|
||||||
|
# kind: Rule
|
||||||
|
# middlewares:
|
||||||
|
# - name: plane-https-redirect
|
||||||
|
# services:
|
||||||
|
# - name: plane-app-web
|
||||||
|
# port: 3000
|
||||||
@@ -60,6 +60,14 @@ OpenClash runs `/etc/openclash/clash` (clash_meta core) with configuration
|
|||||||
> AAAA now `2404:6800…` (new upstream answering, was `2607:f8b0…` via hk2),
|
> AAAA now `2404:6800…` (new upstream answering, was `2607:f8b0…` via hk2),
|
||||||
> taobao/intercept/clash-fake-ip all unchanged. Backup:
|
> taobao/intercept/clash-fake-ip all unchanged. Backup:
|
||||||
> `config.yaml.bak-multi-doh-20260813-105421`.
|
> `config.yaml.bak-multi-doh-20260813-105421`.
|
||||||
|
> 2026-09-01: removed `dns.quad9.net` from `foreign_upstream` — recurring
|
||||||
|
> `WARN foreign_upstream … unexpected EOF` bursts (481 log entries) against
|
||||||
|
> Quad9 DoH; endpoint answers on probe but gets intermittently
|
||||||
|
> connection-reset from this network (same failure class as the excluded
|
||||||
|
> `dns.quad101.net`). Remaining upstreams `adg.chans.xyz` (hk2) +
|
||||||
|
> `dns.cloudflare.com` both verified live; google.com A + youtube.com AAAA
|
||||||
|
> resolve through mosdns :6052 after restart. Backup:
|
||||||
|
> `config.yaml.bak-quad9-remove-20260901-201801`.
|
||||||
- nft: OpenClash injects TPROXY/redirect + DNS-hijack rules into
|
- nft: OpenClash injects TPROXY/redirect + DNS-hijack rules into
|
||||||
`table inet fw4`; a residual `table inet passwall` exists with 0 packets (unused)
|
`table inet fw4`; a residual `table inet passwall` exists with 0 packets (unused)
|
||||||
|
|
||||||
|
|||||||
+312
-64
@@ -11,6 +11,7 @@
|
|||||||
| SSH | `ssh hassio@hass.windy.lan` |
|
| SSH | `ssh hassio@hass.windy.lan` |
|
||||||
| **Host** | **x88 Pro physical box** (HAOS bare-metal, `machine: green`; verified 2026-08-18) |
|
| **Host** | **x88 Pro physical box** (HAOS bare-metal, `machine: green`; verified 2026-08-18) |
|
||||||
| Platform | Home Assistant OS; kernel `6.1.115-haos` (aarch64) |
|
| Platform | Home Assistant OS; kernel `6.1.115-haos` (aarch64) |
|
||||||
|
| Core | 2026.9.3; Supervisor 2026.09.2; HAOS 18.2 (verified 2026-09-23) |
|
||||||
| Web UI | `http://hass.windy.lan:8123` (LAN); WAN port-forward `hass` on gw → `:8123` |
|
| Web UI | `http://hass.windy.lan:8123` (LAN); WAN port-forward `hass` on gw → `:8123` |
|
||||||
|
|
||||||
Use `hassio` for routine SSH inspection. Key-only login was verified on
|
Use `hassio` for routine SSH inspection. Key-only login was verified on
|
||||||
@@ -40,7 +41,7 @@ recovery codes in this repository.
|
|||||||
|
|
||||||
| Interface | Address / role |
|
| Interface | Address / role |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `end0` | IPv4 static `192.168.55.11/24` (gw `.254`, DNS `192.168.66.36`); IPv6 SLAAC `auto` with GUA on the current PD-derived /64 (`240e:3bd:235:1fb2:*` at 2026-08-22; rotates on PPPoE redial); primary LAN55 NIC (interface name verified live 2026-08-22 — `end1` does not exist) |
|
| `end1` | IPv4 static `192.168.55.11/24` (gw `.254`, DNS `192.168.66.36`); IPv6 SLAAC `auto` (prefixes rotate on PPPoE redial; old GUAs can linger). Primary LAN55 NIC. Live name on 2026-09-23 is `end1`; the 2026-08-22 `end0` name is gone |
|
||||||
| `wlan0` | Supervisor **disabled** (verified 2026-08-14, W1N-104); IPv6 remains off on this RTL8821CS radio |
|
| `wlan0` | Supervisor **disabled** (verified 2026-08-14, W1N-104); IPv6 remains off on this RTL8821CS radio |
|
||||||
| `wg0` | `10.13.13.2/32`; WireGuard (add-on / integration tunnel) |
|
| `wg0` | `10.13.13.2/32`; WireGuard (add-on / integration tunnel) |
|
||||||
| `hassio` / `docker0` | internal HAOS Docker bridges (`172.30.32.0/23`, `172.30.232.0/23`) |
|
| `hassio` / `docker0` | internal HAOS Docker bridges (`172.30.32.0/23`, `172.30.232.0/23`) |
|
||||||
@@ -99,7 +100,7 @@ See `~/.config/zsh/env/local/environment.env` for the client-side setting.
|
|||||||
## Safe verification
|
## Safe verification
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh -o BatchMode=yes hassio@hass.windy.lan 'hostname; ip -4 addr show end0'
|
ssh -o BatchMode=yes hassio@hass.windy.lan 'hostname; ip -4 addr show end1'
|
||||||
```
|
```
|
||||||
|
|
||||||
From a LAN client, confirm DNS and UI reachability:
|
From a LAN client, confirm DNS and UI reachability:
|
||||||
@@ -262,8 +263,7 @@ no template errors; 14 csg_* entities total.
|
|||||||
|
|
||||||
**电力监控面板(`lovelace.dashboard_unknown` / view `power-monitor`)
|
**电力监控面板(`lovelace.dashboard_unknown` / view `power-monitor`)
|
||||||
updated 2026-08-29 (W1N-240 + W1N-242):** 「本月累计」gauge 对齐夏季阶梯:
|
updated 2026-08-29 (W1N-240 + W1N-242):** 「本月累计」gauge 对齐夏季阶梯:
|
||||||
`max:650`、segments `0/260/600`(绿/橙/红 = 一/二/三档;冬季 11-01 需切
|
`max:650`、segments `0/260/600`(绿/橙/红 = 一/二/三档)。冬季档不要再手改,由下方 v4 的 `binary_sensor.csg_summer_tariff` 切换。「📊 统计
|
||||||
`max:450`、`0/200/400` — **seasonal switch point**,见下文)。「📊 统计
|
|
||||||
数据」卡新增本年/去年 4 行(原生传感器,口径标注「电费(账单)」、本年
|
数据」卡新增本年/去年 4 行(原生传感器,口径标注「电费(账单)」、本年
|
||||||
「(至今)」)+ 本月日均/预测进度 2 行(`csg_this_month_daily_avg` /
|
「(至今)」)+ 本月日均/预测进度 2 行(`csg_this_month_daily_avg` /
|
||||||
`csg_prediction_progress`,W1N-242);面板共引用 **20** 个实体。改前备份:
|
`csg_prediction_progress`,W1N-242);面板共引用 **20** 个实体。改前备份:
|
||||||
@@ -279,22 +279,149 @@ updated 2026-08-29 (W1N-240 + W1N-242):** 「本月累计」gauge 对齐夏季
|
|||||||
数据日)。备份 `csg_sensors.yaml.bak-20260829-w1n242`。验证:Core
|
数据日)。备份 `csg_sensors.yaml.bak-20260829-w1n242`。验证:Core
|
||||||
restart 后回归值不变(334.81 / -0.3 / 10.8 / 90.3 / 200.94 / 180.28)。
|
restart 后回归值不变(334.81 / -0.3 / 10.8 / 90.3 / 200.94 / 180.28)。
|
||||||
|
|
||||||
**CSG 长期归档(W1N-243, 2026-08-29):** scribe 库新增 `csg_history`
|
**CSG 面板重构 2026-09-04(VPS-90;面板布局已被下方 2026-09-22 v4 取代。仍有效的是均价传感器和两条季节提醒):** 核对
|
||||||
表(逐日 usage/cost/ladder/balance + 逐月累计;2026-07-01 起回填,永久),
|
`power-monitor` 计价与 8 月账单一致(198.65 vs 账单 198.64,差 ≤0.01 元,
|
||||||
由 TimescaleDB 每日任务 **1008** `csg_daily_snapshot()`(22:30
|
因模板用公众圆整价 0.589/0.639/0.889、账单用 6 位精确价),不改阶梯常量。
|
||||||
Asia/Shanghai;**TS job 非 pg_cron**)upsert 维护。日费用在原生
|
改动:① `csg_sensors.yaml` Block B 新增
|
||||||
`latest_day_cost` 缺失时回退 = 昨日用电 × 当前档费率(模板
|
`sensor.csg_this_month_avg_price`(本月阶梯电费÷本月用电,`元/kWh`,
|
||||||
`csg_current_ladder_tariff` 0.639);月费用回退模板
|
availability 照 W1N-239 惯例;**csg_* 实体 14→15**);② 面板改名「环比上月」
|
||||||
`csg_this_month_ladder_cost`。**语义**:day 行 usage/cost 为该日值,
|
→「环比上月同期」;glance「本月/上月」grid 去重为单卡「上月」(本月用电/电费
|
||||||
ladder/balance 为 22:30 快照值。详见 [hosts/pgdb.md](../hosts/pgdb.md)。
|
行归 💰核心数据卡);⚡阶梯电价卡加「本月实际均价」行(当前档位/当前电价/
|
||||||
|
本月实际均价/档位剩余;面板唯一实体引用 20→21);③ `automations.yaml` 加
|
||||||
|
2 条提醒:`automation.csg_mian_ban_qie_dong_ji_dang_ti_xing`(10-25 09:00)
|
||||||
|
与 `automation.csg_mian_ban_qie_xia_ji_dang_ti_xing`(4-25 09:00)经
|
||||||
|
`matrix_e2ee.send_message` 提醒切 gauge。④ 金额单位混排(原生 CNY vs 模板
|
||||||
|
元)**维持**:`config/entity_registry/update` 拒绝自定义文本单位
|
||||||
|
(`extra keys not allowed … Got '元'`),用户确认接受。备份:
|
||||||
|
`.lovelace-backups/dashboard-unknown-power-monitor-20260904-204757-pre-refactor.json`、
|
||||||
|
`.csg-backups/csg_sensors.yaml.bak-20260904-204757-pre-refactor`(及
|
||||||
|
`-205301-pre-avgprice`)、`.automations-backups/automations.yaml.bak-*`。
|
||||||
|
**WS 改法**:`lovelace/config` 读、`lovelace/config/save` 写
|
||||||
|
(`lovelace/config/get` 不存在)。客户端用 [`scripts/ha-ws-client.py`](../scripts/ha-ws-client.py),
|
||||||
|
见下方 Quick 节。勿直改 `.storage/`。验证:新实体
|
||||||
|
0.589 元/kWh、15 个 csg_* 数值齐全、回归值不变(14.09/198.65/331.22/
|
||||||
|
304.99/181.89)、automations on、`ha core check` OK、日志无 template 错误。
|
||||||
|
|
||||||
> **Seasonal gauge switch (W1N-240 已知事项):** 每年 **11-01** 把
|
**CSG 面板 Sections 重构 2026-09-22(VPS-90 v4):**
|
||||||
> `power-monitor` 视图「本月累计」gauge 切到冬季 `max:450` /
|
`dashboard-unknown/power-monitor` 从单列 Masonry 改为原生 Sections(最多 2 列、
|
||||||
> `0/200/400`,**5-01** 切回夏季 `max:650` / `0/260/600`(与模板
|
关闭 dense placement),并新增子视图 `power-details`。主页按「本月概览 → 阶梯
|
||||||
> `now().month` 季节逻辑对齐;模板常量在 Block B/D `variables`)。
|
电价 → 最新数据/季节累计 → 近 30 日趋势」组织:概览只保留本月用电/阶梯电费
|
||||||
|
与明确标为「预计」的用电/电费;余额因口径未独立验证不展示;欠费仅在
|
||||||
|
`sensor.0800041935246530_arrears > 0` 时显示。详情页承载上月、近 30 日、月日均、
|
||||||
|
环比及本年/去年账单统计;移除无决策价值的 `csg_prediction_progress` 展示。
|
||||||
|
趋势图使用 `custom:apexcharts-card` 读取 `sensor.csg_history.history_data` 最近 30 条
|
||||||
|
(该属性来源是上月 + 本月逐日明细),画日柱和 30 日均线,不接 Timescale reader;
|
||||||
|
两条 series 均显式覆盖 `unit: kWh`,避免继承 `sensor.csg_history` 的记录数单位
|
||||||
|
「条」。用户首轮目视反馈后,「最新数据与本月进度」改为跨两栏:最新日用电与
|
||||||
|
本月日均各占桌面半栏,当前季节 gauge 全宽,窄屏由 Sections 自动回落堆叠。
|
||||||
|
外部实施审核收口时补齐预测不确定性标识:两张 Tile 分别命名为
|
||||||
|
「预计本月用电(估算)」/「预计本月电费(估算)」,且「本月概览」副标题明确
|
||||||
|
写明「预测按当前速率推算,月内会调整」;仅改展示文案,不改计算或实体。
|
||||||
|
|
||||||
|
季节 gauge 由 UI Template Helper 自动切换:配置入口
|
||||||
|
`01M337A71HTAEBF8AB769J02WF`(domain `template`,title
|
||||||
|
`CSG Summer Tariff`,source `user`,state `loaded`),实体
|
||||||
|
`binary_sensor.csg_summer_tariff`,状态模板
|
||||||
|
`{{ 5 <= now().month <= 10 }}`。`on` 显示夏季 `max 650 / 260 / 600`,`off`
|
||||||
|
显示冬季 `max 450 / 200 / 400`;状态既非 on 也非 off 时显示警告。现有 4-25 /
|
||||||
|
10-25 Matrix 提醒保留,不改 automation;该 helper 当前只被此 dashboard 引用。
|
||||||
|
|
||||||
|
写入前 WS 快照:
|
||||||
|
`/homeassistant/.lovelace-backups/dashboard-unknown-power-monitor-20260922-0815-pre-sections-v4.json`
|
||||||
|
(4953 bytes,SHA-256
|
||||||
|
`f5e3e05ab29a4c13b870547b0a189e95b793d85e82c85c61190dedf66811a874`)。
|
||||||
|
首轮 v4 到布局/单位修正前的第二快照:
|
||||||
|
`/homeassistant/.lovelace-backups/dashboard-unknown-power-monitor-20260922-0825-pre-layout-unit-fix.json`
|
||||||
|
(SHA-256 `8b56e1303572405d7c850305ffd435cc5c7ca583330160fdd3f8b213473e1fcd`)。
|
||||||
|
外部审核文案修正前的第三快照:
|
||||||
|
`/homeassistant/.lovelace-backups/dashboard-unknown-power-monitor-20260922-0838-pre-estimate-copy-fix.json`
|
||||||
|
(文件 SHA-256 `b1c1e0c6946818ce7fb44d5d731e3e78aebe9c8d51ed465ce8505a0f1855c05c`)。
|
||||||
|
验证:`lovelace/config/save` 后规范化 JSON 哈希与 payload 一致;22 个引用实体全部
|
||||||
|
存在且无 unknown/unavailable;夏季 helper=`on`;图表 30 条为 2026-08-22 至
|
||||||
|
2026-09-20,重算均值 9.15 kWh 与 `sensor.csg_30d_avg` 一致;最终文案修正后的
|
||||||
|
规范化配置哈希为
|
||||||
|
`c16d11c01353f90761da5837b06cc7ad1ea7b400f249c99cf291c03f4980a28d`。浏览器没有 HA
|
||||||
|
登录态,390/768/桌面三宽度视觉验收仍待人工登录后完成。回滚时先把上述 JSON
|
||||||
|
经 WS `lovelace/config/save` 写回;再扫描 helper 引用,确认无消费者后才可删除
|
||||||
|
配置入口,勿直改 `.storage/`。
|
||||||
|
|
||||||
|
**CSG 长期归档(W1N-243, 2026-08-29;任务改号 2026-09-21;口径统一 v5 2026-09-22):**
|
||||||
|
scribe 库 `csg_history` 表(逐日 usage/cost/ladder/balance + 逐月累计;2026-07-01 起
|
||||||
|
回填,永久),由 TimescaleDB **两个**任务维护(**均为 TS job,非 pg_cron**):
|
||||||
|
**1010** `csg_daily_snapshot()`(14:30 UTC = 22:30 Asia/Shanghai)写当日行;
|
||||||
|
**1011** `csg_backfill_missing_days()`(15:10 UTC = 23:10 Asia/Shanghai)是
|
||||||
|
**断档自愈**——只 `INSERT` 缺失日期、`ON CONFLICT DO NOTHING`,**绝不覆盖既有行**。
|
||||||
|
|
||||||
|
> **归档逻辑全部在 pgdb 侧,不经过 HA。** HA 的 scribe 集成只往 `states_raw` 写状态,
|
||||||
|
> 上面两个任务在 pgdb 侧读 `states_raw` 落表。因此 `csg_history` 相关的口径/job 变更
|
||||||
|
> **不需要改 HA 配置、不需要重启 Core**。另注意:模板传感器
|
||||||
|
> `sensor.csg_history`(state = 天数「51 条」)读的是集成 attributes
|
||||||
|
> `this_month_by_day` / `last_month_by_day`,**与这张同名表无关**,别混淆。
|
||||||
|
|
||||||
|
**v5 口径(2026-09-22)**:数据日期与当日用量取**同一实体同一行**
|
||||||
|
`sensor.0800041935246530_latest_day_kwh`(`latest_day_date` 属性 + `value`;v3 曾用它的
|
||||||
|
属性定日期却用 `yesterday_kwh` 取值,两实体可能错配,且后者归档少一条 09-06 = 9.29)。
|
||||||
|
日费用 = `round(csg_ladder_cost_raw(c1) − csg_ladder_cost_raw(c0), 2)`,即**当月累积的
|
||||||
|
边际差分**,与 v3 回填同口径;`csg_ladder_cost_raw` 是新增的**无舍入**版助手(直接用
|
||||||
|
`csg_ladder_cost(c1) − csg_ladder_cost(c0)` 会二次舍入,7 月实测 1 天差 0.01)。月行由
|
||||||
|
`csg_ladder_cost` 单次舍入重算。**语义**:day 行 usage/cost 为该日值,ladder/balance 为
|
||||||
|
快照值;各月 `|sum(day cost) − month cost| ≤ 0.05` 元是**逐日舍入的累积**(非缺陷),
|
||||||
|
月行才是权威值。脚本 `compose/pgdb/csg-snapshot-v5.sql`,快照 `csg_history_bak_20260922`。
|
||||||
|
健康检查:[pgdb-health](../runbooks/pgdb-health.md) 第 9 项专查归档新鲜度——**上游停更时
|
||||||
|
job 会反复 upsert 同一行、`last_run_status` 仍是 `Success`、日期上也没有缺口,只有
|
||||||
|
「最新 day 行日期」这条断言能发现**。详见 [hosts/pgdb.md](pgdb.md) § Known issues
|
||||||
|
2026-09-21(1008 从未成功的根因与回填)与 **2026-09-22**(v5 口径统一 + 自愈 + 校验)。
|
||||||
|
|
||||||
|
**CSG 预测值复核(2026-09-21,只读):** 对
|
||||||
|
`sensor.csg_*` 全部 14 个派生值用独立 Python 实现按同一输入重算,逐个与 HA
|
||||||
|
live state 完全一致(`csg_30d_avg` 9.07 / `csg_predicted_usage` 268.45 /
|
||||||
|
`csg_predicted_cost_ladder` 158.54 / `csg_this_month_ladder_cost` 99.35 /
|
||||||
|
`csg_mom_change` -21.6 等)。同时把 9 月逐日口径对回 scribe:集成
|
||||||
|
`this_month_by_day`(09-01..09-19,19 天,和 168.68)与 `states_raw` 里
|
||||||
|
`yesterday_kwh` 观测值逐日一致(唯一差异 09-06 = 9.29 是 scribe 漏采、集成有值),
|
||||||
|
确认输入数据无缺口。**因此「预测本月电费」不是算错,而是估计器的窗口问题**:
|
||||||
|
`csg_predicted_usage = 本月累计 + avg × 剩余天数`,旧版 `avg = csg_30d_avg`
|
||||||
|
(`history_data[-30:]` 的**普通 30 日均值、跨月重叠**)→ 月初被上月(更热)拖高
|
||||||
|
(09-02 实测 317.93 kWh / 190.16 元),随本月凉快日置换回落到 268.45 / 158.54,
|
||||||
|
一个月内摆动 **-17%**。**另注**:集成原生
|
||||||
|
`latest_day_cost` / `this_month_total_cost` / `last_month_total_cost` 自
|
||||||
|
2026-08-29 起**恒为 unknown**(`states_raw` 无一条有值),故面板上所有「元」
|
||||||
|
均为模板阶梯估算值,从未与账单口径对账。
|
||||||
|
|
||||||
|
**`csg_sensors.yaml` 预测速率 v2(2026-09-21,已实施):** 只改
|
||||||
|
`csg_predicted_usage` 的速率项,`csg_30d_avg` 语义不动(面板「近30日平均」照旧)。
|
||||||
|
新公式:`avg = mtd × w + recent7 × (1 − w)`,其中 `mtd = 本月累计 / 已过天数`、
|
||||||
|
`recent7` = `history_data` 末 7 条均值、`w = min(已过天数, 14) / 14`;两者都取不到
|
||||||
|
才退回 `csg_30d_avg`。**动机**:本月至今速率按天数爬升可信度,避免月初被上月污染;
|
||||||
|
近 7 日速率兜底,因为月初唯一的新数据就是上月末。Block C 触发器加
|
||||||
|
`sensor.csg_history`。**19 天回测**(基准 = 第 19 天本月至今速率外推,逐日重放
|
||||||
|
集成 `this_month_by_day` + `last_month_by_day`):
|
||||||
|
|
||||||
|
| 速率方案 | 预测电费摆动 | 最大偏差 | 平均绝对偏差 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 旧:30 日均值 | 21.1% | 35.99 元 | 14.04 元 |
|
||||||
|
| v2:K=7 | 14.6% | 16.81 元 | 5.65 元 |
|
||||||
|
| **v2:K=14(采用)** | **13.9%** | **15.33 元** | **4.94 元** |
|
||||||
|
| v2:K=14 但兜底用 30 日均值 | 23.3% | 33.12 元 | 6.28 元 |
|
||||||
|
|
||||||
|
(末行证实病根就是那个跨月的 30 日均值兜底。)备份
|
||||||
|
`/homeassistant/.csg-backups/csg_sensors.yaml.bak-20260921-pre-predictor`
|
||||||
|
(改前)与 `…-pre-k14`(K=7 中间态)。**验证**:`ha core check` OK;Core 重启两次
|
||||||
|
(trigger 模板块不吃 `template.reload`,W1N-114 先例);15 个 `csg_*` 全部数值、
|
||||||
|
无 template 错误;当天 `n=19 ≥ 14` 故两版同值,面板读回
|
||||||
|
`csg_predicted_usage` 266.34 kWh / `csg_predicted_cost_ladder` 157.19 元 /
|
||||||
|
`csg_prediction_progress` 63.3%;新值已进 scribe 归档
|
||||||
|
(`states_raw` 08:17:04Z 158.54 → 08:17:10Z 157.19)。**注意**:`w` 的爬升窗口
|
||||||
|
(14 天)是回测选出来的单月参数,属弱证据,换季/换季后值得复看。
|
||||||
|
|
||||||
|
> **季节 gauge:** 不要手改 max/segments。`binary_sensor.csg_summer_tariff`
|
||||||
|
> (`on` = 5–10 月)切换夏季 `650 / 260 / 600` 与冬季 `450 / 200 / 400`,
|
||||||
|
> 见上方 VPS-90 v4。`automation.csg_mian_ban_qie_dong_ji_dang_ti_xing`(10-25)
|
||||||
|
> 与 `automation.csg_mian_ban_qie_xia_ji_dang_ti_xing`(4-25)仍会在 09:00
|
||||||
|
> 发 Matrix 提醒;提醒正文这次没核对。
|
||||||
|
|
||||||
Home PPPoE IPv4 to CSG is still blackholed (`curl -4` to `218.19.148.218:443`
|
Home PPPoE IPv4 to CSG is still blackholed (`curl -4` to `218.19.148.218:443`
|
||||||
times out). `end0` IPv6 is enabled (`ipv6.method: auto`); from HA,
|
times out). `end1` IPv6 is enabled (`ipv6.method: auto`); from HA,
|
||||||
`curl -6 https://95598.csg.cn` returns HTTP 200 via `240e:f9:8060::1:16`.
|
`curl -6 https://95598.csg.cn` returns HTTP 200 via `240e:f9:8060::1:16`.
|
||||||
|
|
||||||
**`tianqi` weather recorder patch (verified 2026-08-13, W1N-75):**
|
**`tianqi` weather recorder patch (verified 2026-08-13, W1N-75):**
|
||||||
@@ -384,9 +511,11 @@ deployed 2026-08-18 from `216cc99` (backup
|
|||||||
reworked in v0.3.1/v0.3.2 to wait for a peer-initiated inbound SAS from
|
reworked in v0.3.1/v0.3.2 to wait for a peer-initiated inbound SAS from
|
||||||
Element with emoji comparison), see
|
Element with emoji comparison), see
|
||||||
[docs/home-assistant-matrix.md § Device verification](../docs/home-assistant-matrix.md).
|
[docs/home-assistant-matrix.md § Device verification](../docs/home-assistant-matrix.md).
|
||||||
### Scribe long-term history (verified 2026-08-29)
|
### Scribe long-term history (3.8.0 setup 2026-08-29; 4.4.0 verified 2026-09-13)
|
||||||
|
|
||||||
- **Scribe 3.8.0** (`/homeassistant/custom_components/scribe/`), configured from
|
- **Scribe 4.4.0** (`/homeassistant/custom_components/scribe/`, HACS repo
|
||||||
|
`jonathan-gtd/scribe`, = latest stable 2026-09-12; upgraded 2026-09-13 together
|
||||||
|
with Core 2026.9.1 / HAOS 18.2), configured from
|
||||||
`/homeassistant/scribe.yaml` — W1N-238 moved the block out of
|
`/homeassistant/scribe.yaml` — W1N-238 moved the block out of
|
||||||
`configuration.yaml` on 2026-08-29 (main config now carries
|
`configuration.yaml` on 2026-08-29 (main config now carries
|
||||||
`scribe: !include scribe.yaml`; content moved verbatim; backup
|
`scribe: !include scribe.yaml`; content moved verbatim; backup
|
||||||
@@ -410,12 +539,32 @@ deployed 2026-08-18 from `216cc99` (backup
|
|||||||
entities (`scribe_states_written`, `scribe_events_written`, rates, sizes).
|
entities (`scribe_states_written`, `scribe_events_written`, rates, sizes).
|
||||||
- Verified post-restart 14:23 CST: writer started, `scribe_events_written=1`
|
- Verified post-restart 14:23 CST: writer started, `scribe_events_written=1`
|
||||||
(homeassistant_start), states ~110/min, buffer 3, no scribe log errors.
|
(homeassistant_start), states ~110/min, buffer 3, no scribe log errors.
|
||||||
- **Scribe 3.8.0 has no retention option.** Retention ships only in the v4.x line,
|
- **4.x upgrade核对 2026-09-13(只读 + 一处配置变更)**:live `manifest.json` =
|
||||||
which as of 2026-08-29 has no stable release (v4.0.0rc1/v4.1.0rc1 are
|
4.4.0。两个 4.0 breaking change 在本机都不需要动作——数据库是 3.x 结构
|
||||||
prereleases; user declined RCs — data keeps growing until an upgrade). v4.x is
|
(`states_raw` PK `(metadata_id, time)` 在,4.2.0 的启动态去重因此可用),
|
||||||
a major rewrite (writer.py largely rewritten, migration.py removed, TimescaleDB
|
TimescaleDB 2.29.2 已装。4.1.0 修了 `db_url` 优先级,YAML 里的
|
||||||
extension required): re-read release notes before upgrading. Do not expect
|
`!secret scribe_url` 现在是权威。`scribe.yaml` 现有键在 4.4.0 全部仍然合法
|
||||||
retention YAML keys to validate on 3.8.0.
|
(未知键被忽略,`extra=vol.ALLOW_EXTRA`)。**配置优先级 YAML > entry
|
||||||
|
`options` > entry `data` > 默认值**,而 `_resolve_settings` 读的是
|
||||||
|
`hass.data[DOMAIN]["yaml_config"]`(只有 `async_setup` 会写),所以
|
||||||
|
**YAML 改动必须重启 Core,reload config entry 不重读 YAML。**
|
||||||
|
- **`stats_io_interval: 300`(2026-09-13 添加**,备份
|
||||||
|
`/homeassistant/scribe.yaml.bak-20260913-191558`)。4.4.0 不再让 HA 每 30s
|
||||||
|
轮询 I/O 统计传感器,改由集成自己每 60s 发布,间隔成为配置项。scribe 自己的
|
||||||
|
传感器此前是本机自写历史的主要来源(变更前 24h:11 019 / 87 461 行状态 =
|
||||||
|
12.6%),60s → 300s 把这部分降约 5 倍(每个 I/O 传感器约 1440 → 288 行/天)。
|
||||||
|
验证:`ha core check` OK;重启 88s;`ScribeWriter started successfully`;
|
||||||
|
无 scribe error/warning;scribe Repairs 问题 0 条;传感器发布间隔实测正好
|
||||||
|
300s(11:19:26 → 11:24:26 UTC)。
|
||||||
|
- **Retention 现在可用但刻意不设**:`retention_states` / `retention_events`
|
||||||
|
(4.0.0)按间隔丢 chunk,留空 = 永久保留,符合本机定位(Scribe 是永久归档,
|
||||||
|
recorder 保留 365 天)。注意 retention 是**绕过** entry `data` 副本读取的
|
||||||
|
(`from_entry_data=False`),所以删掉 YAML 行即撤销策略。`db_schema`、
|
||||||
|
`enable_rollups`、`scribe.purge` 同样未用:图表走 `sensor_minute` +
|
||||||
|
`timescale_database_reader`(见 [hosts/pgdb.md](pgdb.md)),不吃 scribe 自己的
|
||||||
|
视图,配置里也没有任何 `scribe.query` 调用。`flush_interval` 仍是 entry
|
||||||
|
`data` 钉住的 5s——上游下一个版本把默认改成 30s,但 entry 值优先,要采用只能
|
||||||
|
在 YAML 显式写 `flush_interval: 30`。
|
||||||
- Recorder stays external-Postgres with `purge_keep_days: 365` (W1N-243,
|
- Recorder stays external-Postgres with `purge_keep_days: 365` (W1N-243,
|
||||||
2026-08-29, raised from 30 — ~300 MB/yr, 1% of the 30G pgdb disk) for
|
2026-08-29, raised from 30 — ~300 MB/yr, 1% of the 30G pgdb disk) for
|
||||||
native UI per-change history; Scribe is the permanent archive. Long-term
|
native UI per-change history; Scribe is the permanent archive. Long-term
|
||||||
@@ -429,20 +578,20 @@ deployed 2026-08-18 from `216cc99` (backup
|
|||||||
`template: !include_dir_merge_list templates`. No `packages/`.
|
`template: !include_dir_merge_list templates`. No `packages/`.
|
||||||
- `scribe.yaml` (config root): the Scribe block, content identical to the
|
- `scribe.yaml` (config root): the Scribe block, content identical to the
|
||||||
former inline one; import semantics unchanged.
|
former inline one; import semantics unchanged.
|
||||||
- `templates/`: `csg_sensors.yaml` (12 template sensors, top-level **list**)
|
- `templates/`: `csg_sensors.yaml` (15 `csg_*` template sensors, top-level **list**)
|
||||||
+ `quick_sensors.yaml` (scaffold for Quick-derived `quick_*` sensors, empty
|
+ `quick_sensors.yaml` (`sensor.quick_wen_cha`, same top-level list).
|
||||||
list with convention header). **`!include_dir_merge_list` merges per-file
|
**`!include_dir_merge_list` merges per-file lists; non-list files are silently skipped**
|
||||||
lists; non-list files are silently skipped** — every file in `templates/`
|
— every file in `templates/` must be a top-level list (`- sensor:` blocks).
|
||||||
must be a top-level list (`- sensor:` blocks). Directory include only picks
|
Directory include only picks up `*.yaml`, so `.bak` / `.pre-*` backups in the
|
||||||
up `*.yaml`, so the `.bak` / `.pre-*` backups in the dir are ignored. After
|
dir are ignored. UI helpers such as `binary_sensor.csg_summer_tariff` are not
|
||||||
adding sensors, verify template-platform entity count = 12 + N (entity
|
in this directory. After adding sensors, count template-platform entities
|
||||||
registry `platform: template`).
|
against these files plus the UI helpers, not against the old "12".
|
||||||
- Convention (per review + W1N-233): pure sums/averages stay min_max helpers
|
- Convention (per review + W1N-233): pure sums/averages stay min_max helpers
|
||||||
(e.g. `sensor.dang_qian_zong_gong_lu`); only template-logic derivations
|
(e.g. `sensor.dang_qian_zong_gong_lu`); only template-logic derivations
|
||||||
(ladder pricing, cross-entity conditions) go into `quick_sensors.yaml`.
|
(ladder pricing, cross-entity conditions) go into `quick_sensors.yaml`.
|
||||||
- Post-change verification 20:19 CST: `ha core check` ok, 92 s restart
|
- Post-change verification 20:19 CST: `ha core check` ok, 92 s restart
|
||||||
(2026.8.3), `binary_sensor.scribe_database_connection` on,
|
(2026.8.3), `binary_sensor.scribe_database_connection` on,
|
||||||
`scribe_states_written` 18581→19426 growing, template entities still 12,
|
`scribe_states_written` 18581→19426 growing, template entities still 12 that day (later 15 `csg_*` + `quick_wen_cha`),
|
||||||
csg sensors numeric, no scribe/template log errors.
|
csg sensors numeric, no scribe/template log errors.
|
||||||
|
|
||||||
### Timescale Plotly card + database reader (verified 2026-08-29)
|
### Timescale Plotly card + database reader (verified 2026-08-29)
|
||||||
@@ -478,33 +627,48 @@ manual copies): reader `remmob/timescale_database_reader` **v1.1.0** (main
|
|||||||
- Scribe stores numeric sensor values in `states_raw.value` with `state` NULL,
|
- Scribe stores numeric sensor values in `states_raw.value` with `state` NULL,
|
||||||
so `sensor_minute.state` shows `'0'` for numeric sensors; the card plots
|
so `sensor_minute.state` shows `'0'` for numeric sensors; the card plots
|
||||||
`avg_state` (from `value`) — expected, not a bug.
|
`avg_state` (from `value`) — expected, not a bug.
|
||||||
- **Quick 仪表盘(`dashboard-quick`)图表套件**(2026-08-29 创建,经 WS
|
|
||||||
`lovelace/config/save` 写入;W1N-230 修复 + W1N-231 round-2 改进):
|
### Quick 仪表盘(`dashboard-quick`,现行 2026-09-23)
|
||||||
5 张 timescale 卡——大功率电器/常驻负载功率(按量级拆图,避免尖峰压扁
|
|
||||||
<70 W 基线)、按插座用电量(`energy_mode` + cumulative/diff,数据质量前提
|
`http://hass.windy.lan:8123/dashboard-quick`。视图 `type: sections`,`max_columns: 4`。
|
||||||
见 pgdb 的 refresh 过程补丁)、室内外温湿度(温度左轴/湿度右轴,4 位置同色
|
分区宽用 `column_span`(缺省是 1 列,不要省)。卡片宽用 `grid_options.columns`
|
||||||
配对)、人体感应活动状态(3 个 `motion_state`,banded `state_map`
|
(写在卡片上的 `column_span` 会被忽略)。空调实体是 `kong_diao_*`;`kong_tiao` 是笔误,应为 0 处。
|
||||||
none/small/medium/large → 0-11,per-entity `line_color` 红/蓝/绿)。
|
|
||||||
空调实体引用为 `kong_diao_*`(`kong_tiao` 是笔误,W1N-230 修复;`grep -c
|
| 分区 | span | 现在有什么 |
|
||||||
kong_tiao` 应为 0)。灯区:2×2 嵌套 grid(`grid_options: {columns: "full"}`,
|
|---|---|---|
|
||||||
内层 `columns: 2`)+ 4 卡统一 `mushroom-light-card`(显式 name、
|
| 灯与开关 | 4 | 2 列网格:卫生间灯、厨房灯、客厅长明灯、厨房远灯、Matter 开关「马桶换气电源」(`switch.wei_sheng_jian_ma_tong_huan_qi_dian_yuan`)。不放 `switch.0xa4c138a4c2fe4c8b`。徽章「换气功率」= `sensor.0xa4c138a4c2fe4c8b_power` |
|
||||||
`use_light_color: false`、内联亮度/色温控制),heading icon
|
| 实时功率 | 2 | apexcharts donut,11 路当前功率(与下面两张功率图同一批)。末项「马桶换气」= 上面那颗 Zigbee 功率。图例在右,`chart.height` 510;card-mod 把 `ha-card` 撑到 520px。徽章「插座合计」= `sensor.dang_qian_zong_gong_lu` |
|
||||||
`mdi:lightbulb-group`。heading badges:环境 4 温度(迷你/mini数显/数显/广州)、
|
| 人体感应 | 2 | 厨房 / 卫生间 / 厨房(电池)。`state_map` 0–11,`downsample_method: last`。卡高 280,card-mod 520px,和圆环并排。默认 12h |
|
||||||
大功率电器 空调/电脑当前功率、常驻负载 总功率
|
| 峰值负载 | 2 | 厨房电器、空调、电水壶、咖啡机、电脑。`downsample_method: avg`,高 280,默认 6h。徽章:空调、电脑。不要把约 25 W 的马桶换气放这里 |
|
||||||
(`sensor.dang_qian_zong_gong_lu`,min_max **sum** helper,`round_digits: 0`,
|
| 基础负载 | 2 | 主网络、冰箱、大房、水设备、无线AP、马桶换气(`sensor.0xa4c138a4c2fe4c8b_power`)。卡 `fill: tozeroy`;主网络和冰箱有 `fill_color`,其余 4 条 `fill: false`。同样 `avg`、高 280、默认 6h |
|
||||||
任一源掉线 fail-closed → unknown)。常驻负载图卡级 `fill: 'tozeroy'` +
|
| 今日用电 | 4 | `energy_mode`,11 路 kWh。末项「马桶换气」= `sensor.0xa4c138a4c2fe4c8b_energy`(`total_increasing`)。高 320,默认 today |
|
||||||
冰箱/主网络 per-entity `fill_color`(线色 20% 透明)+ 其余 5 条 `fill: false`
|
| 温湿度 | 4 | `sensor.quick_wen_cha`(室内均温 − 室外)+ 湿度 4 条。两张卡 card-mod 520px,默认 24h。徽章:卫生间 / 客厅 / 卧室 / 室外温度 |
|
||||||
(per-entity fill 逐系列退出,卡 JS `seriesConfig.fill !== false`)。
|
|
||||||
布局:视图 `type: sections` + `max_columns: 4`;灯/用电/环境/人体感应
|
功率图档 `1h,3h,6h,12h,24h,3d,7d`。人体感应档 `3h,6h,12h,24h,3d,7d`。温湿度档 `6h,12h,24h,48h,7d,14d,30d`。今日用电档 `today,week,month,3mo,custom`。`sensor_minute` 最早只到 **2026-08-29**,长于约 15 天的档前半段是空的。
|
||||||
`column_span: 4`,功率两图拆两个 `column_span: 2` 分区**并排**(等高 280px,
|
|
||||||
桌面并排、手机回落堆叠;去卡内 title 省半宽图垂直空间)。
|
「插座合计」是 min_max **sum**(entry `01M16N648QBFG13P07XQMESBSP`,`round_digits: 0`),含上面 11 路功率,不含灯。任一源掉线则 unknown。2026-09-23 读回 380 W,与 11 路之和一致。
|
||||||
**分区/卡片是两套尺寸键,不可混用**:分区宽 = `column_span`
|
|
||||||
(`hui-sections-view.ts` 缺省按 1 列渲染,绝不省略);卡片宽 =
|
**别按旧说明操作:**
|
||||||
`grid_options: {columns: <n|"full">}`(`hui-card.ts` 只读 `config.grid_options`,
|
|
||||||
写在卡片上的 `column_span` 被静默忽略;缺省 12 列,分区内格 = 12 × 分区
|
- 功率图必须 `downsample_method: avg`。reader 对 `sensor_minute` 默认 `last`(桶末瞬时值),空调会画成停机。今日用电用累计表的 `last` 再 diff,不要改成 avg。人体感应保持 `last`。
|
||||||
span,故 span-4 分区里缺省卡片只有 1/4 宽)。
|
- 卡 JS 曾把 `7d` 解析成 7 小时。2026-09-21 已改用 `parseDurationToMs`,资源 URL 带 `?v=20260921`。换 JS 后要再 bump `?v=`。命名档只有 `today|week|month|3mo|6mo|year|years|custom`。
|
||||||
修改前备份:`/homeassistant/.lovelace-backups/dashboard-quick-*.json`
|
- apexcharts 的 `chart.height` 含图例,timescale 的 `height` 只是绘图区。并排等高用 card-mod 的 `ha-card`,不要把两个数字写成一样。
|
||||||
(W1N-230 修复: `20260829-190256`;round-2 改进: `20260829-194040`)。
|
- 保存后圆环可能缩在角落。配置没变,拖一下窗口让它重算。
|
||||||
|
- 无线 AP 曾有单点 976 W,会把基础负载的 y 轴顶飞。未查。
|
||||||
|
- `/energy` 的马桶换气仍是 Matter 积分 `sensor.wei_sheng_jian_ma_tong_huan_qi_dian_yuan_energy`,不是这颗 Zigbee 电量。能源面板也没有主网络、无线 AP。
|
||||||
|
|
||||||
|
**改法:** WS `lovelace/config` 读、`lovelace/config/save` 写。勿改 `.storage/`。
|
||||||
|
用 [`scripts/ha-ws-client.py`](../scripts/ha-ws-client.py)(supervisor 镜像里的 python3 + `SUPERVISOR_TOKEN`,不要 `-v /tmp`)。
|
||||||
|
min_max 在 HA 2026.9 没有 WS `config_entries/options/flow`,走 supervisor 的
|
||||||
|
`POST /core/api/config/config_entries/options/flow`。`scribe.yaml` 已是 `record_states: true`,新传感器不用改它。
|
||||||
|
`sensor.quick_wen_cha` 在 `templates/quick_sensors.yaml`,`template.reload` 即可,不用重启 Core。
|
||||||
|
|
||||||
|
**回滚:** 计量改动前
|
||||||
|
`.lovelace-backups/dashboard-quick-20260923-080213-pre-meter-plug.json`
|
||||||
|
(SHA-256 `a30b200350923633ddde0f2f071f2b75f0f37b347da4dd9a688da295dd8c8f35`)。
|
||||||
|
布局和卡 JS:`dashboard-quick-20260921-pre-titles.json` +
|
||||||
|
`timescale-plotly-card.js.bak-20260921-pre-dayfix`。
|
||||||
|
还原 JSON 用 WS save。改过卡 JS 要把资源 `?v=` 再 bump。
|
||||||
|
|
||||||
### 地图仪表盘:CARTO keyed tiles via `custom:map-card` (verified 2026-08-30, W1N-261)
|
### 地图仪表盘:CARTO keyed tiles via `custom:map-card` (verified 2026-08-30, W1N-261)
|
||||||
|
|
||||||
@@ -535,9 +699,8 @@ manual copies): reader `remmob/timescale_database_reader` **v1.1.0** (main
|
|||||||
- **验证 8/30:** 同瓦片无 key=水印 / 带 key=干净(256×256 PNG 视觉对比);
|
- **验证 8/30:** 同瓦片无 key=水印 / 带 key=干净(256×256 PNG 视觉对比);
|
||||||
resource HTTP 200 text/javascript;WS 读回卡片配置(type/entities/key/
|
resource HTTP 200 text/javascript;WS 读回卡片配置(type/entities/key/
|
||||||
attribution/options)全部符合;HA 主机 `curl -4` 带 key 瓦片 200。
|
attribution/options)全部符合;HA 主机 `curl -4` 带 key 瓦片 200。
|
||||||
- **Follow-up:** Core 升 2026.9.0 stable 后内置地图/zone 编辑器自动切
|
- **Follow-up:** Core 现为 2026.9.3(2026-09-23)。内置地图是否已切 OSMF
|
||||||
OSMF 矢量瓦片;届时可保留 custom 卡(继续 keyed CARTO)或用备份还原
|
矢量瓦片没有复查;本仪表盘仍用 keyed CARTO 的 `custom:map-card`。
|
||||||
内置卡。zone 编辑器等其余内置地图的水印在 2026.9 前无解。
|
|
||||||
|
|
||||||
## Known issues
|
## Known issues
|
||||||
|
|
||||||
@@ -598,6 +761,91 @@ advertised correctly over v4+v6. Both ESP32-C2 bulbs now announce `_matter._tcp`
|
|||||||
:5540 sessions** (device-side failure mode C; no errors logged — see
|
:5540 sessions** (device-side failure mode C; no errors logged — see
|
||||||
[docs/matter-pairing-troubleshoot.md §8](../docs/matter-pairing-troubleshoot.md)).
|
[docs/matter-pairing-troubleshoot.md §8](../docs/matter-pairing-troubleshoot.md)).
|
||||||
|
|
||||||
|
## 马桶换气电源(Matter 插座,半计量)+ 电量估算 (2026-09-13)
|
||||||
|
|
||||||
|
Quick 从 2026-09-23 起不再用这颗插座的功率或积分电量,见上方 Quick 节。
|
||||||
|
下面的积分实体仍是 `/energy` 的第 9 个 grid 源(当日读回)。Matter 开关仍在「灯与开关」。
|
||||||
|
|
||||||
|
|
||||||
|
**设备**:Matter `Smart Plug`(SIXWGH,`model_id 3596`,hw 1.0 / sw 1.3.0),node 18
|
||||||
|
(0x12),`device_id 5ef1850953466d6e7a9c6b901fbebe1c`,config entry
|
||||||
|
`01JF51VQ48PGJGXX3RNAG6MVAA`,区域**卫生间** (`wei_sheng_jian`),label `power`;
|
||||||
|
2026-09-13 17:58 CST 配对。实体:
|
||||||
|
`switch.wei_sheng_jian_ma_tong_huan_qi_dian_yuan`(插座)、
|
||||||
|
`sensor.…_dian_yuan`(电源 W)、`sensor.…_dian_ya`(电压 V)、
|
||||||
|
`sensor.…_you_gong_dian_liu`(有功电流 A)、`sensor.…_dian_li`(电力 kWh,
|
||||||
|
**永久 unknown**)。
|
||||||
|
|
||||||
|
**根因(实测 Matter 属性,node 18)**:电量簇 0x0091 `FeatureMap = 13`
|
||||||
|
(IMPE|CUME|PERE,即**声明**支持导入/累计/周期电量),但
|
||||||
|
`CumulativeEnergyImported (0x0001)` 恒为 `null`,`PeriodicEnergyImported
|
||||||
|
(0x0003)` 带载也恒为 `{Energy: 0}`;`CumulativeEnergyExported (0x0002)`
|
||||||
|
不存在(EXPE 未声明,自洽)。HA 只用 `CumulativeEnergyImported` 建能量实体
|
||||||
|
(`components/matter/sensor.py:1083`,`allow_none_value=True`)→ 该实体
|
||||||
|
**永远不会出数**。**功率计量本身正常**:0x0090 `FeatureMap = 2` (ALTC),
|
||||||
|
Voltage / ActiveCurrent / ActivePower 都随负载变化(实测 220.3 V / 118 mA /
|
||||||
|
24.7 W,HA `电源` 0.0→24.9 W 有历史)。厂商 `update` 实体报无新固件。
|
||||||
|
|
||||||
|
**处理(方案 A:功率积分补电量)**:
|
||||||
|
|
||||||
|
- 新建 **Integration (Riemann sum) 辅助元素**:config entry
|
||||||
|
`01M2D53T188FW8WEC547ENHSVH`(domain `integration`,state `loaded`),
|
||||||
|
source `sensor.wei_sheng_jian_ma_tong_huan_qi_dian_yuan_dian_yuan`,
|
||||||
|
`method: trapezoidal`、`unit_prefix: k`、`unit_time: h`、`round: 3`、
|
||||||
|
`max_sub_interval: 60s`。
|
||||||
|
- 实体 `sensor.wei_sheng_jian_ma_tong_huan_qi_dian_yuan_energy`(创建时 HA
|
||||||
|
自动生成 `…_dian_yuan_ma_tong_huan_qi_dian_yuan_dian_liang`,随后立即
|
||||||
|
`config/entity_registry/update` 改名为 `<插座>_energy` 以对齐约定;
|
||||||
|
该实体新建、无引用,改名安全),friendly name「马桶换气电源 电力」,
|
||||||
|
unit kWh、`device_class: energy`、**`state_class: total`**——能源仪表盘
|
||||||
|
允许 `TOTAL` 与 `TOTAL_INCREASING`(`components/energy/validate.py:279`)。
|
||||||
|
- **能源仪表盘** (`/energy`):grid 源 `[8]` 由 `…_dian_li` 改为 `…_energy`,
|
||||||
|
其余 8 条插座源未动。注意这 9 条「插座」全部以 `type: grid` 注册,被当作
|
||||||
|
全屋用电代理;`switch` 卡片所在的 Grid 卡片此前第 9 行是空的,即本次修复点。
|
||||||
|
- **Quick(已过时):** 2026-09-13 曾把用电量图指到 `…_energy`,并单独加过开关分区。
|
||||||
|
现行 Quick 见上方。不要按本段把 Quick 改回去。
|
||||||
|
|
||||||
|
**口径警告**:`…_energy` 是**估算值**(Riemann 积分,只在 HA 运行期间累计、
|
||||||
|
非账单级),与另外 8 个原生计量插座的累计电量口径不同;功率传感器更新
|
||||||
|
间隔约 5–10 s(实测 24.9/24.8/25.0 W 抖动),加 `max_sub_interval: 60s`
|
||||||
|
保证静默时也继续累计。
|
||||||
|
|
||||||
|
**Agent 侧建辅助元素的方法(2026-09-13 实测)**:HA 的 config flow 走
|
||||||
|
**REST**(WS 只有 `config_entries/flow/progress|subscribe`,没有 start)。
|
||||||
|
经 supervisor 代理即可,无需 HA 长连接/长寿命 token:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SUPERVISOR_TOKEN 由 sudo -n -i 提供
|
||||||
|
curl -s -X POST -H "Authorization: Bearer $SUPERVISOR_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" -d '{"handler":"integration"}' \
|
||||||
|
http://supervisor/core/api/config/config_entries/flow # → {flow_id, step_id:"user", data_schema}
|
||||||
|
curl -s -X POST -H "Authorization: Bearer $SUPERVISOR_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"…","source":"sensor.x","method":"trapezoidal","round":3,
|
||||||
|
"unit_prefix":"k","unit_time":"h","max_sub_interval":{"minutes":1}}' \
|
||||||
|
http://supervisor/core/api/config/config_entries/flow/<flow_id> # → create_entry
|
||||||
|
```
|
||||||
|
|
||||||
|
`auth/long_lived_access_token` 在 supervisor 代理身份下**失败**
|
||||||
|
(`unknown_error`),故无法用长寿命 token 开浏览器会话;`DurationSelector`
|
||||||
|
的值是 `{"minutes":1}` 形式(`cv.time_period`)。
|
||||||
|
|
||||||
|
**备份/回滚**:`.lovelace-backups/dashboard-quick-20260913-181251-pre-ma-tong-plug.json`
|
||||||
|
(改动前原件)、`…-20260913-183210-pre-repoint.json`(改名/换源前);
|
||||||
|
`.ha-backups/energy-20260913-183135-pre-ma-tong-repoint.json`(能源 prefs)。
|
||||||
|
回滚能源估算 = 把能源 prefs 的源 [8] 指回 `…_dian_li`。
|
||||||
|
不要用 2026-09-13 的 Quick JSON 覆盖现在的面板,那会撤掉后来的布局和 Zigbee 计量。
|
||||||
|
彻底放弃估算电量 = 删除 config entry `01M2D53T188FW8WEC547ENHSVH`。
|
||||||
|
|
||||||
|
**验证 (2026-09-13 18:3x)**:`…_energy` 0.002→0.003 kWh 且随 24.6 W 负载
|
||||||
|
增长(换气扇关掉后回落 0.0 W,累计值保留);`recorder/list_statistic_ids`
|
||||||
|
已含该实体;Quick 面板 WS 读回 7 分区、用电量图 11 项指向新实体、旧
|
||||||
|
`_dian_li` 引用 0 处;能源 prefs 读回 9 源、第 9 条为新实体(Quick 的这次读回只对 2026-09-13 有效;2026-09-23 起 Quick 改指 Zigbee,`/energy` 第 9 源仍是该积分实体)。
|
||||||
|
**`energy/validate` 已全绿**(9 源 0 issue):创建后 ~5 min 内曾报
|
||||||
|
`statistics_not_defined`(recorder 的统计任务周期是 5 min,`statistics_meta`
|
||||||
|
行由该任务建立),18:39 复核时已自动消失——建辅助元素后**不要**把这条
|
||||||
|
瞬时告警当作失败。
|
||||||
|
|
||||||
## Related docs
|
## Related docs
|
||||||
|
|
||||||
- [runbooks/home-assistant-maintenance.md](../runbooks/home-assistant-maintenance.md) — `ha` CLI maintenance runbook + [script](../runbooks/scripts/ha-maintenance.sh); custom-component zip install is §7
|
- [runbooks/home-assistant-maintenance.md](../runbooks/home-assistant-maintenance.md) — `ha` CLI maintenance runbook + [script](../runbooks/scripts/ha-maintenance.sh); custom-component zip install is §7
|
||||||
|
|||||||
@@ -185,6 +185,23 @@ dig @202.91.35.141 SOA wsvc.info +short
|
|||||||
|
|
||||||
On-server docs: `/opt/pdns/README.md`, `CHANGELOG.md`.
|
On-server docs: `/opt/pdns/README.md`, `CHANGELOG.md`.
|
||||||
|
|
||||||
|
## Disk / logging (VPS-81, 2026-09-02)
|
||||||
|
|
||||||
|
Root disk cleanup performed (runbook: [host-disk-cleanup](../runbooks/host-disk-cleanup.md)):
|
||||||
|
|
||||||
|
- Root `/` (20G vda1): 76% used → **38% used** (15G → 7.1G; free 4.7G → 12G).
|
||||||
|
- **AGH log flood root cause fixed**: `/opt/adguard/conf/AdGuardHome.yaml`
|
||||||
|
`log.verbose: true → false` (backup `AdGuardHome.yaml.bak-20260902-vps81`).
|
||||||
|
Verbose debug was streaming to stderr → container `json.log` (~120MB/day);
|
||||||
|
`log.file: ""` makes AGH's own rotation keys inert. Restart only (no recreate).
|
||||||
|
- Journald capped: `/etc/systemd/journald.conf.d/00-vps81.conf`
|
||||||
|
`SystemMaxUse=200M`; journal vacuumed to ~96M.
|
||||||
|
- Docker: engine **29.7.2**; 14 unused images removed (kept `pdns-auth-50:5.0.5`
|
||||||
|
rollback pin); 12 orphan anonymous volumes + build cache pruned. In-use
|
||||||
|
volumes intact (`pdns_dbdata`, `b594d738…` PG data, `e855d078…` backup).
|
||||||
|
- Follow-up: re-check AGH `json.log` growth **2026-09-09** (one-week checkpoint);
|
||||||
|
global docker log rotation only if still needed.
|
||||||
|
|
||||||
## Verified
|
## Verified
|
||||||
|
|
||||||
Last checked: **2026-08-01 21:40 CST** — operational; docs audit recorded.
|
Last checked: **2026-08-01 21:40 CST** — operational; docs audit recorded.
|
||||||
@@ -194,3 +211,4 @@ Last checked: **2026-08-01 21:40 CST** — operational; docs audit recorded.
|
|||||||
- `only-notify=` + `also-notify=202.91.35.141`; MASTER `domains.master` cleared
|
- `only-notify=` + `also-notify=202.91.35.141`; MASTER `domains.master` cleared
|
||||||
- https://pdns.wsvc.info → **302**; https://pgweb.wsvc.info → **401**
|
- https://pdns.wsvc.info → **302**; https://pgweb.wsvc.info → **401**
|
||||||
- Hardening backlog: API/DB credential rotation + TSIG rotate (see upstream doc)
|
- Hardening backlog: API/DB credential rotation + TSIG rotate (see upstream doc)
|
||||||
|
- 2026-09-02 (VPS-81): post-cleanup verified — 10 containers Up (adguardhome healthy), DNS SOA/NS + web endpoints OK; see Disk/logging section above.
|
||||||
|
|||||||
+10
-6
@@ -8,8 +8,8 @@
|
|||||||
| IPv4 | `192.168.55.15` (LAN55) |
|
| IPv4 | `192.168.55.15` (LAN55) |
|
||||||
| DNS | (none) |
|
| DNS | (none) |
|
||||||
| SSH | `ssh -4 windy@192.168.55.15`(key auth 已验证可用 2026-08-29;agent 沙箱用 `ssh -F /dev/null -o BatchMode=yes`;password auth 亦可) |
|
| SSH | `ssh -4 windy@192.168.55.15`(key auth 已验证可用 2026-08-29;agent 沙箱用 `ssh -F /dev/null -o BatchMode=yes`;password auth 亦可) |
|
||||||
| Host | PVE 管理的 QEMU VM(i440FX),Debian 13 (trixie),内核 6.12.105 |
|
| Host | PVE 管理的 QEMU VM(i440FX,**VMID 100**),Debian 13 (trixie),内核 6.12.105;宿主机 **pve2 `192.168.55.25`**(Proxmox 9.2.2,SSH `root@192.168.55.25`,`onboot: 1`,QEMU guest agent 已装;2026-08-31 补记) |
|
||||||
| Resources | 2 GB RAM / 30 GB disk(26 G 空闲) |
|
| Resources | 3 GB RAM(08-30 13:58 由 2G 上调、删除 balloon/ksm/shares 后重启生效)/ 30 GB disk(26 G 空闲) |
|
||||||
| Docker | 29.7.2;容器 `timescaledb` = `timescale/timescaledb:latest-pg18`(PG **18.6** + TimescaleDB **2.29.2**,Apache-2.0 版) |
|
| Docker | 29.7.2;容器 `timescaledb` = `timescale/timescaledb:latest-pg18`(PG **18.6** + TimescaleDB **2.29.2**,Apache-2.0 版) |
|
||||||
| Ports | `192.168.55.15:5432`(PG,IPv4 only);`192.168.55.15:8081`(pgweb GUI,basic auth) |
|
| Ports | `192.168.55.15:5432`(PG,IPv4 only);`192.168.55.15:8081`(pgweb GUI,basic auth) |
|
||||||
|
|
||||||
@@ -17,8 +17,8 @@
|
|||||||
|
|
||||||
| DB | Owner | Size | 用途 |
|
| DB | Owner | Size | 用途 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `hass` | hass | ~14 MB | HA recorder(states/events/statistics),客户端 HAOS `192.168.55.11` |
|
| `hass` | hass | ~603 MB(2026-09-21) | HA recorder(states/events/statistics),客户端 HAOS `192.168.55.11` |
|
||||||
| `scribe` | postgres | ~73 MB | HA scribe 集成(entities/areas/devices 注册表同步 + `states_raw` hypertable + `csg_history` 长期归档表) |
|
| `scribe` | postgres | ~2851 MB(2026-09-21) | HA scribe 集成(entities/areas/devices 注册表同步 + `states_raw`/`events` hypertable + `csg_history` 长期归档表);体积由 `sensor_minute` 图表管道主导(2.46 GB 未压缩 chunk),见 Known issues |
|
||||||
| `postgres` | postgres | ~9 MB | 默认库 |
|
| `postgres` | postgres | ~9 MB | 默认库 |
|
||||||
|
|
||||||
## Ops notes
|
## Ops notes
|
||||||
@@ -38,11 +38,14 @@
|
|||||||
- **开机自愈**(2026-08-30):新增 systemd oneshot `pgdb-compose.service`(enabled,源码在仓库 `compose/pgdb/pgdb-compose.service`):`After=network-online.target docker.service`,开机后幂等执行 `docker compose up -d`,重试直到 `192.168.55.15:5432` 监听,重试耗尽 `--force-recreate` 兜底(数据在 bind mount,无损)。原因:2026-08-30 开机竞态——docker 恢复容器时 VM IP 尚未可绑(EADDRNOTAVAIL),timescaledb/pgweb 启动失败且 docker 不重试。手动重跑:`sudo systemctl restart pgdb-compose.service`。
|
- **开机自愈**(2026-08-30):新增 systemd oneshot `pgdb-compose.service`(enabled,源码在仓库 `compose/pgdb/pgdb-compose.service`):`After=network-online.target docker.service`,开机后幂等执行 `docker compose up -d`,重试直到 `192.168.55.15:5432` 监听,重试耗尽 `--force-recreate` 兜底(数据在 bind mount,无损)。原因:2026-08-30 开机竞态——docker 恢复容器时 VM IP 尚未可绑(EADDRNOTAVAIL),timescaledb/pgweb 启动失败且 docker 不重试。手动重跑:`sudo systemctl restart pgdb-compose.service`。
|
||||||
- 本机无防火墙(ufw/nft/iptables 均未装)——待办:如要彻底隔离可加 ufw 白名单 192.168.55.11。
|
- 本机无防火墙(ufw/nft/iptables 均未装)——待办:如要彻底隔离可加 ufw 白名单 192.168.55.11。
|
||||||
- `/opt/database/backups/` 根下残留 `*-2026-08-29_1359.dump`(compose 化之前旧备份机制产物)与 `backup.log`——健康检查只看 `daily/`,残留可清理。
|
- `/opt/database/backups/` 根下残留 `*-2026-08-29_1359.dump`(compose 化之前旧备份机制产物)与 `backup.log`——健康检查只看 `daily/`,残留可清理。
|
||||||
- **Runbooks**:[pgdb-health](../runbooks/pgdb-health.md)(只读健康检查)、[pgdb-restore](../runbooks/pgdb-restore.md)(pg_restore 还原)、[pgdb-update](../runbooks/pgdb-update.md)(镜像/compose 升级)。
|
- **Runbooks**:[pgdb-health](../runbooks/pgdb-health.md)(只读健康检查,**9 项**;第 9 项为 CSG 归档新鲜度,2026-09-22 新增)、[pgdb-restore](../runbooks/pgdb-restore.md)(pg_restore 还原)、[pgdb-update](../runbooks/pgdb-update.md)(镜像/compose 升级)。
|
||||||
- **CSG 长期归档(2026-08-29, W1N-243)**:`csg_history` 表(`period date / kind('day'|'month') / usage_kwh / cost / ladder / balance / updated_at`,PK(period,kind),`GRANT SELECT TO hass`)保存南方电网有价值数据:day = 逐日(昨日用电/费用/阶梯/余额,2026-07-01 起),month = 当月累计(用电/费用,2025-01 起)。由 TimescaleDB 每日任务 **1008** `csg_daily_snapshot()`(22:30 Asia/Shanghai;**TS job 非 pg_cron**,本库未装 pg_cron)upsert 维护:取「最新有值行」防瞬态 unknown 竞态;日费用缺原生 `latest_day_cost` 时回退 = 昨日用电 × 当前档费率(模板 `csg_current_ladder_tariff` 0.639);月费用回退模板 `csg_this_month_ladder_cost`。验证:day 08-28 = 7.66 / 4.89474 / 二档 / 0,month 08 = 302.47 / 180.28。回填来源:集成 attributes `history_data`(59 天)+ `by_month`(19 月)——08-29 前唯一残存历史。回滚:`DROP TABLE csg_history` + `SELECT delete_job(1008)`。
|
- **CSG 长期归档(2026-08-29, W1N-243)**:`csg_history` 表(`period date / kind('day'|'month') / usage_kwh / cost / ladder / balance / updated_at`,PK(period,kind),`GRANT SELECT TO hass`)保存南方电网有价值数据:day = 逐日(昨日用电/费用/阶梯/余额,2026-07-01 起),month = 当月累计(用电/费用,2025-01 起)。由 TimescaleDB 每日任务 **1010**(2026-09-21 起;原 1008)`csg_daily_snapshot()`(**14:30 UTC = 22:30 Asia/Shanghai**;**TS job 非 pg_cron**,本库未装 pg_cron)upsert 维护;计费用 `csg_ladder_cost(kwh, month)` 助手(归档侧唯一计价来源)。回填来源:集成 attributes `history_data`(59 天)+ `by_month`(19 月)——08-29 前唯一残存历史。回滚:`DROP TABLE csg_history` + 删任务(另有迁移前快照表 `csg_history_bak_20260921`)。详见下方 Known issues 2026-09-21。**2026-09-22 v5 补两件事**:① **断档自愈** job **1011** `csg_backfill_missing_days()`(**15:10 UTC = 23:10 Asia/Shanghai**,排在 1010 之后)——只 `INSERT` 缺失日期、`ON CONFLICT DO NOTHING`,**绝不覆盖既有行**,数据源是 `states_raw` 里 `latest_day_kwh` 的 (`latest_day_date`, `value`) 观测对;② day 行费用改为 `csg_ladder_cost_raw` 的**累积边际差分 + 单次 `round(...,2)`**(与 v3 回填同口径),快照表 `csg_history_bak_20260922`,脚本 `compose/pgdb/csg-snapshot-v5.sql`。详见下方 Known issues 2026-09-22。
|
||||||
|
|
||||||
## Known issues
|
## Known issues
|
||||||
|
|
||||||
|
- 2026-09-22:**CSG day 费用口径统一 + 断档自愈(v5,脚本 `compose/pgdb/csg-snapshot-v5.sql`)**。只读复核发现 `csg_daily_snapshot()` 写 day 费用用的是「昨日用电 × 当前档费率」**单一费率且未舍入**,而 v3 的回填用「当月累积边际差分 + `round(...,2)`」——两套口径混在同一列:当时只差精度(3 行:`2026-08-28` 4.89474 / `09-19` 6.94431 / `09-20` 6.31408,总差 -0.01),但**一旦某个跨档日由夜间任务写出就会破坏「day 费用之和 ≈ 阶梯月费用」**;时限是 2026-09-29 前后跨二档(当时 `csg_current_ladder_remaining_kwh` = 80.6、日均 8.97)。**改动**:① 新增 `csg_ladder_cost_raw(kwh, month)`(无舍入版;直接用 `csg_ladder_cost(c1) − csg_ladder_cost(c0)` 会**二次舍入**,7 月实测 1 天差 0.01);② `csg_daily_snapshot()` v5 —— day 费用改为 `round(raw(c1) − raw(c0), 2)`,`p_month` 计算**上移到计费之前**并加**跨月守卫**(每月 1 日的 `d` 落在上月,此时 `m_usage` 是新月份累计,不可作起点,否则为负);③ 数据日期与当日用量改为取**同一实体同一行** `latest_day_kwh`(v3 用它的 `latest_day_date` 定日期、却用 `yesterday_kwh` 取值,两实体可能错配;且 `latest_day_kwh` 归档更全,含 09-06 = 9.29 那条 `yesterday_kwh` 没有的观测);④ 新增 job **1011** `csg_backfill_missing_days()`(**断档自愈**,insert-only)。**验证**:归一 `UPDATE 3`(与 dry-run 预测一致);`cost <> round(cost,2)` 的行数 0;`CALL run_job(1011)` 与调度器实跑均 `Success`(1/1/0、`job_errors` 0);函数签名为 `(job_id integer, config jsonb)`(即 job 1008 失败的根因形态,已显式排除);两函数手动调用幂等(总行数 103、`max(day period)` 仍 09-20);各月 `|sum(day cost) − month cost|` = 0.00 / -0.03 / -0.03(**逐日舍入累积,非缺陷**;月行由 `csg_ladder_cost` 单次舍入,是权威值)。回滚:`delete_job(1011)` + `DROP FUNCTION csg_backfill_missing_days/csg_ladder_cost_raw` + 用 v3 脚本重建主函数 + 从 `csg_history_bak_20260922` 恢复行。健康检查新增第 9 项(`runbooks/pgdb-health.md`),专查「最新 day 行日期」——**上游停更时 job 会反复 upsert 同一行、`last_run_status` 仍 `Success`、日期上也无缺口,只有该断言能发现**。
|
||||||
|
- 2026-09-21:**CSG 快照任务 1008 自创建起从未成功 + 已修复(迁移脚本 `compose/pgdb/csg-snapshot-v3.sql`)**。只读复核发现 `job_stats` 1008 = **294 次运行 / 0 成功 / 294 失败**,`job_errors` 与容器日志一致:`function or procedure "public.csg_daily_snapshot(integer, jsonb)" does not exist. Custom job actions must accept (integer, jsonb) arguments.` 根因:函数实际签名是零参数 `csg_daily_snapshot()`,而 TimescaleDB 自定义 job 动作按名字调用 `schema.proc(job_id integer, config jsonb)`;08-29 那批数据是**手工写入**的,任务本身没写过一行。后果:`csg_history` 的 day 行停在 08-28、month 行停在 08-01,且 2026-08 月行是 08-29 当时的「本月至今」**302.47 kWh / 180.28 元**,而非月终值 **331.22 / 198.65**(账单 198.64)。**修复**:① 不能直接 `DROP FUNCTION`(job 持有依赖,报 `cannot drop ... because background job 1008 depends on it`)→ `delete_job(1008)` → 换 `(job_id integer DEFAULT NULL, config jsonb DEFAULT NULL)` 签名重建 → `add_job` 重挂为 **1010**,排程从 22:00 UTC(=06:00 CST,那时集成还没发布前一日数据)对齐到 **14:30 UTC = 22:30 CST**;② 新增 `csg_ladder_cost(kwh, month)` 助手(夏季 5-10 月 260/600、非夏季 200/400;0.589/0.639/0.889),month 行费用一律由它重算;③ month 行改为**按数据日期 `d` 所属月份归属**——当月用集成本月累计,上月由该月 day 行汇总(不读集成 `last_month_*`,避开翻月瞬态:08-31 16:32 实测读到过 323.49 这种误值),并加「汇总值小于已记录值则不动」的不降级保护;④ 回填 08-29→09-19 共 22 个 day 行(08-29..08-31 取 `states_raw` 里 `yesterday_kwh` 观测值减一天;09-01..09-19 取集成 `this_month_by_day`,其中 09-06 = 9.29 是 scribe 漏采的观测),日费用按「当日用电 × 当日边际档位」计,使 day 费用之和 == 阶梯月费用。**验证**:job 1010 `last_run_status=Success`(1/1/0),`next_start = 2026-09-21 14:30 UTC`;day 行 81 条(07-01..09-19)、month 行 21 条(2025-01..2026-09);2026-08 = 331.22/198.65、2026-09 = 168.68/99.35;9 月 day 合计 168.68 kWh / 99.33 元(与阶梯 99.35 差 0.02 为逐日四舍五入)。迁移前快照表 `csg_history_bak_20260921`(79 行)保留;旧任务 1008 的 294 条 `job_errors` 作为历史保留(Timescale 的 error retention 任务会自行清理)。差异核对:day 新增 22 条、补 cost 80 条(仅填 NULL,未覆盖既有非空值),month 改 2 条。
|
||||||
|
- 2026-09-13:**`sensor_minute` 体积构成与压缩窗口(只读诊断,暂不处理)**。`scribe` 库 2.6 GB = `sensor_minute` **2.26 GB**(850 万行 / 16 天,约 56–59 万行/天 = 331 实体 × 1440 分钟 LOCF)+ `states_raw` 290 MB + `events` 1.5 MB;`hass` 库另 406 MB。2.26 GB 中 1.50 GB 是 chunk `[09-03,09-10]`、0.76 GB 是 `[09-10,09-17]`,**都还没到压缩窗口**——TimescaleDB 的 `compress_after` 按 **chunk 结束时间**判断(09-10 结束 + 7 天 = **09-17** 才合格),所以「7 天 chunk + 7 天 compress_after」的设计下限就是盘上常驻近 14 天原始数据;已压缩的 `[08-27,09-03]` 从 1.04 GB → **1.5 MB**(LOCF 重复度极高,~700:1)。任务 1005 健康(30 成功 / 0 失败)。**2026-09-21 复查已通过**:`_hyper_4_6_chunk`(09-03→09-10)`is_compressed=t`(32 kB),`_hyper_4_10_chunk`(09-10→09-17,1557 MB)按 09-24 合格待压,稳态平台期估算不变(≈4–5 GB);pgdata 实测 3.6 G / 32 G(13%),**无需处理**。可选调优:chunk 间隔 7 天 → 1 天 + `compress_after` → 2 天(`set_chunk_time_interval` 只对新 chunk 生效)。**注意:这是 pgdb 侧对象,HA/scribe 的 `retention_states` 管不到它;HA 侧唯一杠杆是少记/少画(等于砍图)。**
|
||||||
- 2026-08-29:HA 侧 HACS 集成 `custom_components.scribe`(YAML `scribe: db_url:`,连 `scribe` 库)建表被拒(`permission denied for schema public`,hass 无 CREATE 权限),之后持续报 `relation "entities" does not exist`。**已解决**:① `GRANT CREATE ON SCHEMA public TO hass;`(scribe 库)② 重启 HA Core 触发重跑建表。重启后自动创建 `entities`(1591 行)/`users`/`areas`/`devices`/`integrations`/`states_raw` 表并启用 TimescaleDB 时间序列能力。报错已停止(最后一条 06:06 UTC),`states_raw` 持续写入。2026-08-29 复查:scribe 现有**两个** hypertable——`states_raw`(segmentby `metadata_id`、orderby `time`)与 `events`(segmentby `event_type`、orderby `time`),均 1 维 `time`;压缩已配置(`timescaledb_information.compression_settings` 可见对应行;2.29.x 该视图无 `compression_enabled` 列)。
|
- 2026-08-29:HA 侧 HACS 集成 `custom_components.scribe`(YAML `scribe: db_url:`,连 `scribe` 库)建表被拒(`permission denied for schema public`,hass 无 CREATE 权限),之后持续报 `relation "entities" does not exist`。**已解决**:① `GRANT CREATE ON SCHEMA public TO hass;`(scribe 库)② 重启 HA Core 触发重跑建表。重启后自动创建 `entities`(1591 行)/`users`/`areas`/`devices`/`integrations`/`states_raw` 表并启用 TimescaleDB 时间序列能力。报错已停止(最后一条 06:06 UTC),`states_raw` 持续写入。2026-08-29 复查:scribe 现有**两个** hypertable——`states_raw`(segmentby `metadata_id`、orderby `time`)与 `events`(segmentby `event_type`、orderby `time`),均 1 维 `time`;压缩已配置(`timescaledb_information.compression_settings` 可见对应行;2.29.x 该视图无 `compression_enabled` 列)。
|
||||||
- 2026-08-29:**timescale reader 图表对象**(配套 hass 的 `timescale_database_reader` 集成 + `timescale-plotly-card`,上游 SQL `remmob/timescale_database_reader` `SQL/scribe/01+02` @ `bb8776a`,以 postgres 执行):`sensor_minute_aggregate` 连续聚合(1 分钟桶,last(state)/last(value),实时聚合开启)+ `sensor_minute_aggregate_entity` 视图(join `entities`)+ `sensor_minute` hypertable(`minute`/`entity_id`/`state`/`value`,LOCF 前向填充)。任务:1005 `sensor_minute` 压缩(7 天)、1006 `sensor_minute` 保留(10 年)、1007 `every_minute_refresh` 每分钟增量刷新(含 5 分钟回溯窗口修正)。授权:`GRANT SELECT ON sensor_minute_aggregate, sensor_minute_aggregate_entity, sensor_minute, entities TO hass`。种子 19529 行(331 实体,自首个数据点起)。**刻意跳过**了上游脚本对 `states_raw` 的 3 个月保留 + 压缩策略语句——与"`states_raw` 永久归档"定位冲突,如需磁盘回收属用户决策(scribe 自己的压缩任务 1000/1001 未动)。
|
- 2026-08-29:**timescale reader 图表对象**(配套 hass 的 `timescale_database_reader` 集成 + `timescale-plotly-card`,上游 SQL `remmob/timescale_database_reader` `SQL/scribe/01+02` @ `bb8776a`,以 postgres 执行):`sensor_minute_aggregate` 连续聚合(1 分钟桶,last(state)/last(value),实时聚合开启)+ `sensor_minute_aggregate_entity` 视图(join `entities`)+ `sensor_minute` hypertable(`minute`/`entity_id`/`state`/`value`,LOCF 前向填充)。任务:1005 `sensor_minute` 压缩(7 天)、1006 `sensor_minute` 保留(10 年)、1007 `every_minute_refresh` 每分钟增量刷新(含 5 分钟回溯窗口修正)。授权:`GRANT SELECT ON sensor_minute_aggregate, sensor_minute_aggregate_entity, sensor_minute, entities TO hass`。种子 19529 行(331 实体,自首个数据点起)。**刻意跳过**了上游脚本对 `states_raw` 的 3 个月保留 + 压缩策略语句——与"`states_raw` 永久归档"定位冲突,如需磁盘回收属用户决策(scribe 自己的压缩任务 1000/1001 未动)。
|
||||||
- 2026-08-29:**`sensor_minute_refresh` 本地补丁(类比 tianqi 补丁,重跑上游 02 SQL 后需重打)**:值 CASE 的 `ELSE 0` → `ELSE NULL`。原因:scribe 对 unavailable 分钟 value 为 NULL,上游刷新过程兜底写 0;对差分模式的用电图,0→计数器回升会把插座的**生命周期累计值**(最高 1588 kWh)算进掉线那一小时。同日一次性清理既有脏 0:头部占位行 DELETE 505 行(各实体首次非零分钟之前的 value=0);`sensor.%_energy` 与温湿度实体的 value=0 → NULL(10+16 行,物理上不可能的真 0,图表渲染为断点)。功率实体的中途 0 是真实待机读数,保留。
|
- 2026-08-29:**`sensor_minute_refresh` 本地补丁(类比 tianqi 补丁,重跑上游 02 SQL 后需重打)**:值 CASE 的 `ELSE 0` → `ELSE NULL`。原因:scribe 对 unavailable 分钟 value 为 NULL,上游刷新过程兜底写 0;对差分模式的用电图,0→计数器回升会把插座的**生命周期累计值**(最高 1588 kWh)算进掉线那一小时。同日一次性清理既有脏 0:头部占位行 DELETE 505 行(各实体首次非零分钟之前的 value=0);`sensor.%_energy` 与温湿度实体的 value=0 → NULL(10+16 行,物理上不可能的真 0,图表渲染为断点)。功率实体的中途 0 是真实待机读数,保留。
|
||||||
@@ -50,6 +53,7 @@
|
|||||||
|
|
||||||
## Verification history
|
## Verification history
|
||||||
|
|
||||||
|
- 2026-08-31:**13:58 重启根因确认,非停电**(W1N-263):pve2(`192.168.55.25`)任务日志显示 08-30 **13:58:00 `root@pam` 在 PVE Web UI 修改 VM 100 配置**(`-delete allow-ksm,balloon,shares -memory 3072`),**13:58:06 点 Reboot**(`qmreboot` → 客机 13:58:08 干净 ACPI 关机 → 13:58:13 自动重启)。宿主机全程在线(08-30 09:00 开机至今连续运行 1d12h+),`.66.26` PVE 及各 VM 均无重启——排除停电。HA recorder 在窗口(13:58:46–47)报 2 次 `Connection refused`,DB 恢复后自动重连,**无数据丢失**(`hass.states`/`scribe.states_raw` 13:55–14:02 逐分钟无缺口,recorder 内存队列吸收回写)。13:58:47 三容器已起,13:58:56 自愈单元 `pgdb-compose.service` 执行成功——本次自愈按设计工作。同日下午 12:54–12:55 另有一次**客机内自重启**(无 PVE 任务,工作站 SSH 会话相邻)。08-29 22:19→08-30 09:00 宿主机停机 10h41m 为**干净关机**(systemd 有序关闭,非停电)。
|
||||||
- 2026-08-30:**开机竞态故障 + 修复**(W1N-260):09:01 开机后 docker 恢复容器时绑定 `192.168.55.15:5432/8081` 失败(EADDRNOTAVAIL)→ timescaledb/pgweb 停摆至 12:16,pg-backup 开机备份失败(解析不到 timescaledb)→ unhealthy。12:22 `docker compose up -d --force-recreate` 修复(三容器回 `database_default`、端口发布、今日备份、pgweb 恢复);用户重启 HA Core 后写入管道恢复。12:43 新增开机自愈 unit `pgdb-compose.service`(enabled,已实测幂等 reconcile)。pgdb-health 8 项全绿。
|
- 2026-08-30:**开机竞态故障 + 修复**(W1N-260):09:01 开机后 docker 恢复容器时绑定 `192.168.55.15:5432/8081` 失败(EADDRNOTAVAIL)→ timescaledb/pgweb 停摆至 12:16,pg-backup 开机备份失败(解析不到 timescaledb)→ unhealthy。12:22 `docker compose up -d --force-recreate` 修复(三容器回 `database_default`、端口发布、今日备份、pgweb 恢复);用户重启 HA Core 后写入管道恢复。12:43 新增开机自愈 unit `pgdb-compose.service`(enabled,已实测幂等 reconcile)。pgdb-health 8 项全绿。
|
||||||
- 2026-08-29:首次检查(只读)+ 修复 scribe 权限 + 安装夜间备份。见 Linear vps 项目登记。
|
- 2026-08-29:首次检查(只读)+ 修复 scribe 权限 + 安装夜间备份。见 Linear vps 项目登记。
|
||||||
- 2026-08-29:**compose 改造完成**(W1N-227,用户已验收):裸 `docker run` → `/opt/database/docker-compose.yml` 三服务(timescaledb + pgweb + pg-backup);superuser 换强密码;端口收紧 IPv4;备份容器化(TZ=Asia/Shanghai,cron 02:00 本地);`pg_restore` 还原实测通过;pgweb UI 用户确认可查 hass/scribe 数据。源码在仓库 `compose/pgdb/`。
|
- 2026-08-29:**compose 改造完成**(W1N-227,用户已验收):裸 `docker run` → `/opt/database/docker-compose.yml` 三服务(timescaledb + pgweb + pg-backup);superuser 换强密码;端口收紧 IPv4;备份容器化(TZ=Asia/Shanghai,cron 02:00 本地);`pg_restore` 还原实测通过;pgweb UI 用户确认可查 hass/scribe 数据。源码在仓库 `compose/pgdb/`。
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ ssh -4 windy@synapse.chans.xyz
|
|||||||
| DB | ESS embedded PostgreSQL 17 (PVC 20Gi, local-path) |
|
| DB | ESS embedded PostgreSQL 17 (PVC 20Gi, local-path) |
|
||||||
| Cache | ESS embedded Redis (PVC 2Gi) |
|
| Cache | ESS embedded Redis (PVC 2Gi) |
|
||||||
| Chart | `oci://ghcr.io/element-hq/ess-helm/matrix-stack`, version `26.7.2` |
|
| Chart | `oci://ghcr.io/element-hq/ess-helm/matrix-stack`, version `26.7.2` |
|
||||||
|
| Plane | Helm `plane-ce-1.8.0` (app `v1.4.1`), namespace `plane` — self-hosted Plane project management |
|
||||||
|
|
||||||
### Matrix service endpoints
|
### Matrix service endpoints
|
||||||
|
|
||||||
@@ -51,8 +52,50 @@ All other ports internal only (no K3s API, no database, no Redis exposed).
|
|||||||
|
|
||||||
- `ess` — all ESS workloads (Synapse, MAS, Element, Postgres, Redis, HAProxy)
|
- `ess` — all ESS workloads (Synapse, MAS, Element, Postgres, Redis, HAProxy)
|
||||||
- `matrix-system` — cluster base resources (ResourceQuota, LimitRange, mrtc-placeholder)
|
- `matrix-system` — cluster base resources (ResourceQuota, LimitRange, mrtc-placeholder)
|
||||||
|
- `plane` — Plane project management (Helm release `plane-app`)
|
||||||
- `cert-manager` — cert-manager
|
- `cert-manager` — cert-manager
|
||||||
|
|
||||||
|
## Plane (project management)
|
||||||
|
|
||||||
|
Self-hosted [Plane](https://github.com/makeplane/plane) on the same K3s node, deployed via the official `plane-ce` Helm chart.
|
||||||
|
|
||||||
|
| Item | Detail |
|
||||||
|
|------|--------|
|
||||||
|
| Release | `plane-app` (ns `plane`), chart `plane-ce-1.8.0`, app `v1.4.1`, revision 1 |
|
||||||
|
| URL | https://plane.chans.xyz |
|
||||||
|
| Install date | 2026-09-01 |
|
||||||
|
| Values source | `/home/windy/plane-k3s/values.yaml` (plain file, not a git repo) |
|
||||||
|
| Images | `artifacts.plane.so/makeplane/*` (`plane-frontend`, `plane-backend`, `plane-admin`, `plane-live`), pullPolicy `Always` |
|
||||||
|
| Ingress | Traefik `IngressRoute` `plane-app-ingress` — `/`→web, `/api` `/auth`→api, `/spaces`→space, `/god-mode`→admin, `/live`→live, `/uploads`→minio; `maxRequestBodyBytes` 20Mi |
|
||||||
|
| TLS | Own namespace `Issuer` `plane-app-cert-issuer` (HTTP-01, LE prod, `admin@chans.xyz`); cert `plane-app-ssl-cert` (CN `plane.chans.xyz`) |
|
||||||
|
| DB | Bundled Postgres `15.7-alpine` (PVC 5Gi, local-path) |
|
||||||
|
| Cache/queue | Bundled Redis (PVC 100Mi), RabbitMQ `3.13.6-management-alpine` (PVC 100Mi) |
|
||||||
|
| Storage | Bundled MinIO (`minio/minio:latest`, root user `admin`, PVC 5Gi) — S3 for uploads/docs |
|
||||||
|
| Resources | Every workload: cpu 50m/500m, mem 50Mi/1000Mi, replicas 1 |
|
||||||
|
| SMTP | Not configured (no `smtp` values) — Plane invites/password resets won't email yet |
|
||||||
|
|
||||||
|
Workloads (all 1/1 Running): 7 Deployments (`plane-app-{admin,api,beat-worker,live,space,web,worker}-wl`) + 4 StatefulSets (`plane-app-{minio,pgdb,rabbitmq,redis}-wl`); init Jobs `api-migrate-1` / `minio-bucket-1` Completed. All PVCs Bound on `local-path` (root disk).
|
||||||
|
|
||||||
|
### Plane configuration notes
|
||||||
|
|
||||||
|
- **`planeVersion: v1.4.1`** pinned in values.yaml; chart tracks Plane's own tags.
|
||||||
|
- **Secrets**: Helm-generated Opaque secrets (`plane-app-app-secrets`, `-doc-store-secrets`, `-pgdb-secrets`, `-rabbitmq-secrets`, `-live-secrets`); `requireExplicitSecrets: false`. Values live in `$SECRET_KEY`, `DATABASE_URL`, `AMQP_URL`, `REDIS_URL` etc.
|
||||||
|
- **Sentry / CORS**: `sentry_dsn` and `cors_allowed_origins` empty (defaults fine for single-host).
|
||||||
|
- **MinIO is `latest` tag** — pin a version for reproducibility.
|
||||||
|
- **Backup**: NOT covered by `/var/backups/matrix` (which is paused anyway) — Plane Postgres/MinIO PVCs have no backup tier yet.
|
||||||
|
|
||||||
|
### Plane verification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Release + workloads
|
||||||
|
sudo helm list -A
|
||||||
|
sudo k3s kubectl -n plane get deploy,sts,pods -o wide
|
||||||
|
# Cert + ingress
|
||||||
|
sudo k3s kubectl -n plane get certificate,ingressroute
|
||||||
|
# Endpoint
|
||||||
|
curl -4 -s -o /dev/null -w '%{http_code}\n' https://plane.chans.xyz/
|
||||||
|
```
|
||||||
|
|
||||||
## Local backup
|
## Local backup
|
||||||
|
|
||||||
| Item | Detail |
|
| Item | Detail |
|
||||||
@@ -62,7 +105,7 @@ All other ports internal only (no K3s API, no database, no Redis exposed).
|
|||||||
| Retention | 7 days |
|
| Retention | 7 days |
|
||||||
| Disk warning | 80% (healthcheck), 90% (backup stops) |
|
| Disk warning | 80% (healthcheck), 90% (backup stops) |
|
||||||
| Content | Planned: PostgreSQL `synapse` + `mas` logical dumps, media store archive, `/etc/matrix-bootstrap` |
|
| Content | Planned: PostgreSQL `synapse` + `mas` logical dumps, media store archive, `/etc/matrix-bootstrap` |
|
||||||
| Status | **Not operational** — no current Matrix backup or recovery tier |
|
| Status | **Not operational** — no current Matrix backup or recovery tier. **Plane data (its own Postgres + MinIO PVCs in ns `plane`) is also not covered by any backup.** |
|
||||||
|
|
||||||
## Health checks
|
## Health checks
|
||||||
|
|
||||||
@@ -101,5 +144,6 @@ diagnosis and imperative recovery work.
|
|||||||
- MatrixRTC / Element Call / LiveKit / Coturn not deployed (`mrtc.chans.xyz` reserved only)
|
- MatrixRTC / Element Call / LiveKit / Coturn not deployed (`mrtc.chans.xyz` reserved only)
|
||||||
- SMTP email not yet configured (requires manual secret bootstrap followed by a
|
- SMTP email not yet configured (requires manual secret bootstrap followed by a
|
||||||
reviewed Ansible stack deployment)
|
reviewed Ansible stack deployment)
|
||||||
|
- Plane `minio` image uses `latest` tag (pin a version)
|
||||||
- No off-site Restic backup
|
- No off-site Restic backup
|
||||||
- Single-node K3s (no HA for control plane)
|
- Single-node K3s (no HA for control plane)
|
||||||
|
|||||||
+18
-13
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
| Item | Value |
|
| Item | Value |
|
||||||
|------|--------|
|
|------|--------|
|
||||||
| Role | Multi-service VPS (Vaultwarden, Traefik, Soft Serve, …) |
|
| Role | Multi-service VPS (Vaultwarden, Traefik, Gitea, …) |
|
||||||
| SSH | `ssh -4 windy@us2.wsvc.info` (prefer IPv4 from WSL) |
|
| SSH | `ssh -4 windy@us2.wsvc.info` (prefer IPv4 from WSL) |
|
||||||
| IPv4 | `193.9.44.165` |
|
| IPv4 | `193.9.44.165` |
|
||||||
| Also DNS | `auth.wsvc.info` → this host; `repo.windy.me` → this host (Soft Serve) |
|
| Also DNS | `auth.wsvc.info` → this host; `repo.windy.me` → this host (Gitea) |
|
||||||
| Public HTTPS | Traefik on `:80` / `:443` (`/opt/traefik`) |
|
| Public HTTPS | Traefik on `:80` / `:443` (`/opt/traefik`) |
|
||||||
|
|
||||||
## Vaultwarden (Bitwarden-compatible)
|
## Vaultwarden (Bitwarden-compatible)
|
||||||
@@ -80,24 +80,29 @@ ansible-playbook playbooks/compose-reconcile.yml --limit vaultwarden \
|
|||||||
|
|
||||||
| Container | Status | Image / notes |
|
| Container | Status | Image / notes |
|
||||||
|-----------|--------|---------------|
|
|-----------|--------|---------------|
|
||||||
| `soft-serve` | Up | `charmcli/soft-serve:v0.12.2` (`repo.windy.me:2222`) |
|
| `gitea` | Up | `gitea/gitea@sha256:1c17ecaead42e…` (1.27.3-rootless) — SSH `repo.windy.me:2222`, web `https://repo.windy.me` |
|
||||||
| `soft-serve-backup` | Up | alpine + sqlite3 sidecar (daily backup 02:00 / prune 03:00, crond) |
|
| `gitea-backup` | Up | alpine + sqlite3/rsync sidecar (daily backup 02:00 / prune 03:00, crond) |
|
||||||
|
|
||||||
Soft Serve details (verified/updated 2026-08-30; 核查 [W1N-244](https://linear.app/w1ndy/issue/W1N-244), 修复 [W1N-245](https://linear.app/w1ndy/issue/W1N-245)/[W1N-246](https://linear.app/w1ndy/issue/W1N-246)/[W1N-247](https://linear.app/w1ndy/issue/W1N-247)):
|
### Gitea (replaced Soft Serve 2026-09-18; [Plane VPS-94](https://plane.chans.xyz))
|
||||||
- `/opt/soft-serve/compose.yml` (+ `Dockerfile.backup`, `scripts/`, `backups/`); data `/opt/soft-serve/data` → `/var/lib/soft-serve` (sqlite `soft-serve.db`, 12 repos + `windyboy`); env `.env` (`SOFT_SERVE_INITIAL_ADMIN_KEYS` = admin pubkey, first-boot only). 仓库镜像: `compose/soft-serve/`(参考, 服务器文件为准)
|
|
||||||
- No host ports published: Traefik TCP entrypoint `ssh` (`:2222` → `soft-serve:23231`, `HostSNI(*)`, `tls=false`) on `vw-net`. Container listens 23231 SSH / 23232 HTTP (git smart-http, no web UI) / 9418 git / 23233 stats (localhost only)
|
- `/opt/gitea/compose.yml` (+ `Dockerfile.backup`, `scripts/`, `config/app.ini`, `data/`, `secrets/`, `backups/`); 镜像: `compose/gitea/`(参考, 服务器文件为准)
|
||||||
- **镜像已固定** `charmcli/soft-serve:v0.12.2`(digest `sha256:554cc770…`, Docker Hub 稳定源; GHCR 为 dev/nightly 源且无 v0.12.x tag); `/soft-serve` 由 named volume `soft-serve_soft-serve-app` 承载, 旧匿名卷已清理(2026-08-30)
|
- **rootless 镜像** uid 1000:1000; SQLite `/opt/gitea/data/data/gitea.db`; repos `/opt/gitea/data/data/git/repositories/`; app.ini `/opt/gitea/config/app.ini`(600, 含 SECRET_KEY)
|
||||||
- **非 root 运行**(2026-08-30, [W1N-248](https://linear.app/w1ndy/issue/W1N-248)): compose `user: "1000:1000"`(uid=windy, 镜像无内置用户); data 全量 `chown 1000:1000`, 容器内 `id` = uid 1000, 功能验证通过
|
- SSH: 内置 server 容器内 `:2322`(`SSH_LISTEN_PORT` 非特权), Traefik TCP entrypoint `ssh`(`:2222` → `gitea:2322`, `HostSNI(*)`, `tls=false`) on `vw-net`; clone URL `ssh://git@repo.windy.me:2222/windy/<repo>.git`(owner 段 `windy`)
|
||||||
- **`ssh.public_url` 已修复**(2026-08-30): `config.yaml` → `ssh://git@repo.windy.me:2222`; 失效 env `SOFT_SERVE_SSH_PUBLIC_URL` 已删(v0.12 不读取); `http`/`git` public_url 保持 `localhost`(未对外暴露)
|
- **host key 复用 soft-serve**(`SSH_SERVER_HOST_KEYS=/secrets/soft_serve_host_ed25519`, ed25519, 指纹 `SHA256:PdxZRe74…`): 客户端 known_hosts 零变更; 仅公钥认证(密码认证未启用)
|
||||||
- **备份**: sidecar `soft-serve-backup` 每日 02:00 → `backups/soft-serve_<TS>/{repos-config.tar.gz, soft-serve.db}`(db 用 `sqlite3 .backup` 在线快照), 03:00 prune 保留 14 份; 产物 chown windy:windy 600(含 `ssh/` host keys)。**恢复**: `docker compose stop soft-serve` → 解包 `repos-config.tar.gz` + 放回 `soft-serve.db` 到 `data/` → `docker compose up -d`。异地副本(hk2/WSL 每日拉取)= follow-up(见 W1N-247)
|
- Web: `https://repo.windy.me`(Traefik websecure + letsencrypt); `DISABLE_REGISTRATION=true`, Actions 关闭; 管理员 `windy`(凭据仅存服务器 `/opt/gitea/.admin-credentials`, 勿入库/入 Plane)
|
||||||
|
- 仓库: 17 个(`windy/` 13 — 含 vps、自 `windyboy` 并入的 go-caatsm — + `cdia/` 4; 2026-09-19 复核), 2026-09-18 自 soft-serve `push --mirror` 迁移, 逐仓 `ls-remote` ref 全集 + HEAD symref 两端一致; 可见性仅 `dotfiles-personal` private, 其余 public(与 soft-serve 现状一致)
|
||||||
|
- 备份: sidecar 每日 02:00 → `backups/gitea_<TS>/{app.ini.tar.gz, gitea.db, repos.tar.gz}`(app.ini 含恢复必需 SECRET_KEY), 03:00 prune 保留 14 份; 已验证手动备份产物 109.9M
|
||||||
|
- 回滚: `/opt/soft-serve` 未删(compose stop + sidecar 停, 数据与旧备份冻结保留), 回滚 = Traefik `:2222` 指回 `soft-serve:23231` + 客户端 remote 回改旧无 owner 段路径; 观察 2–4 周后清理(历史: W1N-244~248)
|
||||||
| `traefik` | Up | `traefik:v3.6.2` (`/opt/traefik`, public `:80`/`:443`) |
|
| `traefik` | Up | `traefik:v3.6.2` (`/opt/traefik`, public `:80`/`:443`) |
|
||||||
| `nghttpx-proxy` + `squid-backend` | Up | HTTP forward-proxy stack (`/opt/nghttpx`), network `nghttpx_internal-net`; details TBD |
|
| `nghttpx-proxy` + `squid-backend` | Up | HTTP forward-proxy stack (`/opt/nghttpx`), network `nghttpx_internal-net`; details TBD |
|
||||||
|
|
||||||
Directories for `authelia`, `conduit`, `dendrite`, `mastodon`, `rustdesk`, `zitadel`, etc. exist under `/opt` but have no running containers; treat them as dormant, not documented services.
|
**Disk cleanup 2026-09-18** ([Plane vps VPS-93](https://plane.chans.xyz)): root 71% → **23%** (~33G freed) keeping soft-serve / vaultwarden / traefik (nghttpx kept running per operator choice). Removed: unused Docker images + orphan volumes (incl. `zitadel_data` 801M), dormant `/opt` dirs (dendrite + its disabled `dendrite.service` unit, mastodon, dailysync, keycloak, media-repo, authelia, conduit, npm, manager, fusion, zitadel, rustdesk), rootless podman storage (6.4G stale goauthentik), home dev caches, apt cache, journal 3.8G→162M (+`SystemMaxUse=200M` drop-in, active next boot), truncated container logs (nghttpx 550M / traefik / squid). Follow-up: nghttpx-proxy logs grow ~25M/day (INFO per-connection); root-cause log-level/rotation fix still open (needs container restart approval).
|
||||||
|
|
||||||
|
Remaining running services on this host: `gitea`, `vaultwarden` stack, `traefik`, `nghttpx-proxy` + `squid-backend` (undocumented forward proxy, `/opt/nghttpx`). `/opt/soft-serve` kept stopped as rollback (2–4 weeks, data intact). `/home/windy/authelia` (76M) left in place — outside approved cleanup scope.
|
||||||
|
|
||||||
## Verified
|
## Verified
|
||||||
|
|
||||||
Last checked: **2026-08-29** — operational.
|
Last checked: **2026-09-19** — operational. Gitea independently audited (read-only): `1.27.3` = latest 1.27.x patch; HTTPS 200 (LE cert → 2026-12-17); SSH `git@…:2222` key auth OK; backup 02:00 OK (110M); memory available 584 MB; logs clean (record: Plane VPS-94 comment 2026-09-19). Prior: 2026-09-18 migration + disk cleanup (VPS-93); 2026-08-29 full Vaultwarden probe.
|
||||||
|
|
||||||
- `vaultwarden` + `vw-db` healthy; `DATABASE_URL` → `pg:5432/vaultwarden`
|
- `vaultwarden` + `vw-db` healthy; `DATABASE_URL` → `pg:5432/vaultwarden`
|
||||||
- `https://auth.wsvc.info/` **200**, `/admin` **200**, `/api/config` OK (`disableUserRegistration: true`)
|
- `https://auth.wsvc.info/` **200**, `/admin` **200**, `/api/config` OK (`disableUserRegistration: true`)
|
||||||
|
|||||||
+3
-2
@@ -15,9 +15,9 @@ the software deployed there, see [the LAN overview](../docs/lan-overview.md).
|
|||||||
| Host | Role | SSH | IPv4 | Ansible | Status | Facts |
|
| Host | Role | SSH | IPv4 | Ansible | Status | Facts |
|
||||||
|------|------|-----|------|---------|--------|-------|
|
|------|------|-----|------|---------|--------|-------|
|
||||||
| mx2.windy.me | mailcow (primary MX prio 20) | `ssh -4 windy@mx2.windy.me` | 194.163.160.244 | ✓ (mx2) | active | [hosts/mx2.windy.me.md](../hosts/mx2.windy.me.md) |
|
| mx2.windy.me | mailcow (primary MX prio 20) | `ssh -4 windy@mx2.windy.me` | 194.163.160.244 | ✓ (mx2) | active | [hosts/mx2.windy.me.md](../hosts/mx2.windy.me.md) |
|
||||||
| us2.wsvc.info | Vaultwarden/Postgres (+ Traefik, Soft Serve, …) | `ssh -4 windy@us2.wsvc.info` | 193.9.44.165 | ✓ (us2) | active | [hosts/us2.wsvc.info.md](../hosts/us2.wsvc.info.md) |
|
| us2.wsvc.info | Vaultwarden/Postgres (+ Traefik, Gitea, …) | `ssh -4 windy@us2.wsvc.info` | 193.9.44.165 | ✓ (us2) | active | [hosts/us2.wsvc.info.md](../hosts/us2.wsvc.info.md) |
|
||||||
| mx.windy.me | mail (secondary MX prio 30) | TBD | see AAAA/A | — (stub) | stub | — |
|
| mx.windy.me | mail (secondary MX prio 30) | TBD | see AAAA/A | — (stub) | stub | — |
|
||||||
| repo.windy.me | Soft Serve git (on us2) | `ssh -p 2222 windy@repo.windy.me` | 193.9.44.165 | — (service on us2) | stub | see us2 |
|
| repo.windy.me | Gitea git (on us2) | `ssh -p 2222 git@repo.windy.me` | 193.9.44.165 | — (service on us2) | stub | see us2 |
|
||||||
| auth.wsvc.info | Vaultwarden public hostname | — (HTTPS) | → us2 | — (alias) | active | see us2 |
|
| auth.wsvc.info | Vaultwarden public hostname | — (HTTPS) | → us2 | — (alias) | active | see us2 |
|
||||||
| us1.wsvc.info | PowerDNS secondary (ns2 host) | TBD | 202.91.35.141 | — (stub) | stub | Auth 5.0.5; see hk2 |
|
| us1.wsvc.info | PowerDNS secondary (ns2 host) | TBD | 202.91.35.141 | — (stub) | stub | Auth 5.0.5; see hk2 |
|
||||||
| us4.wsvc.info | WireGuard VPN | `ssh -4 windy@us4.wsvc.info` | 185.201.226.122 | ✓ (us4) | active | [hosts/us4.wsvc.info.md](../hosts/us4.wsvc.info.md) |
|
| us4.wsvc.info | WireGuard VPN | `ssh -4 windy@us4.wsvc.info` | 185.201.226.122 | ✓ (us4) | active | [hosts/us4.wsvc.info.md](../hosts/us4.wsvc.info.md) |
|
||||||
@@ -48,4 +48,5 @@ the software deployed there, see [the LAN overview](../docs/lan-overview.md).
|
|||||||
| https://synapse.chans.xyz | Synapse API | Client-Server + Federation API |
|
| https://synapse.chans.xyz | Synapse API | Client-Server + Federation API |
|
||||||
| https://account.chans.xyz | MAS | Matrix Authentication Service (local passwords) |
|
| https://account.chans.xyz | MAS | Matrix Authentication Service (local passwords) |
|
||||||
| https://admin.chans.xyz | Element Admin | Admin console (MAS admin auth) |
|
| https://admin.chans.xyz | Element Admin | Admin console (MAS admin auth) |
|
||||||
|
| https://plane.chans.xyz | Plane | Project management (Helm `plane-ce` v1.4.1, ns `plane`) |
|
||||||
| `mrtc.chans.xyz` | MatrixRTC | **Reserved** – not deployed |
|
| `mrtc.chans.xyz` | MatrixRTC | **Reserved** – not deployed |
|
||||||
|
|||||||
@@ -15,11 +15,14 @@ runbooks start from [`_template.md`](_template.md).
|
|||||||
| Vaultwarden SQLite→PG migrate | [vaultwarden-sqlite-to-postgres.md](vaultwarden-sqlite-to-postgres.md) | change (destructive) |
|
| Vaultwarden SQLite→PG migrate | [vaultwarden-sqlite-to-postgres.md](vaultwarden-sqlite-to-postgres.md) | change (destructive) |
|
||||||
| PowerDNS health check | [pdns-health.md](pdns-health.md) | read-only |
|
| PowerDNS health check | [pdns-health.md](pdns-health.md) | read-only |
|
||||||
| RustDesk health check | [rustdesk-health.md](rustdesk-health.md) | read-only |
|
| RustDesk health check | [rustdesk-health.md](rustdesk-health.md) | read-only |
|
||||||
|
| Gitea health check | [gitea-health.md](gitea-health.md) | read-only |
|
||||||
| Matrix health check | [matrix-health.md](matrix-health.md) | read-only |
|
| Matrix health check | [matrix-health.md](matrix-health.md) | read-only |
|
||||||
|
| Plane health check | [plane-health.md](plane-health.md) | read-only |
|
||||||
| pgdb health check | [pgdb-health.md](pgdb-health.md) | read-only |
|
| pgdb health check | [pgdb-health.md](pgdb-health.md) | read-only |
|
||||||
| pgdb DB restore (pg_restore) | [pgdb-restore.md](pgdb-restore.md) | change (procedure) |
|
| pgdb DB restore (pg_restore) | [pgdb-restore.md](pgdb-restore.md) | change (procedure) |
|
||||||
| pgdb image/compose update | [pgdb-update.md](pgdb-update.md) | change (gated) |
|
| pgdb image/compose update | [pgdb-update.md](pgdb-update.md) | change (gated) |
|
||||||
| AdGuard Home health check | [adguard-home-health.md](adguard-home-health.md) | read-only |
|
| AdGuard Home health check | [adguard-home-health.md](adguard-home-health.md) | read-only |
|
||||||
|
| Host disk cleanup (logs/apt/docker) | [host-disk-cleanup.md](host-disk-cleanup.md) | change (gated) |
|
||||||
| Matter packet capture | [matter-packet-capture.md](matter-packet-capture.md) | read-only |
|
| Matter packet capture | [matter-packet-capture.md](matter-packet-capture.md) | read-only |
|
||||||
| Home Assistant maintenance | [home-assistant-maintenance.md](home-assistant-maintenance.md) | change (gated) |
|
| Home Assistant maintenance | [home-assistant-maintenance.md](home-assistant-maintenance.md) | change (gated) |
|
||||||
| matrix_e2ee integration update | [matrix-e2ee-update.md](matrix-e2ee-update.md) | change (gated) |
|
| matrix_e2ee integration update | [matrix-e2ee-update.md](matrix-e2ee-update.md) | change (gated) |
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# Gitea health (us2)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Read-only health check of the `/opt/gitea` stack: web UI, git SSH, backup
|
||||||
|
sidecar, and resource headroom.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Applicable: [us2.wsvc.info](../hosts/us2.wsvc.info.md), `/opt/gitea`.
|
||||||
|
- Read-only: does not change Gitea configuration or service state.
|
||||||
|
- Not applicable: Traefik-wide issues (see hosts/us2 notes); Soft Serve
|
||||||
|
rollback (see the rollback section of hosts/us2.wsvc.info.md).
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
- Owner: personal ops (Windy)
|
||||||
|
- Last reviewed: 2026-09-19
|
||||||
|
- Related systems: us2.wsvc.info, Traefik (`vw-net`), repo.windy.me DNS
|
||||||
|
|
||||||
|
Facts: [hosts/us2.wsvc.info.md](../hosts/us2.wsvc.info.md). Reference
|
||||||
|
compose mirror: [compose/gitea/](../compose/gitea/) (server files win).
|
||||||
|
|
||||||
|
## Focused SSH diagnostics
|
||||||
|
|
||||||
|
From the agent shell use `ssh -F /dev/null -4 windy@us2.wsvc.info`.
|
||||||
|
|
||||||
|
### Containers
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -F /dev/null -4 windy@us2.wsvc.info 'cd /opt/gitea && docker compose ps -a'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expect `gitea` and `gitea-backup` both `Up`. `soft-serve*` containers are
|
||||||
|
intentionally `Exited` (rollback retention, keep as-is).
|
||||||
|
|
||||||
|
### Version / image pin
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -F /dev/null -4 windy@us2.wsvc.info \
|
||||||
|
'docker exec gitea /usr/local/bin/gitea --version; grep "^ *image:" /opt/gitea/compose.yml'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expect `gitea version 1.27.3` and the digest pin
|
||||||
|
`gitea/gitea@sha256:1c17ecaead42…` (full digest in compose.yml). If upstream
|
||||||
|
has a newer 1.27.x patch with security fixes, plan an upgrade — do not
|
||||||
|
auto-pull `latest`.
|
||||||
|
|
||||||
|
### Web UI (from laptop / WSL)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -4sI https://repo.windy.me/ | head -3 # expect HTTP/2 200
|
||||||
|
echo | openssl s_client -connect repo.windy.me:443 -servername repo.windy.me 2>/dev/null \
|
||||||
|
| openssl x509 -noout -dates # expect LE cert not expired
|
||||||
|
```
|
||||||
|
|
||||||
|
### Git SSH (the critical check)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -F /dev/null -o BatchMode=yes -p 2222 git@repo.windy.me 2>&1 | head -2
|
||||||
|
```
|
||||||
|
|
||||||
|
Expect `Hi there, windy! You've successfully authenticated …` and no shell.
|
||||||
|
Login user **must be `git@`** (`windy@` fails: publickey denied). Clone URLs
|
||||||
|
need the owner segment: `ssh://git@repo.windy.me:2222/windy/<repo>.git`.
|
||||||
|
The host key is the reused Soft Serve ed25519 key (fingerprint prefix
|
||||||
|
`SHA256:PdxZRe74…`) — a changed fingerprint means someone rotated it.
|
||||||
|
|
||||||
|
### Registration / hardening (server-side, definitive)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -F /dev/null -4 windy@us2.wsvc.info \
|
||||||
|
'grep -E "DISABLE_REGISTRATION|INSTALL_LOCK" /opt/gitea/config/app.ini; stat -c "%a %U" /opt/gitea/config/app.ini'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expect `DISABLE_REGISTRATION = true`, `INSTALL_LOCK = true`, file mode `600`.
|
||||||
|
|
||||||
|
### Backup freshness
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -F /dev/null -4 windy@us2.wsvc.info 'tail -4 /opt/gitea/backups/backup.log; ls -lt /opt/gitea/backups/ | head -4'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expect a `Backup OK` line from today's 02:00 run (products include
|
||||||
|
`app.ini.tar.gz`, `gitea.db`, `repos.tar.gz`) and ≤ 14 retained sets. A
|
||||||
|
backup set older than ~25h with no log error means cron silently died —
|
||||||
|
investigate before proceeding.
|
||||||
|
|
||||||
|
### Logs and memory gate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -F /dev/null -4 windy@us2.wsvc.info \
|
||||||
|
'docker logs gitea --since 24h 2>&1 | grep -iE "error|panic" | tail -5; free -m | head -2'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expect no recurring errors (single SSH auth-failure warnings from the
|
||||||
|
Traefik proxy IP are background noise) and `available` ≥ 150 MB
|
||||||
|
(VPS-94 memory gate; gitea idles ~400 MB RSS).
|
||||||
|
|
||||||
|
## Safety
|
||||||
|
|
||||||
|
- Read-only: never mutate configuration or service state during this check.
|
||||||
|
- If live state conflicts with an expected value (wrong digest, HTTPS
|
||||||
|
non-200, stale backup, registration enabled, `available` < 150 MB),
|
||||||
|
`STOP` and report per repo rules.
|
||||||
|
|
||||||
|
## After config changes
|
||||||
|
|
||||||
|
`/opt/gitea` has no Ansible coverage yet. Reviewed changes
|
||||||
|
(compose edit, digest bump) are applied manually and verified:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -F /dev/null -4 windy@us2.wsvc.info 'cd /opt/gitea && docker compose up -d && docker compose ps'
|
||||||
|
```
|
||||||
|
|
||||||
|
Then re-run the diagnostics above. Keep [compose/gitea/](../compose/gitea/)
|
||||||
|
in sync (reference copy, server files win).
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [hosts/us2.wsvc.info.md](../hosts/us2.wsvc.info.md) — deployment facts, rollback path
|
||||||
|
- Plane `vps` project VPS-94 — migration plan, acceptance checks, audit record
|
||||||
|
- Upstream: https://docs.gitea.com/ (self-hosted admin/config docs)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Runbook: Home Assistant maintenance (hass.windy.lan)
|
# Runbook: Home Assistant maintenance (hass.windy.lan)
|
||||||
|
|
||||||
Target: [hass.windy.lan](../hosts/hass.windy.lan.md) (physical x88 Pro box, HAOS `machine: green`)
|
Target: [hass.windy.lan](../hosts/hass.windy.lan.md) (physical x88 Pro box, HAOS `machine: green`)
|
||||||
Upstream: HAOS 18.1 / Supervisor 2026.07.5 (verified 2026-08-14); Core 2026.8.3 (verified 2026-08-29)
|
Upstream: HAOS 18.2 / Supervisor 2026.09.2 / Core 2026.9.3 (verified 2026-09-23)
|
||||||
|
|
||||||
This runbook covers routine Home Assistant maintenance through the **`ha`
|
This runbook covers routine Home Assistant maintenance through the **`ha`
|
||||||
supervisor CLI**. All commands are wrapped by a single script
|
supervisor CLI**. All commands are wrapped by a single script
|
||||||
@@ -307,6 +307,40 @@ There is no long-lived `HA_TOKEN` in the agent environment. Read entity
|
|||||||
states via Supervisor (`SUPERVISOR_TOKEN` after `sudo -n -i`) at
|
states via Supervisor (`SUPERVISOR_TOKEN` after `sudo -n -i`) at
|
||||||
`http://supervisor/core/api/states/<entity_id>`.
|
`http://supervisor/core/api/states/<entity_id>`.
|
||||||
|
|
||||||
|
#### CSG display refactor 2026-09-04 (VPS-90)
|
||||||
|
|
||||||
|
Template/dashboard changes made **after** pricing cross-check (8月账单
|
||||||
|
198.64 元 vs 模板 198.65 元,≤0.01 元;阶梯常量 0.589/0.639/0.889、
|
||||||
|
260/600 夏档未动):
|
||||||
|
|
||||||
|
- `templates/csg_sensors.yaml` Block B 新增
|
||||||
|
`sensor.csg_this_month_avg_price`(本月阶梯电费÷本月用电,`元/kWh`);
|
||||||
|
**csg_* template sensors = 15**。
|
||||||
|
- Panel `power-monitor`(`lovelace.dashboard_unknown`):环比行改名
|
||||||
|
「环比上月同期」;glance「本月/上月」去重为单卡「上月」(本月行归
|
||||||
|
💰核心数据卡);⚡阶梯电价卡加「本月实际均价」行。实体引用 20→21。
|
||||||
|
- `automations.yaml` +2 提醒:`automation.csg_mian_ban_qie_dong_ji_dang_ti_xing`
|
||||||
|
(10-25)/ `automation.csg_mian_ban_qie_xia_ji_dang_ti_xing`(4-25)
|
||||||
|
09:00 Matrix 提醒人工切「本月累计」gauge 季节档(max/segments 不可模板化)。
|
||||||
|
- 金额单位混排(原生 CNY vs 模板 元)**保留**:`config/entity_registry/update`
|
||||||
|
拒绝自定义文本单位(`extra keys not allowed … Got '元'`),已定案接受。
|
||||||
|
|
||||||
|
**WS 改面板(2026.8,本机实测,后续沿用)**: core/主机 python 无 ws 库、
|
||||||
|
core 容器内经 supervisor 代理 WS 被拒(loop prevention)。用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -o BatchMode=yes hassio@hass.windy.lan \
|
||||||
|
'sudo -n -i sh -c "docker run --rm -i --network host -e SUPERVISOR_TOKEN \
|
||||||
|
--entrypoint python3 r.hassbus.com/home-assistant/aarch64-hassio-supervisor:2026.08.0 \
|
||||||
|
- < /tmp/x.py"'
|
||||||
|
```
|
||||||
|
|
||||||
|
连 `ws://172.30.32.2/core/websocket`(aiohttp,header `Authorization: Bearer
|
||||||
|
$SUPERVISOR_TOKEN`,随后 auth 帧同 token)。命令名 **`lovelace/config`**(读)
|
||||||
|
+ **`lovelace/config/save`**(写,url_path + 全量 config);`lovelace/config/get`
|
||||||
|
已不存在(unknown_command)。备份与细节见
|
||||||
|
[hosts/hass.windy.lan.md](../hosts/hass.windy.lan.md) § CSG 面板重构 2026-09-04。
|
||||||
|
|
||||||
## Command families intentionally NOT scripted
|
## Command families intentionally NOT scripted
|
||||||
|
|
||||||
These exist in `ha` but are either rare, dangerous, or better done in the web
|
These exist in `ha` but are either rare, dangerous, or better done in the web
|
||||||
@@ -361,7 +395,7 @@ full backup `pre-maintenance-20260813` (slug `411a4ba5`). Remaining:
|
|||||||
- `host info` reports `disk_life_time: 10` (boot eMMC ~10% life left) —
|
- `host info` reports `disk_life_time: 10` (boot eMMC ~10% life left) —
|
||||||
monitor on each snapshot; plan disk replacement / data-disk migration.
|
monitor on each snapshot; plan disk replacement / data-disk migration.
|
||||||
- **Home PPPoE IPv4 to CSG is blackholed** (`curl -4` to
|
- **Home PPPoE IPv4 to CSG is blackholed** (`curl -4` to
|
||||||
`218.19.148.218:443` times out). `end0` IPv6 works (`curl -6
|
`218.19.148.218:443` times out). `end1` IPv6 works (`curl -6
|
||||||
https://95598.csg.cn` → HTTP 200). Entry `ip_family: ipv4` still
|
https://95598.csg.cn` → HTTP 200). Entry `ip_family: ipv4` still
|
||||||
matches the stored option; first post-restart poll can still time out
|
matches the stored option; first post-restart poll can still time out
|
||||||
— reload the config entry rather than reinstalling.
|
— reload the config entry rather than reinstalling.
|
||||||
|
|||||||
@@ -0,0 +1,276 @@
|
|||||||
|
# Runbook: Host disk cleanup (unbounded container logs / apt cache / docker artifacts)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Reclaim space on a root filesystem that is filling up (≥70% used) on a Docker
|
||||||
|
Compose host, by fixing unbounded container log growth at the source, clearing
|
||||||
|
apt/journal caches, and removing unused Docker images/volumes. Success: root
|
||||||
|
usage drops to a safe band (≤55% used, or per acceptance in the tracking issue)
|
||||||
|
and log growth stays bounded afterwards.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- 适用环境: production single-root-fs hosts running Docker Compose stacks
|
||||||
|
(first application: `hk2.chans.xyz`; reusable for `mx2.windy.me` / `us2.wsvc.info`
|
||||||
|
which run the same unbounded-`json.log` pattern).
|
||||||
|
- 适用对象: root filesystem usage; container stdout/stderr log files
|
||||||
|
(`/var/lib/docker/containers/*/*-json.log`); `/var/cache/apt`; systemd journal;
|
||||||
|
unused Docker images / anonymous volumes / build cache.
|
||||||
|
- 不适用情形: hosts without systemd-journald or without Docker; LAN/HAOS hosts
|
||||||
|
(use their own runbooks); cases needing disk *growth* (provider resize) rather
|
||||||
|
than cleanup; anything touching service data volumes or `/opt/*` configs
|
||||||
|
(STOP and use the service-specific runbook instead).
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
- Owner: windy (operator) + agent executing per approval
|
||||||
|
- Last reviewed: 2026-09-02
|
||||||
|
- Related systems: hk2.chans.xyz (PowerDNS auth / AdGuard Home / Traefik / RustDesk compose stacks)
|
||||||
|
|
||||||
|
## Preconditions
|
||||||
|
|
||||||
|
- SSH access to the target host with **passwordless sudo** (`sudo -n true` must succeed).
|
||||||
|
- A recorded `df -h` baseline and `docker system df` baseline.
|
||||||
|
- **Explicit user approval** for every service touch listed in Approval gates
|
||||||
|
(recorded in the tracking issue, e.g. Plane `vps` VPS-81).
|
||||||
|
- No open incident on the target host.
|
||||||
|
- Container log growth root cause identified in Diagnose before mutating.
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
| Input | Source | Required | Validation |
|
||||||
|
|---|---:|---|
|
||||||
|
| Target host | inventory/hosts.md | yes | SSH login + `uname -r` |
|
||||||
|
| df/docker baseline | live read-only probe | yes | recorded before first mutation |
|
||||||
|
| Approved service touches | user confirmation in tracking issue | yes | issue comment states approval |
|
||||||
|
| Image keep-list (rollback pins) | operator decision in issue | yes | review `docker image ls` before rmi |
|
||||||
|
| Backup of any config edited | local copy with timestamp | yes | exists before edit |
|
||||||
|
|
||||||
|
## Safety
|
||||||
|
|
||||||
|
### Non-negotiable rules
|
||||||
|
|
||||||
|
- Prefer read-only diagnosis before mutation (never mutate on an unmeasured disk).
|
||||||
|
- Never use `rm` on a live container log — use `truncate -s 0` (keeps the fd valid).
|
||||||
|
- Never run `docker image prune -a` when a keep-list is intended — no keep-list
|
||||||
|
exists; delete explicitly with `docker rmi`.
|
||||||
|
- Never run `docker volume prune -a` — plain `docker volume prune` (no `-a`)
|
||||||
|
removes only unused anonymous volumes; named/in-use volumes stay.
|
||||||
|
- After every mutation, verify the expected state (`df -h`, container status).
|
||||||
|
- Destructive actions require explicit approval (Approval gates).
|
||||||
|
|
||||||
|
### Stop conditions
|
||||||
|
|
||||||
|
- Live state conflicts with this runbook's preconditions or expectations (e.g.
|
||||||
|
root usage differs wildly from baseline, or a container is unhealthy).
|
||||||
|
- Missing approval, missing backup, or missing rollback ability.
|
||||||
|
- A verification step fails with no documented next step.
|
||||||
|
- Any step would touch a volume/container/mount that is not on the approved list.
|
||||||
|
|
||||||
|
### Approval gates
|
||||||
|
|
||||||
|
| Action | Risk | Explicit approval | Approval record |
|
||||||
|
|---|---:|---|---|
|
||||||
|
| `docker restart <chatty container>` | low (sec-level blip of that service only) | yes | tracking issue (VPS-81 T1) |
|
||||||
|
| `systemctl restart systemd-journald` | low (sec-level, no state loss) | yes | tracking issue (VPS-81 T2) |
|
||||||
|
| `apt-get clean` | low (re-downloadable) | no | — |
|
||||||
|
| `journalctl --vacuum-*` / journald drop-in | low | no (restart above is gated) | — |
|
||||||
|
| `docker rmi` of unused images | medium (rollback pin removed unless kept) | yes (keep-list) | tracking issue (VPS-81 T3) |
|
||||||
|
| `docker volume prune` | medium (data in anonymous volumes lost) | yes | tracking issue (VPS-81 T4) |
|
||||||
|
| `docker builder prune` | low | no | — |
|
||||||
|
|
||||||
|
## Procedure
|
||||||
|
|
||||||
|
### Step 1 — Diagnose
|
||||||
|
|
||||||
|
**Action**
|
||||||
|
|
||||||
|
Read-only: `df -h`, `df -i`, `sudo du -x -h --max-depth=1 /`, `docker system df`,
|
||||||
|
and locate oversized container logs:
|
||||||
|
`sudo ls -la /var/lib/docker/containers/*/*-json.log`. Map a big log to its
|
||||||
|
container (`docker inspect -f '{{.Name}} {{.LogPath}}' <id>`), then inspect what
|
||||||
|
it logs (`sudo tail -c 400000 <logpath>`; count `[debug]` lines) and find the
|
||||||
|
config flag driving it (e.g. AGH `log.verbose` in its YAML; note `log.file: ""`
|
||||||
|
means the app's own rotation keys are inert and output goes to the container log).
|
||||||
|
|
||||||
|
**Expected**
|
||||||
|
|
||||||
|
A full accounting of root usage and identification of: (a) any unbounded
|
||||||
|
container log and its root-cause flag; (b) reclaimable apt cache; (c) journal
|
||||||
|
size and journald limits; (d) unused images (0 dangling expected) and unused
|
||||||
|
anonymous volumes.
|
||||||
|
|
||||||
|
**Decision**
|
||||||
|
|
||||||
|
- If root is ≥70% used or any container log is unbounded → Step 2.
|
||||||
|
- If root is healthy and logs are bounded → STOP (no change needed; record evidence).
|
||||||
|
- If state conflicts with expectations (e.g. missing sudo, unexpected mount) → STOP.
|
||||||
|
|
||||||
|
### Step 2 — Fix noisy container logging at the source, then truncate
|
||||||
|
|
||||||
|
**Action**
|
||||||
|
|
||||||
|
1. Back up the app config: `sudo cp <config> <config>.bak-YYYYMMDD-<issue>`.
|
||||||
|
2. Disable the debug/verbose flag (e.g. `log.verbose: true → false` in the AGH YAML).
|
||||||
|
3. Apply config with a container restart: `docker restart <container>` (config-level
|
||||||
|
change; **no recreate** needed and daemon.json rotation would not apply anyway).
|
||||||
|
4. Truncate the accumulated logs: `sudo truncate -s 0 <json.log>` for the chatty
|
||||||
|
container(s) (and any other oversized ones, e.g. traefik).
|
||||||
|
5. Record `df -h` before/after.
|
||||||
|
|
||||||
|
**Expected**
|
||||||
|
|
||||||
|
`docker logs <container>` no longer shows the `[debug]` flood; the `*-json.log`
|
||||||
|
stops growing; several GB reclaimed.
|
||||||
|
|
||||||
|
**Verification**
|
||||||
|
|
||||||
|
- `sudo tail -c 200000 <json.log>` after ≥1 minute → no new debug lines.
|
||||||
|
- `df -h` improvement recorded.
|
||||||
|
- Container still `Up (healthy)`.
|
||||||
|
|
||||||
|
**Rollback**
|
||||||
|
|
||||||
|
- Trigger: log volume unchanged, service degraded, or debug output is actually needed.
|
||||||
|
- Action: restore the config backup and `docker restart <container>`.
|
||||||
|
- Verify: original verbose behaviour back; container healthy.
|
||||||
|
|
||||||
|
### Step 3 — Clear apt cache and cap journald
|
||||||
|
|
||||||
|
**Action**
|
||||||
|
|
||||||
|
1. `sudo apt-get clean` (clears only `/var/cache/apt/archives`; `/var/lib/apt/lists`
|
||||||
|
is not cleared by it and regenerates on `apt update` — optional/low value, skip).
|
||||||
|
2. `sudo journalctl --vacuum-size=100M`.
|
||||||
|
3. Write drop-in `/etc/systemd/journald.conf.d/00-disk-<issue>.conf`:
|
||||||
|
`[Journal]` + `SystemMaxUse=200M`.
|
||||||
|
4. `sudo systemctl restart systemd-journald` (approved service touch).
|
||||||
|
5. Record `df -h` before/after.
|
||||||
|
|
||||||
|
**Expected**
|
||||||
|
|
||||||
|
Archives cleared (~1.4G on hk2), journal ≤100M, future journal capped at 200M.
|
||||||
|
|
||||||
|
**Verification**
|
||||||
|
|
||||||
|
- `du -sh /var/cache/apt/archives` → ~0.
|
||||||
|
- `journalctl --disk-usage` → ≤100M.
|
||||||
|
- `systemctl show systemd-journald -p ...` or restart log confirms new limit;
|
||||||
|
`journalctl -b` still readable.
|
||||||
|
|
||||||
|
**Rollback**
|
||||||
|
|
||||||
|
- Trigger: journald fails to start or logs lost unexpectedly.
|
||||||
|
- Action: remove the drop-in, `sudo systemctl restart systemd-journald`.
|
||||||
|
- Verify: journald active, prior journal entries still listed.
|
||||||
|
|
||||||
|
### Step 4 — Remove unused Docker images (explicit keep-list)
|
||||||
|
|
||||||
|
**Action**
|
||||||
|
|
||||||
|
1. Enumerate unused images: `docker image ls` cross-checked against the images of
|
||||||
|
running containers (`docker ps --format '{{.Image}}'`). Re-enumerate at
|
||||||
|
execution time — the list drifts.
|
||||||
|
2. Present the exact removal list to the operator; keep the agreed rollback pin(s)
|
||||||
|
(e.g. `powerdns/pdns-auth-50:5.0.5`) and delete the rest explicitly:
|
||||||
|
`docker rmi <repo:tag> ...` (per image).
|
||||||
|
3. Record `df -h` before/after.
|
||||||
|
|
||||||
|
**Expected**
|
||||||
|
|
||||||
|
Only in-use images + kept pins remain; ~1–2.5G reclaimed (reclaim is an upper
|
||||||
|
bound — layers shared with kept images are not freed; measure with `df`, do not
|
||||||
|
promise the estimate).
|
||||||
|
|
||||||
|
**Verification**
|
||||||
|
|
||||||
|
- `docker image ls` shows only the expected set.
|
||||||
|
- `docker system df` images reclaimable ≈ 0 for the removed set.
|
||||||
|
- All containers still `Up`.
|
||||||
|
|
||||||
|
**Rollback**
|
||||||
|
|
||||||
|
- Trigger: an image that was actually needed was removed.
|
||||||
|
- Action: re-pull it from the registry (`docker pull <repo:tag>`); if a kept pin
|
||||||
|
must change, update the compose pin and `up -d`.
|
||||||
|
- Verify: image present; affected service healthy.
|
||||||
|
|
||||||
|
### Step 5 — Remove unused anonymous volumes and build cache
|
||||||
|
|
||||||
|
**Action**
|
||||||
|
|
||||||
|
1. Enumerate volumes: `docker volume ls`, and confirm which are referenced by
|
||||||
|
containers (`docker inspect` Mounts). Expected targets: anonymous volumes with
|
||||||
|
no container reference.
|
||||||
|
2. `docker volume prune` (**no `-a`**) — engine ≥ v23 removes only unused
|
||||||
|
anonymous volumes; in-use volumes (e.g. PG data) are protected by container
|
||||||
|
references in every version.
|
||||||
|
3. `docker builder prune -f`.
|
||||||
|
4. Record `df -h` before/after.
|
||||||
|
|
||||||
|
**Expected**
|
||||||
|
|
||||||
|
Unused anonymous volumes (~1.2G on hk2) and build cache gone; in-use volumes intact.
|
||||||
|
|
||||||
|
**Verification**
|
||||||
|
|
||||||
|
- `docker volume ls` shows only in-use volumes.
|
||||||
|
- Services that own volumes (e.g. postgres) report healthy and data present.
|
||||||
|
- `df -h` improvement recorded.
|
||||||
|
|
||||||
|
**Rollback**
|
||||||
|
|
||||||
|
- Trigger: data loss suspected in a removed volume.
|
||||||
|
- Action: restore from backup if the volume ever contained data; verify against
|
||||||
|
the pre-prune enumeration (targets must be anonymous + unreferenced before prune).
|
||||||
|
- Note: this is why target enumeration is recorded before pruning.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Troubleshooting A — Log still grows after disabling verbose
|
||||||
|
|
||||||
|
- Evidence: `sudo tail -c 200000 <json.log>` still shows new lines; app config re-checked.
|
||||||
|
- Allowed actions: check for a second verbose source (container entrypoint flags,
|
||||||
|
other apps in the same log); check `docker inspect <c> --format '{{.HostConfig.LogConfig}}'`.
|
||||||
|
- Next step: back to Step 2 or STOP if a container-level log-opts change (recreate)
|
||||||
|
would be needed — that is a separate approval.
|
||||||
|
|
||||||
|
### Troubleshooting B — `docker rmi` fails (image in use)
|
||||||
|
|
||||||
|
- Evidence: `image is being used by stopped container ...`.
|
||||||
|
- Allowed actions: identify the stopped container (`docker ps -a`); confirm it is
|
||||||
|
not needed; remove it only with explicit approval.
|
||||||
|
- Next step: re-run rmi for the remaining images; never force-delete blindly.
|
||||||
|
|
||||||
|
### Troubleshooting C — `docker volume prune` would remove more than expected
|
||||||
|
|
||||||
|
- Evidence: prune dry-run/listing includes a named or referenced volume.
|
||||||
|
- Allowed actions: abort; do not add `-a`; re-check references.
|
||||||
|
- Next step: STOP and report to the operator with the enumeration.
|
||||||
|
|
||||||
|
## Final Verification
|
||||||
|
|
||||||
|
The flow is successful only when all of the following hold:
|
||||||
|
|
||||||
|
- `df -h` root usage is in the agreed band (VPS-81: 76% → ≤55% used; measure, do not assume).
|
||||||
|
- `docker system df` shows reclaimable ≈ 0 for images/volumes targeted.
|
||||||
|
- All containers `Up` (health checks pass); public services verified
|
||||||
|
(`dig @<host-ip> SOA <zone>` for DNS hosts; service URLs reachable).
|
||||||
|
- Tracking issue updated with before/after `df`, actions, and the one-week
|
||||||
|
observation checkpoint for log growth.
|
||||||
|
|
||||||
|
## Failure Handling
|
||||||
|
|
||||||
|
If the flow cannot complete:
|
||||||
|
|
||||||
|
1. Stop further mutation.
|
||||||
|
2. Collect command output, timestamps, and the exact step that failed.
|
||||||
|
3. Record completed steps, actual results, unmet expectations, and whether a
|
||||||
|
rollback ran.
|
||||||
|
4. Hand over per the tracking issue with evidence; do not guess further.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- Plane `vps` issue VPS-81 "hk2: 释放根盘空间" (+ subtasks VPS-82…88) — plan, review findings, approvals.
|
||||||
|
- [hosts/hk2.chans.xyz.md](../hosts/hk2.chans.xyz.md) — host facts.
|
||||||
|
- [RUNBOOKS.md](../RUNBOOKS.md) — runbook spec; [runbooks/README.md](README.md) — index.
|
||||||
@@ -11,6 +11,7 @@ is read-only and changes no device or network state.
|
|||||||
|
|
||||||
- Environment: LAN55 (`hass.windy.lan`, Aqara M3, ESP32-C2 Matter bulbs,
|
- Environment: LAN55 (`hass.windy.lan`, Aqara M3, ESP32-C2 Matter bulbs,
|
||||||
phone / HA matter-server all on the 55 subnet).
|
phone / HA matter-server all on the 55 subnet).
|
||||||
|
- NIC on hass is `end1` (verified 2026-09-23). Notes from 2026-08 called it `end0`; that name is gone.
|
||||||
- Subject: Matter over Wi-Fi and Thread relay nodes. The Thread 802.15.4 air
|
- Subject: Matter over Wi-Fi and Thread relay nodes. The Thread 802.15.4 air
|
||||||
side itself is not capturable — only IPv6 forwarding by a Thread relay such
|
side itself is not capturable — only IPv6 forwarding by a Thread relay such
|
||||||
as the M3 is visible.
|
as the M3 is visible.
|
||||||
@@ -28,11 +29,11 @@ equals capturing the whole flow.
|
|||||||
|
|
||||||
| Capture point | Sees | Blind spot | Notes |
|
| Capture point | Sees | Blind spot | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| **hass `end0` — commissioner side (recommended)** | The full HA-driven commissioning conversation: all mDNS queries/announcements (segment multicast) + the complete TCP 5540 PASE/CASE session | Phone-as-commissioner flows (the phone's session to the device does not pass through hass) | `core_matter_server` uses **host networking**, so tcpdump on `end0` sees the add-on's traffic directly; `/` is overlay with ~42 GB free — no 60 MB tmpfs rotation needed |
|
| **hass `end1` — commissioner side (recommended)** | The full HA-driven commissioning conversation: all mDNS queries/announcements (segment multicast) + the complete TCP 5540 PASE/CASE session | Phone-as-commissioner flows (the phone's session to the device does not pass through hass) | `core_matter_server` uses **host networking**, so tcpdump on `end1` sees the add-on's traffic directly; `/` is overlay with ~42 GB free — no 60 MB tmpfs rotation needed |
|
||||||
| **UAP-AC-Lite `br0` (192.168.55.5)** | All mDNS multicast (flooded; igmp snooping off) + all wireless-client unicast + unicast to/from the AP | Wired↔wired unicast — e.g. HA↔M3 TCP 5540 while a Thread device commissions via the M3 (wired, observed) — is switched locally and never traverses the AP | AP `/tmp` is a ~60 MB tmpfs → rotating capture is **mandatory** |
|
| **UAP-AC-Lite `br0` (192.168.55.5)** | All mDNS multicast (flooded; igmp snooping off) + all wireless-client unicast + unicast to/from the AP | Wired↔wired unicast — e.g. HA↔M3 TCP 5540 while a Thread device commissions via the M3 (wired, observed) — is switched locally and never traverses the AP | AP `/tmp` is a ~60 MB tmpfs → rotating capture is **mandatory** |
|
||||||
|
|
||||||
For the common "add device" case with HA matter-server as the commissioner,
|
For the common "add device" case with HA matter-server as the commissioner,
|
||||||
capture on hass `end0`. Use the AP `br0` point for wireless-device or
|
capture on hass `end1`. Use the AP `br0` point for wireless-device or
|
||||||
phone-driven flows (a wireless client's unicast to/from its AP is only visible
|
phone-driven flows (a wireless client's unicast to/from its AP is only visible
|
||||||
there).
|
there).
|
||||||
|
|
||||||
@@ -45,7 +46,7 @@ A third point, `gw` `switch0`, is **verified as a limited capture point**
|
|||||||
- Owner: personal ops (Windy)
|
- Owner: personal ops (Windy)
|
||||||
- Last reviewed: 2026-08-22
|
- Last reviewed: 2026-08-22
|
||||||
- Related systems: UAP-AC-Lite AP `192.168.55.5` (br0), `core_matter_server` on
|
- Related systems: UAP-AC-Lite AP `192.168.55.5` (br0), `core_matter_server` on
|
||||||
`hass.windy.lan` (`end0`), Aqara M3, ESP32-C2 Matter bulbs
|
`hass.windy.lan` (`end1`), Aqara M3, ESP32-C2 Matter bulbs
|
||||||
|
|
||||||
## Preconditions
|
## Preconditions
|
||||||
|
|
||||||
@@ -89,7 +90,7 @@ A third point, `gw` `switch0`, is **verified as a limited capture point**
|
|||||||
|
|
||||||
**Action**
|
**Action**
|
||||||
|
|
||||||
- HA matter-server is the commissioner (the "add device" case) → hass `end0`.
|
- HA matter-server is the commissioner (the "add device" case) → hass `end1`.
|
||||||
- Wireless device or phone-driven flow → AP `br0`.
|
- Wireless device or phone-driven flow → AP `br0`.
|
||||||
|
|
||||||
**Expected**
|
**Expected**
|
||||||
@@ -114,7 +115,7 @@ ssh zhiqiangf@192.168.55.5 "tcpdump -ni br0 -s 0 -tt 'udp port 5353 or tcp port
|
|||||||
hass (commissioner side):
|
hass (commissioner side):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh hassio@hass.windy.lan "sudo -n -i tcpdump -ni end0 -s 0 -tt 'udp port 5353 or tcp port 5540 or tcp port 5552'"
|
ssh hassio@hass.windy.lan "sudo -n -i tcpdump -ni end1 -s 0 -tt 'udp port 5353 or tcp port 5540 or tcp port 5552'"
|
||||||
```
|
```
|
||||||
|
|
||||||
Keep the window open, trigger the device behavior (enter commissioning mode /
|
Keep the window open, trigger the device behavior (enter commissioning mode /
|
||||||
@@ -153,7 +154,7 @@ ssh zhiqiangf@192.168.55.5 "tcpdump -ni br0 -s 0 -C 5 -W 12 -w /tmp/matter.pcap
|
|||||||
hass (rotation optional — overlay disk):
|
hass (rotation optional — overlay disk):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh hassio@hass.windy.lan "sudo -n -i tcpdump -ni end0 -s 0 -C 5 -W 12 -w /tmp/matter.pcap 'udp port 5353 or tcp port 5540 or tcp port 5552'"
|
ssh hassio@hass.windy.lan "sudo -n -i tcpdump -ni end1 -s 0 -C 5 -W 12 -w /tmp/matter.pcap 'udp port 5353 or tcp port 5540 or tcp port 5552'"
|
||||||
```
|
```
|
||||||
|
|
||||||
Trigger the traffic, then `Ctrl+C`. Files are `/tmp/matter.pcap`,
|
Trigger the traffic, then `Ctrl+C`. Files are `/tmp/matter.pcap`,
|
||||||
@@ -246,7 +247,7 @@ ssh zhiqiangf@192.168.55.5 "tcpdump -ni br0 -s 0 -C 5 -W 12 -w /tmp/matter-\$(da
|
|||||||
> command so the remote shell expands it.
|
> command so the remote shell expands it.
|
||||||
|
|
||||||
For the hass point, prefix the same commands with
|
For the hass point, prefix the same commands with
|
||||||
`ssh hassio@hass.windy.lan "sudo -n -i tcpdump -ni end0 …"`.
|
`ssh hassio@hass.windy.lan "sudo -n -i tcpdump -ni end1 …"`.
|
||||||
|
|
||||||
## Capture point: gw switch0
|
## Capture point: gw switch0
|
||||||
|
|
||||||
@@ -266,7 +267,7 @@ LAN55.**
|
|||||||
- **What `switch0` still sees:** cross-subnet (66↔55) unicast, traffic to/from
|
- **What `switch0` still sees:** cross-subnet (66↔55) unicast, traffic to/from
|
||||||
the gateway itself (DHCP, DNS forwarding, port-forwards), and LAN55 mDNS
|
the gateway itself (DHCP, DNS forwarding, port-forwards), and LAN55 mDNS
|
||||||
multicast (flooded up the uplink). Use it only for those flows; for a full
|
multicast (flooded up the uplink). Use it only for those flows; for a full
|
||||||
commissioning conversation use the hass `end0` or AP `br0` point instead.
|
commissioning conversation use the hass `end1` or AP `br0` point instead.
|
||||||
- **Full mirror:** only via SE5420 port mirroring (the switch cannot run
|
- **Full mirror:** only via SE5420 port mirroring (the switch cannot run
|
||||||
tcpdump). Not configured; out of scope here.
|
tcpdump). Not configured; out of scope here.
|
||||||
- **Verification commands (EdgeOS v3.0.1 build 5862409):**
|
- **Verification commands (EdgeOS v3.0.1 build 5862409):**
|
||||||
@@ -295,7 +296,7 @@ LAN55.**
|
|||||||
- Realtime capture consistently shows the target device's mDNS announcements
|
- Realtime capture consistently shows the target device's mDNS announcements
|
||||||
(`_matterc` / `_matter._tcp`) on the chosen point.
|
(`_matterc` / `_matter._tcp`) on the chosen point.
|
||||||
- Commissioning shows the TCP 5540 handshake (SYN/SYN-ACK/ACK); on the hass
|
- Commissioning shows the TCP 5540 handshake (SYN/SYN-ACK/ACK); on the hass
|
||||||
`end0` point this includes wired Thread-relay commissioning (HA↔M3), which
|
`end1` point this includes wired Thread-relay commissioning (HA↔M3), which
|
||||||
the AP point cannot see.
|
the AP point cannot see.
|
||||||
- Saved pcap opens in Wireshark and filters by `mDNS` / `tcp.port==5540`.
|
- Saved pcap opens in Wireshark and filters by `mDNS` / `tcp.port==5540`.
|
||||||
|
|
||||||
|
|||||||
+50
-2
@@ -13,8 +13,8 @@ Read-only health check of the pgdb TimescaleDB compose stack (PG18 + pgweb GUI +
|
|||||||
## Ownership
|
## Ownership
|
||||||
|
|
||||||
- Owner: personal ops (Windy)
|
- Owner: personal ops (Windy)
|
||||||
- Last reviewed: 2026-08-29
|
- Last reviewed: 2026-09-22(新增 check 9:CSG 归档新鲜度)
|
||||||
- Related systems: pgdb (`/opt/database`, TimescaleDB 18.6 / TS 2.29.2), HA `192.168.55.11` (hass/scribe clients)
|
- Related systems: pgdb (`/opt/database`, TimescaleDB 18.6 / TS 2.29.2), HA `192.168.55.11` (hass/scribe clients), `scribe.csg_history` 永久归档(南方电网逐日/逐月,由 job 1010/1011 维护)
|
||||||
|
|
||||||
## Access
|
## Access
|
||||||
|
|
||||||
@@ -43,6 +43,8 @@ From the agent sandbox, always use `-F /dev/null` (system ssh config is unreadab
|
|||||||
- Latest daily backup older than today, not custom format, or `pg_restore -l` fails.
|
- Latest daily backup older than today, not custom format, or `pg_restore -l` fails.
|
||||||
- Disk usage near full on `/srv/pgdata` or `/`.
|
- Disk usage near full on `/srv/pgdata` or `/`.
|
||||||
- New `ERROR`/`FATAL` lines in the timescaledb log or backup failures in the pg-backup log.
|
- New `ERROR`/`FATAL` lines in the timescaledb log or backup failures in the pg-backup log.
|
||||||
|
- CSG 归档新鲜度断言失败(见 check 9):`csg_history` 最新 day 行落后超过 2 天、
|
||||||
|
`period` 上出现缺口、或任一 CSG job 非 `Success` / `total_failures > 0`。
|
||||||
|
|
||||||
## Pass criteria
|
## Pass criteria
|
||||||
|
|
||||||
@@ -54,6 +56,9 @@ From the agent sandbox, always use `-F /dev/null` (system ssh config is unreadab
|
|||||||
- `daily/*-latest.dump` symlinks point to today's dumps; `file -L` reports `PostgreSQL custom database dump`.
|
- `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`.
|
- `/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.
|
- timescaledb log: no new `ERROR`/`FATAL`; pg-backup log: recent successful backup.
|
||||||
|
- `scribe.csg_history`:最新 day 行 `period >= current_date - 2`、`period` 上无缺口;
|
||||||
|
`csg_daily_snapshot`(1010)与 `csg_backfill_missing_days`(1011)均 `Success`、`total_failures = 0`;
|
||||||
|
各月 `|sum(day cost) − month cost| ≤ 0.05`(逐日舍入的累积,非缺陷)。
|
||||||
|
|
||||||
## Checks
|
## Checks
|
||||||
|
|
||||||
@@ -179,3 +184,46 @@ ssh -F /dev/null -o BatchMode=yes windy@192.168.55.15 'cd /opt/database && docke
|
|||||||
- pg-backup: recent successful run (`SQL backup created successfully` for each database, no restore/cleanup errors).
|
- 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.
|
**Stop** if repeated `ERROR`/`FATAL` appear or a backup run failed.
|
||||||
|
|
||||||
|
### 9. CSG 长期归档新鲜度(`scribe.csg_history`)
|
||||||
|
|
||||||
|
> 为什么必须单独看这张表:它承载南方电网的**永久**逐日/逐月归档,但写入它的
|
||||||
|
> job 曾经 **294 次运行 0 成功**、静默造成 23 天缺口而无人发现。更隐蔽的是:
|
||||||
|
> **上游停更时 job 会反复 upsert 同一行、`last_run_status` 仍是 `Success`、
|
||||||
|
> 日期上也不会出现"缺口"** —— 只有本节的"最新日期"断言能发现它。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# a) 最新 day 行日期(期望 >= 今天 - 2;CSG 数据本身滞后一天)
|
||||||
|
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`。
|
||||||
|
- d) 每个月 `|diff| <= 0.05` 元。这是**逐日四舍五入到 2 位的累积**,不是缺陷;
|
||||||
|
月行(由 `csg_ladder_cost` 单次舍入)才是权威值。
|
||||||
|
|
||||||
|
**Stop** if
|
||||||
|
|
||||||
|
- `last_day` 落后超过 2 天 → 上游停更或 job 未运行。先看 c) 的 `last_run_started_at`
|
||||||
|
与 `ha core logs` 里的 `china_southern_power_grid_stat` 错误,再决定是否需要
|
||||||
|
重新加载集成 config entry(见 [home-assistant-maintenance](home-assistant-maintenance.md))。
|
||||||
|
- b) 出现缺口 → 等 `csg_backfill_missing_days` 下一轮(或手动 `CALL run_job(1011);`)
|
||||||
|
后复查;仍缺则查 `timescaledb_information.job_errors`。
|
||||||
|
- c) 任一 job 非 `Success` 或 `total_failures > 0` → 查 `job_errors` 的 `err_message`;
|
||||||
|
历史上 job 1008 的失败原因是**函数签名与 TimescaleDB 自定义 job 约定不符**
|
||||||
|
(必须是 `(job_id integer, config jsonb)`),修复记录见
|
||||||
|
`compose/pgdb/csg-snapshot-v3.sql` / `-v5.sql`。
|
||||||
|
- d) 某个 `|diff| > 0.05` → 说明有新写入路径绕过了 v5 的边际差分口径,停止并复核。
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# Runbook: Plane Health Check
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Read-only health check of the self-hosted Plane project-management instance
|
||||||
|
(`plane.chans.xyz`) running on the synapse K3s cluster.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Applicable: [synapse.chans.xyz](../hosts/synapse.chans.xyz.md), namespace `plane`.
|
||||||
|
- Read-only: does not change pods, ingress, certificates, secrets, or configuration.
|
||||||
|
- Not applicable: Plane upgrade, values changes, or data recovery — those need a
|
||||||
|
reviewed change (see `ansible-operations.md` / `release.md`).
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
- Owner: personal ops (Windy)
|
||||||
|
- Last reviewed: 2026-09-02
|
||||||
|
- Related systems: synapse.chans.xyz (Helm `plane-app`, chart `plane-ce-1.8.0`, app `v1.4.1`)
|
||||||
|
|
||||||
|
## Pass criteria
|
||||||
|
|
||||||
|
All of the following must hold; any conflict means `STOP` and record evidence.
|
||||||
|
|
||||||
|
- `sudo helm list -A` shows `plane-app` in ns `plane`, STATUS `deployed`.
|
||||||
|
- All 7 Deployments + 4 StatefulSets in ns `plane` are `1/1 Running` with 0 recent restarts.
|
||||||
|
- Init Jobs `api-migrate-*` / `minio-bucket-*` are `Completed`.
|
||||||
|
- Certificate `plane-app-ssl-cert` is `READY=True` (CN `plane.chans.xyz`).
|
||||||
|
- `https://plane.chans.xyz/` returns HTTP 200 with a valid Let's Encrypt cert.
|
||||||
|
- Root disk usage below the 80% warning threshold.
|
||||||
|
|
||||||
|
## Procedure
|
||||||
|
|
||||||
|
### 1. Release and workloads
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -4 windy@synapse.chans.xyz 'sudo helm list -A'
|
||||||
|
ssh -4 windy@synapse.chans.xyz 'sudo k3s kubectl -n plane get deploy,sts,pods -o wide'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `plane-app` deployed; all workloads `1/1 Running`, `RESTARTS` low;
|
||||||
|
no `CrashLoopBackOff` / `Evicted`. Otherwise `STOP` and record evidence.
|
||||||
|
|
||||||
|
### 2. TLS certificate and ingress
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -4 windy@synapse.chans.xyz 'sudo k3s kubectl -n plane get certificate,issuer,ingressroute'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `plane-app-ssl-cert` `READY=True`; `plane-app-ingress` present with
|
||||||
|
routes for `/`, `/api`, `/spaces`, `/god-mode`, `/live`, `/uploads`.
|
||||||
|
|
||||||
|
If `READY=False` with a pending HTTP-01 challenge, the usual cause is the node
|
||||||
|
DNS chain (coreDNS → systemd-resolved → uplink) failing to resolve
|
||||||
|
`plane.chans.xyz` — check `resolvectl query plane.chans.xyz` vs
|
||||||
|
`dig +short plane.chans.xyz @8.8.8.8`. If the record exists publicly but the
|
||||||
|
node fails, `sudo resolvectl flush-caches` and wait for the cert-manager retry;
|
||||||
|
do not mutate the issuer.
|
||||||
|
|
||||||
|
### 3. Endpoint verification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -4 -s -o /dev/null -w '%{http_code}\n' https://plane.chans.xyz/
|
||||||
|
echo | openssl s_client -connect plane.chans.xyz:443 -servername plane.chans.xyz 2>/dev/null | openssl x509 -noout -subject -issuer -dates
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: HTTP 200, cert `CN=plane.chans.xyz` issued by Let's Encrypt with a
|
||||||
|
future `notAfter`. `https://plane.chans.xyz/api/` may 404 — the API serves
|
||||||
|
under `/api/...` paths only, so a bare 404 there is not a failure.
|
||||||
|
|
||||||
|
### 4. Storage and disk
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -4 windy@synapse.chans.xyz 'sudo k3s kubectl -n plane get pvc'
|
||||||
|
ssh -4 windy@synapse.chans.xyz 'df -hP /'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all 4 PVCs `Bound` (minio 5Gi, pgdb 5Gi, rabbitmq 100Mi, redis 100Mi
|
||||||
|
on `local-path`); root disk < 80%.
|
||||||
|
|
||||||
|
## Safety
|
||||||
|
|
||||||
|
- Read-only: never mutate pods, ingress, certificates, or configuration during
|
||||||
|
this check.
|
||||||
|
- Plane data (ns `plane` Postgres + MinIO PVCs) has **no backup tier**; treat
|
||||||
|
the instance as at-risk until a backup design exists.
|
||||||
|
- If live state conflicts with the expected values above, `STOP` and record
|
||||||
|
evidence; do not "fix in passing".
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [hosts/synapse.chans.xyz.md](../hosts/synapse.chans.xyz.md) — plane stack facts
|
||||||
|
- [matrix-health.md](matrix-health.md) — sibling service on the same cluster
|
||||||
Executable
+172
@@ -0,0 +1,172 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Stdlib-only Home Assistant WebSocket client (via the Supervisor core proxy).
|
||||||
|
|
||||||
|
Why this exists
|
||||||
|
---------------
|
||||||
|
Some HA operations have **no REST route** and must go through the WebSocket API
|
||||||
|
-- writing Lovelace config (`lovelace/config/save`) is the main one, and the
|
||||||
|
`.storage/` files must not be hand-edited (HA may overwrite them, and edits skip
|
||||||
|
validation). The obvious paths are dead ends on this host: the HA host and the
|
||||||
|
core container have no usable WS client (the Supervisor proxy rejects core's own
|
||||||
|
loop), and the Supervisor image has neither `websockets` nor `aiohttp`.
|
||||||
|
HA's WS protocol is plain text frames, so ~90 lines of stdlib is enough.
|
||||||
|
|
||||||
|
Run it from the HA host (see hosts/hass.windy.lan.md "改法(可复用)"):
|
||||||
|
|
||||||
|
B64=$(base64 < scripts/ha-ws-client.py | tr -d '\n')
|
||||||
|
echo '{"action":"get","url_path":"dashboard-quick"}' | \
|
||||||
|
docker run --rm -i --network host -e SUPERVISOR_TOKEN --entrypoint python3 \
|
||||||
|
r.hassbus.com/home-assistant/aarch64-hassio-supervisor:<ver> \
|
||||||
|
-c "import base64,sys;exec(base64.b64decode('$B64').decode())" - -
|
||||||
|
|
||||||
|
Do NOT use `-v /tmp/...`: that path is resolved by the Docker daemon on the
|
||||||
|
HAOS host, not inside the SSH add-on, so the mount comes up empty.
|
||||||
|
|
||||||
|
Payload / output
|
||||||
|
----------------
|
||||||
|
{"action":"get", "url_path":"dashboard-quick"}
|
||||||
|
{"action":"save", "url_path":"dashboard-quick", "config": {...}}
|
||||||
|
{"action":"raw", "command": {"type":"lovelace/resources/list"}}
|
||||||
|
|
||||||
|
python3 ha-ws-client.py <payload.json|-> <out.json|->
|
||||||
|
|
||||||
|
"-" reads the payload from stdin / prints the result to stdout, wrapped in
|
||||||
|
<<<DSH_RESULT>>> / <<<DSH_END>>> markers so it survives the add-on login banner.
|
||||||
|
"""
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
HOST = os.environ.get("WS_HOST", "172.30.32.2")
|
||||||
|
PORT = int(os.environ.get("WS_PORT", "80"))
|
||||||
|
PATH = os.environ.get("WS_PATH", "/core/websocket")
|
||||||
|
|
||||||
|
|
||||||
|
class WS:
|
||||||
|
def __init__(self, host, port, path, extra_headers=None):
|
||||||
|
self.s = socket.create_connection((host, port), timeout=30)
|
||||||
|
key = base64.b64encode(bytes(random.getrandbits(8) for _ in range(16))).decode()
|
||||||
|
req = (
|
||||||
|
f"GET {path} HTTP/1.1\r\nHost: {host}\r\nUpgrade: websocket\r\n"
|
||||||
|
f"Connection: Upgrade\r\nSec-WebSocket-Key: {key}\r\n"
|
||||||
|
f"Sec-WebSocket-Version: 13\r\n"
|
||||||
|
)
|
||||||
|
for k, v in (extra_headers or {}).items():
|
||||||
|
req += f"{k}: {v}\r\n"
|
||||||
|
self.s.sendall((req + "\r\n").encode())
|
||||||
|
buf = b""
|
||||||
|
while b"\r\n\r\n" not in buf:
|
||||||
|
d = self.s.recv(4096)
|
||||||
|
if not d:
|
||||||
|
raise RuntimeError("closed during handshake")
|
||||||
|
buf += d
|
||||||
|
head, _, rest = buf.partition(b"\r\n\r\n")
|
||||||
|
status = head.split(b"\r\n")[0]
|
||||||
|
if b"101" not in status:
|
||||||
|
raise RuntimeError("handshake failed: " + status.decode(errors="replace"))
|
||||||
|
self.buf = rest
|
||||||
|
|
||||||
|
def _exact(self, n):
|
||||||
|
while len(self.buf) < n:
|
||||||
|
d = self.s.recv(65536)
|
||||||
|
if not d:
|
||||||
|
raise RuntimeError("socket closed")
|
||||||
|
self.buf += d
|
||||||
|
out, self.buf = self.buf[:n], self.buf[n:]
|
||||||
|
return out
|
||||||
|
|
||||||
|
def send(self, text):
|
||||||
|
data = text.encode()
|
||||||
|
mask = bytes(random.getrandbits(8) for _ in range(4))
|
||||||
|
n = len(data)
|
||||||
|
hdr = bytearray([0x81])
|
||||||
|
if n < 126:
|
||||||
|
hdr.append(0x80 | n)
|
||||||
|
elif n < 65536:
|
||||||
|
hdr.append(0x80 | 126)
|
||||||
|
hdr += struct.pack(">H", n)
|
||||||
|
else:
|
||||||
|
hdr.append(0x80 | 127)
|
||||||
|
hdr += struct.pack(">Q", n)
|
||||||
|
hdr += mask
|
||||||
|
self.s.sendall(bytes(hdr) + bytes(b ^ mask[i % 4] for i, b in enumerate(data)))
|
||||||
|
|
||||||
|
def recv(self):
|
||||||
|
while True:
|
||||||
|
b0, b1 = self._exact(2)
|
||||||
|
op = b0 & 0x0F
|
||||||
|
ln = b1 & 0x7F
|
||||||
|
if ln == 126:
|
||||||
|
ln = struct.unpack(">H", self._exact(2))[0]
|
||||||
|
elif ln == 127:
|
||||||
|
ln = struct.unpack(">Q", self._exact(8))[0]
|
||||||
|
payload = self._exact(ln) if ln else b""
|
||||||
|
if op == 0x8:
|
||||||
|
raise RuntimeError("server closed")
|
||||||
|
if op in (0x9, 0xA):
|
||||||
|
continue
|
||||||
|
return payload.decode()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
token = os.environ["SUPERVISOR_TOKEN"]
|
||||||
|
src = sys.argv[1]
|
||||||
|
payload = json.load(sys.stdin if src == "-" else open(src, encoding="utf-8"))
|
||||||
|
action = payload.pop("action")
|
||||||
|
url_path = payload.pop("url_path", "dashboard-quick")
|
||||||
|
out_path = sys.argv[2]
|
||||||
|
|
||||||
|
ws = WS(HOST, PORT, PATH, {"Authorization": f"Bearer {token}"})
|
||||||
|
while True:
|
||||||
|
hello = json.loads(ws.recv())
|
||||||
|
if hello.get("type") == "auth_required":
|
||||||
|
break
|
||||||
|
if hello.get("type") == "auth_ok":
|
||||||
|
break
|
||||||
|
ws.send(json.dumps({"type": "auth", "access_token": token}))
|
||||||
|
while True:
|
||||||
|
r = json.loads(ws.recv())
|
||||||
|
if r.get("type") == "auth_ok":
|
||||||
|
break
|
||||||
|
if r.get("type") == "auth_invalid":
|
||||||
|
print("AUTH_INVALID:", json.dumps(r, ensure_ascii=False)[:300])
|
||||||
|
return 1
|
||||||
|
|
||||||
|
cmd = {
|
||||||
|
"id": 1,
|
||||||
|
"type": "lovelace/config/save" if action == "save" else "lovelace/config",
|
||||||
|
"url_path": url_path,
|
||||||
|
}
|
||||||
|
if action == "save":
|
||||||
|
cmd["config"] = payload["config"]
|
||||||
|
elif action == "raw":
|
||||||
|
cmd = dict(payload["command"])
|
||||||
|
cmd["id"] = 1
|
||||||
|
ws.send(json.dumps(cmd))
|
||||||
|
while True:
|
||||||
|
r = json.loads(ws.recv())
|
||||||
|
if r.get("id") != 1:
|
||||||
|
continue
|
||||||
|
if not r.get("success"):
|
||||||
|
print("ERROR:", json.dumps(r, ensure_ascii=False)[:600])
|
||||||
|
return 1
|
||||||
|
result = r.get("result")
|
||||||
|
blob = json.dumps(result if result is not None else {"ok": True},
|
||||||
|
ensure_ascii=False, indent=1)
|
||||||
|
if out_path == "-":
|
||||||
|
print("<<<DSH_RESULT>>>")
|
||||||
|
print(blob)
|
||||||
|
print("<<<DSH_END>>>")
|
||||||
|
else:
|
||||||
|
with open(out_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(blob)
|
||||||
|
print("OK ->", out_path)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -138,7 +138,7 @@ fi
|
|||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
say "== 4. Runbook spec compliance (RUNBOOKS.md) =="
|
say "== 4. Runbook spec compliance (RUNBOOKS.md) =="
|
||||||
PROCEDURE_RUNBOOKS="fix-ci.md issue-to-merge.md network-change.md network-recovery.md pgdb-restore.md release.md rollback.md"
|
PROCEDURE_RUNBOOKS="fix-ci.md host-disk-cleanup.md issue-to-merge.md network-change.md network-recovery.md pgdb-restore.md release.md rollback.md"
|
||||||
GATED_REF_RUNBOOKS="mailcow-update.md ansible-operations.md home-assistant-maintenance.md matrix-e2ee-update.md pgdb-update.md vaultwarden-sqlite-to-postgres.md"
|
GATED_REF_RUNBOOKS="mailcow-update.md ansible-operations.md home-assistant-maintenance.md matrix-e2ee-update.md pgdb-update.md vaultwarden-sqlite-to-postgres.md"
|
||||||
RUNBOOK_INDEX="runbooks/README.md"
|
RUNBOOK_INDEX="runbooks/README.md"
|
||||||
for f in runbooks/*.md; do
|
for f in runbooks/*.md; do
|
||||||
|
|||||||
Reference in New Issue
Block a user