Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de52cb8b57 | ||
|
|
b15e19bce9 | ||
|
|
bee54a6858 | ||
|
|
d6747028b4 | ||
|
|
f174aa1219 | ||
|
|
5b5f6042e6 | ||
|
|
50136b2ffd | ||
|
|
6707cebc88 | ||
|
|
908ff5412a | ||
|
|
e58283210a | ||
|
|
8c73d1f894 | ||
|
|
bc0a86245d | ||
|
|
13032fd0bb | ||
|
|
d2063e7496 | ||
|
|
ab808088b8 |
@@ -30,3 +30,5 @@ skills-lock.json
|
|||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
*~
|
*~
|
||||||
|
# Agent working scratch (not repo content).
|
||||||
|
.agent-work/
|
||||||
|
|||||||
@@ -59,7 +59,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 (stub details) |
|
| `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) |
|
||||||
| 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 |
|
||||||
@@ -100,6 +100,7 @@ not maintain a second copy of the machine table here.
|
|||||||
| UniFi local-service proxy bypass | [docs/unifi-openclash-localhost.md](docs/unifi-openclash-localhost.md) |
|
| UniFi local-service proxy bypass | [docs/unifi-openclash-localhost.md](docs/unifi-openclash-localhost.md) |
|
||||||
| UniFi SSO login setting (Ansible) | `cd ansible && ansible-playbook playbooks/unifi-sso.yml --limit unifi` |
|
| UniFi SSO login setting (Ansible) | `cd ansible && ansible-playbook playbooks/unifi-sso.yml --limit unifi` |
|
||||||
| Routine Ansible operations | [runbooks/ansible-operations.md](runbooks/ansible-operations.md) |
|
| Routine Ansible operations | [runbooks/ansible-operations.md](runbooks/ansible-operations.md) |
|
||||||
|
| Routine make commands | `make help` (wraps `ansible-operations.md` read-only + gated flows) |
|
||||||
| Issue → mergeable change | [runbooks/issue-to-merge.md](runbooks/issue-to-merge.md) |
|
| Issue → mergeable change | [runbooks/issue-to-merge.md](runbooks/issue-to-merge.md) |
|
||||||
| Fix failing health/playbook run | [runbooks/fix-ci.md](runbooks/fix-ci.md) |
|
| Fix failing health/playbook run | [runbooks/fix-ci.md](runbooks/fix-ci.md) |
|
||||||
| Release a reviewed change | [runbooks/release.md](runbooks/release.md) |
|
| Release a reviewed change | [runbooks/release.md](runbooks/release.md) |
|
||||||
@@ -166,4 +167,5 @@ hosts/ # per-host facts
|
|||||||
runbooks/ # step-by-step ops (README.md = index, _template.md = template)
|
runbooks/ # step-by-step ops (README.md = index, _template.md = template)
|
||||||
docs/ # upstream refs / design notes / research records (active + archive/)
|
docs/ # upstream refs / design notes / research records (active + archive/)
|
||||||
scripts/validate-repo.sh # repo-wide validation (run before merging)
|
scripts/validate-repo.sh # repo-wide validation (run before merging)
|
||||||
|
Makefile # routine validate / health / gated ansible wrappers
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
# VPS ops hub — routine validate / health / gated Ansible wrappers.
|
||||||
|
# See runbooks/ansible-operations.md for playbook semantics.
|
||||||
|
|
||||||
|
SHELL := /usr/bin/env bash
|
||||||
|
.SHELLFLAGS := -eu -o pipefail -c
|
||||||
|
|
||||||
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
|
REPO_ROOT := $(CURDIR)
|
||||||
|
ANSIBLE_DIR := $(REPO_ROOT)/ansible
|
||||||
|
export ANSIBLE_LOCAL_TEMP := $(REPO_ROOT)/.ansible/tmp
|
||||||
|
export ANSIBLE_HOME := $(REPO_ROOT)/.ansible
|
||||||
|
|
||||||
|
LIMIT ?=
|
||||||
|
EXTRA ?=
|
||||||
|
VERBOSE ?= 0
|
||||||
|
CONFIRM ?= 0
|
||||||
|
TARGETS ?=
|
||||||
|
TRAEFIK ?= 0
|
||||||
|
|
||||||
|
LIMIT_FLAG := $(if $(LIMIT),--limit $(LIMIT),)
|
||||||
|
VERBOSE_FLAG := $(if $(filter 1,$(VERBOSE)),-v,$(if $(filter 2,$(VERBOSE)),-vvv,))
|
||||||
|
|
||||||
|
.PHONY: help validate check deps galaxy syntax ansible-prep \
|
||||||
|
ping inventory audit health health-mailcow health-matrix \
|
||||||
|
maint-preview baseline compose-check \
|
||||||
|
install-healthchecks install-matrix-healthchecks compose-deploy reconcile
|
||||||
|
|
||||||
|
help:
|
||||||
|
@printf '%s\n' \
|
||||||
|
'VPS ops hub — make targets (run from repo root)' \
|
||||||
|
'' \
|
||||||
|
'Variables: LIMIT=<group|host> CONFIRM=1 TARGETS=<svc[,svc]> TRAEFIK=1 VERBOSE=0|1|2 EXTRA=...' \
|
||||||
|
'' \
|
||||||
|
'Local / repo:' \
|
||||||
|
' validate, check scripts/validate-repo.sh (pre-merge gate)' \
|
||||||
|
' deps, galaxy ansible-galaxy collection install' \
|
||||||
|
' syntax ansible-playbook --syntax-check all playbooks' \
|
||||||
|
'' \
|
||||||
|
'Read-only remote (ansible):' \
|
||||||
|
' ping ansible managed -m ping' \
|
||||||
|
' inventory ansible-inventory --graph' \
|
||||||
|
' audit playbooks/audit.yml' \
|
||||||
|
' health [LIMIT=…] playbooks/health-report.yml' \
|
||||||
|
' health-mailcow health --limit mailcow' \
|
||||||
|
' health-matrix health --limit matrix' \
|
||||||
|
' maint-preview playbooks/maintenance-preview.yml' \
|
||||||
|
' baseline playbooks/baseline.yml' \
|
||||||
|
' compose-check compose-deploy --check --diff (requires LIMIT=)' \
|
||||||
|
'' \
|
||||||
|
'Mutating (require CONFIRM=1; host-scoped targets require LIMIT=):' \
|
||||||
|
' install-healthchecks playbooks/healthchecks.yml' \
|
||||||
|
' install-matrix-healthchecks playbooks/matrix-healthchecks.yml' \
|
||||||
|
' compose-deploy playbooks/compose-deploy.yml' \
|
||||||
|
' reconcile playbooks/compose-reconcile.yml (requires TARGETS=)' \
|
||||||
|
'' \
|
||||||
|
'Examples:' \
|
||||||
|
' make validate' \
|
||||||
|
' make health LIMIT=mailcow' \
|
||||||
|
' make compose-check LIMIT=vaultwarden' \
|
||||||
|
' make compose-deploy LIMIT=vaultwarden CONFIRM=1' \
|
||||||
|
' make reconcile LIMIT=powerdns TARGETS=auth CONFIRM=1' \
|
||||||
|
' make reconcile LIMIT=vaultwarden TARGETS=vaultwarden TRAEFIK=1 CONFIRM=1' \
|
||||||
|
'' \
|
||||||
|
'Advanced (not wrapped — use ansible-playbook directly):' \
|
||||||
|
' us4-firewalld, unifi-sso, k3s-server, matrix-stack, wireguard-harden,' \
|
||||||
|
' restic, rustdesk, email-alerts, mailcow update runbook'
|
||||||
|
|
||||||
|
validate check:
|
||||||
|
@bash "$(REPO_ROOT)/scripts/validate-repo.sh"
|
||||||
|
|
||||||
|
deps galaxy: ansible-prep
|
||||||
|
@command -v ansible-galaxy >/dev/null 2>&1 || { echo "ansible-galaxy not found; install Ansible first." >&2; exit 1; }
|
||||||
|
@cd "$(ANSIBLE_DIR)" && ansible-galaxy collection install -r requirements.yml
|
||||||
|
|
||||||
|
syntax: ansible-prep
|
||||||
|
@if ! command -v ansible-playbook >/dev/null 2>&1; then \
|
||||||
|
echo "ansible-playbook not found; syntax check skipped." >&2; \
|
||||||
|
exit 0; \
|
||||||
|
fi
|
||||||
|
@fail=0; \
|
||||||
|
for p in "$(ANSIBLE_DIR)"/playbooks/*.yml; do \
|
||||||
|
if ! (cd "$(ANSIBLE_DIR)" && ansible-playbook --syntax-check "playbooks/$$(basename "$$p")" >/dev/null 2>&1); then \
|
||||||
|
echo "syntax-check failed: $$p" >&2; \
|
||||||
|
fail=1; \
|
||||||
|
fi; \
|
||||||
|
done; \
|
||||||
|
exit $$fail
|
||||||
|
|
||||||
|
ansible-prep:
|
||||||
|
@mkdir -p "$(ANSIBLE_HOME)/tmp" "$(ANSIBLE_HOME)/ssh-control"
|
||||||
|
|
||||||
|
define require_ansible
|
||||||
|
@command -v ansible-playbook >/dev/null 2>&1 || { echo "ansible-playbook not found; install Ansible first." >&2; exit 1; }
|
||||||
|
endef
|
||||||
|
|
||||||
|
define require_limit
|
||||||
|
@if [ -z "$(LIMIT)" ]; then \
|
||||||
|
echo "LIMIT is required (e.g. LIMIT=mailcow, LIMIT=vaultwarden, LIMIT=powerdns)." >&2; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
endef
|
||||||
|
|
||||||
|
define require_confirm
|
||||||
|
@if [ "$(CONFIRM)" != "1" ]; then \
|
||||||
|
echo "Mutating operation blocked. Re-run with CONFIRM=1" >&2; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
endef
|
||||||
|
|
||||||
|
define require_targets
|
||||||
|
@if [ -z "$(TARGETS)" ]; then \
|
||||||
|
echo "TARGETS is required (comma-separated service names, e.g. TARGETS=auth or TARGETS=vaultwarden)." >&2; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
endef
|
||||||
|
|
||||||
|
ping: ansible-prep
|
||||||
|
$(require_ansible)
|
||||||
|
@cd "$(ANSIBLE_DIR)" && ansible managed -m ping $(LIMIT_FLAG) $(VERBOSE_FLAG) $(EXTRA)
|
||||||
|
|
||||||
|
inventory: ansible-prep
|
||||||
|
$(require_ansible)
|
||||||
|
@cd "$(ANSIBLE_DIR)" && ansible-inventory --graph $(EXTRA)
|
||||||
|
|
||||||
|
audit: ansible-prep
|
||||||
|
$(require_ansible)
|
||||||
|
@cd "$(ANSIBLE_DIR)" && ansible-playbook playbooks/audit.yml $(LIMIT_FLAG) $(VERBOSE_FLAG) $(EXTRA)
|
||||||
|
|
||||||
|
health: ansible-prep
|
||||||
|
$(require_ansible)
|
||||||
|
@cd "$(ANSIBLE_DIR)" && ansible-playbook playbooks/health-report.yml $(LIMIT_FLAG) $(VERBOSE_FLAG) $(EXTRA)
|
||||||
|
|
||||||
|
health-mailcow: ansible-prep
|
||||||
|
$(require_ansible)
|
||||||
|
@cd "$(ANSIBLE_DIR)" && ansible-playbook playbooks/health-report.yml --limit mailcow $(VERBOSE_FLAG) $(EXTRA)
|
||||||
|
|
||||||
|
health-matrix: ansible-prep
|
||||||
|
$(require_ansible)
|
||||||
|
@cd "$(ANSIBLE_DIR)" && ansible-playbook playbooks/health-report.yml --limit matrix $(VERBOSE_FLAG) $(EXTRA)
|
||||||
|
|
||||||
|
maint-preview: ansible-prep
|
||||||
|
$(require_ansible)
|
||||||
|
@cd "$(ANSIBLE_DIR)" && ansible-playbook playbooks/maintenance-preview.yml $(LIMIT_FLAG) $(VERBOSE_FLAG) $(EXTRA)
|
||||||
|
|
||||||
|
baseline: ansible-prep
|
||||||
|
$(require_ansible)
|
||||||
|
@cd "$(ANSIBLE_DIR)" && ansible-playbook playbooks/baseline.yml $(LIMIT_FLAG) $(VERBOSE_FLAG) $(EXTRA)
|
||||||
|
|
||||||
|
compose-check: ansible-prep
|
||||||
|
$(require_ansible)
|
||||||
|
$(require_limit)
|
||||||
|
@cd "$(ANSIBLE_DIR)" && ansible-playbook playbooks/compose-deploy.yml --check --diff --limit $(LIMIT) $(VERBOSE_FLAG) $(EXTRA)
|
||||||
|
|
||||||
|
install-healthchecks: ansible-prep
|
||||||
|
$(require_ansible)
|
||||||
|
$(require_confirm)
|
||||||
|
@cd "$(ANSIBLE_DIR)" && ansible-playbook playbooks/healthchecks.yml $(LIMIT_FLAG) $(VERBOSE_FLAG) $(EXTRA)
|
||||||
|
|
||||||
|
install-matrix-healthchecks: ansible-prep
|
||||||
|
$(require_ansible)
|
||||||
|
$(require_confirm)
|
||||||
|
@cd "$(ANSIBLE_DIR)" && ansible-playbook playbooks/matrix-healthchecks.yml $(LIMIT_FLAG) $(VERBOSE_FLAG) $(EXTRA)
|
||||||
|
|
||||||
|
compose-deploy: ansible-prep
|
||||||
|
$(require_ansible)
|
||||||
|
$(require_limit)
|
||||||
|
$(require_confirm)
|
||||||
|
@cd "$(ANSIBLE_DIR)" && ansible-playbook playbooks/compose-deploy.yml --limit $(LIMIT) \
|
||||||
|
-e '{"compose_deploy_confirm": true}' $(VERBOSE_FLAG) $(EXTRA)
|
||||||
|
|
||||||
|
reconcile: ansible-prep
|
||||||
|
$(require_ansible)
|
||||||
|
$(require_limit)
|
||||||
|
$(require_targets)
|
||||||
|
$(require_confirm)
|
||||||
|
@json=$$(python3 -c 'import json,sys; t=[x.strip() for x in sys.argv[1].split(",") if x.strip()]; \
|
||||||
|
(not t) and sys.exit("TARGETS must contain at least one non-empty service name"); \
|
||||||
|
d={"service_reconcile_confirm": True, "service_reconcile_targets": t}; \
|
||||||
|
(sys.argv[2]=="1") and d.update({"service_reconcile_restart_traefik": True}); \
|
||||||
|
print(json.dumps(d))' "$(TARGETS)" "$(TRAEFIK)"); \
|
||||||
|
cd "$(ANSIBLE_DIR)" && ansible-playbook playbooks/compose-reconcile.yml --limit $(LIMIT) \
|
||||||
|
-e "$$json" $(VERBOSE_FLAG) $(EXTRA)
|
||||||
@@ -31,9 +31,27 @@ compose_ps() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
check_compose() {
|
check_compose() {
|
||||||
local output
|
local output services bad
|
||||||
output="$(compose_ps)" || { record critical 'compose_ps_failed'; return; }
|
# Only flag containers of *active* services (config --services excludes
|
||||||
if grep -qiE 'Exited|Restarting|[[:space:]]Dead[[:space:]]' <<<"$output"; then
|
# debug/profile-gated services such as vaultwarden's pgweb, which is
|
||||||
|
# intentionally stopped unless started with --profile debug).
|
||||||
|
services="$(docker compose --project-directory '{{ compose_project_dir }}' config --services 2>/dev/null)" || { record critical 'compose_ps_failed'; return; }
|
||||||
|
output="$(docker compose --project-directory '{{ compose_project_dir }}' ps --all --format json 2>&1)" || { record critical 'compose_ps_failed'; return; }
|
||||||
|
bad="$(printf '%s\n' "$output" | python3 -c '
|
||||||
|
import json, sys
|
||||||
|
services = set(sys.argv[1].split())
|
||||||
|
for line in sys.stdin:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
c = json.loads(line)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if c.get("Service") in services and c.get("State") in ("exited", "restarting", "dead"):
|
||||||
|
print(c.get("Service"))
|
||||||
|
' "$services")"
|
||||||
|
if [[ -n "$bad" ]]; then
|
||||||
record critical 'compose_unhealthy_container'
|
record critical 'compose_unhealthy_container'
|
||||||
else
|
else
|
||||||
record ok 'compose_ok'
|
record ok 'compose_ok'
|
||||||
|
|||||||
@@ -14,5 +14,11 @@ rm -f '{{ healthcheck_state_dir }}/latest-{{ healthcheck_profile_scripts[profile
|
|||||||
this_rc="${PIPESTATUS[0]}"
|
this_rc="${PIPESTATUS[0]}"
|
||||||
[ "$this_rc" -gt "$rc" ] && rc="$this_rc"
|
[ "$this_rc" -gt "$rc" ] && rc="$this_rc"
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
aggregate_result{% for profile in healthcheck_profiles %} {{ healthcheck_profile_scripts[profile] | replace('.sh', '') }}{% endfor %}
|
# Collect profile check names line-by-line (robust against Jinja trim_blocks
|
||||||
|
# whitespace control, which would otherwise merge this into one line).
|
||||||
|
aggregate_args=""
|
||||||
|
{% for profile in healthcheck_profiles %}
|
||||||
|
aggregate_args="$aggregate_args {{ healthcheck_profile_scripts[profile] | replace('.sh', '') }}"
|
||||||
|
{% endfor %}
|
||||||
|
aggregate_result $aggregate_args
|
||||||
exit "$rc"
|
exit "$rc"
|
||||||
|
|||||||
@@ -15,11 +15,12 @@ grep -Fq 'vw-db' <<<"$health" || record critical 'postgres_missing'
|
|||||||
check_https 'https://auth.wsvc.info/' '^200$'
|
check_https 'https://auth.wsvc.info/' '^200$'
|
||||||
check_tls_days auth.wsvc.info 443
|
check_tls_days auth.wsvc.info 443
|
||||||
|
|
||||||
# Read effective config only inside the service and report booleans/fingerprints,
|
# Read effective config from the mounted vw-data dir on the host and run the
|
||||||
# never its SMTP password or other secret fields.
|
# SMTP AUTH probe from the host (the vaultwarden image has no python3; the
|
||||||
smtp_result="$(docker compose --project-directory '{{ compose_project_dir }}' exec -T vaultwarden python3 - <<'PY' 2>&1
|
# host does). Never print the SMTP password.
|
||||||
|
smtp_result="$(python3 - <<'PY' 2>&1
|
||||||
import json, pathlib, smtplib, ssl
|
import json, pathlib, smtplib, ssl
|
||||||
cfg=json.loads(pathlib.Path('/data/config.json').read_text())
|
cfg=json.loads(pathlib.Path('{{ compose_project_dir }}/vw-data/config.json').read_text())
|
||||||
host=cfg.get('smtp_host'); port=int(cfg.get('smtp_port') or 0)
|
host=cfg.get('smtp_host'); port=int(cfg.get('smtp_port') or 0)
|
||||||
user=cfg.get('smtp_username')
|
user=cfg.get('smtp_username')
|
||||||
smtp_secret=cfg.get('smtp_password')
|
smtp_secret=cfg.get('smtp_password')
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ 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; 服务器文件为准) |
|
||||||
| `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,24 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Reconcile pgdb compose stack (timescaledb + pgweb + pg-backup) at boot
|
||||||
|
Documentation=file:///opt/database/docker-compose.yml
|
||||||
|
After=network-online.target docker.service
|
||||||
|
Wants=network-online.target
|
||||||
|
Requires=docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
RemainAfterExit=yes
|
||||||
|
WorkingDirectory=/opt/database
|
||||||
|
# Idempotent boot-time reconcile. docker's own restore can fail to bind the
|
||||||
|
# published ports (192.168.55.15:5432/8081) when the VM IP is not yet usable
|
||||||
|
# right after boot (EADDRNOTAVAIL, observed 2026-08-30): timescaledb/pgweb
|
||||||
|
# then stay stopped until a manual `docker compose up`. This unit retries
|
||||||
|
# `docker compose up -d` (a no-op when the stack is healthy) until the port
|
||||||
|
# listens, and force-recreates as a last resort to recover a network-detached
|
||||||
|
# container. Data lives on bind mounts (/srv/pgdata, /opt/database/backups),
|
||||||
|
# so recreation is safe.
|
||||||
|
ExecStart=/bin/bash -c 'for i in $(seq 1 12); do docker compose up -d --remove-orphans; sleep 2; if ss -tln | grep -q "192.168.55.15:5432"; then exit 0; fi; sleep 3; done; echo "pgdb-compose: retries exhausted, force-recreating"; docker compose up -d --force-recreate; sleep 10; ss -tln | grep -q "192.168.55.15:5432"'
|
||||||
|
TimeoutStartSec=180
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Soft Serve initial admin public key (used only on first boot)
|
||||||
|
SOFT_SERVE_INITIAL_ADMIN_KEYS=ssh-ed25519 AAAA... # replace with admin public key
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
FROM alpine:3.20
|
||||||
|
RUN apk add --no-cache sqlite tzdata
|
||||||
|
WORKDIR /scripts
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
services:
|
||||||
|
soft-serve:
|
||||||
|
image: charmcli/soft-serve:v0.12.2
|
||||||
|
container_name: soft-serve
|
||||||
|
restart: unless-stopped
|
||||||
|
# non-root (uid 1000 = windy; 与 backup sidecar BACKUP_UID 一致)
|
||||||
|
user: "1000:1000"
|
||||||
|
|
||||||
|
environment:
|
||||||
|
SOFT_SERVE_DATA_PATH: /var/lib/soft-serve
|
||||||
|
SOFT_SERVE_INITIAL_ADMIN: windy
|
||||||
|
SOFT_SERVE_INITIAL_ADMIN_KEYS: ${SOFT_SERVE_INITIAL_ADMIN_KEYS}
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- ./data:/var/lib/soft-serve
|
||||||
|
- soft-serve-app:/soft-serve
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- traefik
|
||||||
|
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
|
||||||
|
# SSH over TCP via Traefik (entryPoint ssh -> container port 23231)
|
||||||
|
- traefik.tcp.routers.softserve-ssh.entrypoints=ssh
|
||||||
|
- traefik.tcp.routers.softserve-ssh.rule=HostSNI(`*`)
|
||||||
|
- traefik.tcp.routers.softserve-ssh.tls=false
|
||||||
|
- traefik.tcp.services.softserve-ssh.loadbalancer.server.port=23231
|
||||||
|
|
||||||
|
soft-serve-backup:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.backup
|
||||||
|
container_name: soft-serve-backup
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./data:/data: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] soft-serve backup cron installed' &&
|
||||||
|
crond -f -l 8
|
||||||
|
"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
soft-serve-app:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik:
|
||||||
|
external: true
|
||||||
|
name: vw-net
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
umask 077
|
||||||
|
D() { date "+%Y-%m-%d %H:%M:%S"; }
|
||||||
|
TS=$(date +%Y%m%d_%H%M%S)
|
||||||
|
OUT="/backup/soft-serve_${TS}"
|
||||||
|
mkdir -p "$OUT"
|
||||||
|
echo "[$(D)] Starting soft-serve backup -> $OUT"
|
||||||
|
tar czf "$OUT/repos-config.tar.gz" -C /data repos hooks config.yaml ssh
|
||||||
|
sqlite3 /data/soft-serve.db ".backup '$OUT/soft-serve.db'"
|
||||||
|
chmod 600 "$OUT/repos-config.tar.gz" "$OUT/soft-serve.db"
|
||||||
|
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 soft-serve 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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
D() { date "+%Y-%m-%d %H:%M:%S"; }
|
||||||
|
ls -dt /backup/soft-serve_* 2>/dev/null | tail -n +15 | xargs -r rm -rf
|
||||||
|
echo "[$(D)] Pruned. Kept $(ls -d /backup/soft-serve_* 2>/dev/null | wc -l) backups (max 14)"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
vaultwarden:
|
vaultwarden:
|
||||||
image: vaultwarden/server:1.37.1
|
image: vaultwarden/server:1.37.2
|
||||||
container_name: vaultwarden
|
container_name: vaultwarden
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file: ".env"
|
env_file: ".env"
|
||||||
|
|||||||
+198
-7
@@ -228,6 +228,71 @@ cost/ladder sensors can stay `unknown` because CSG
|
|||||||
dashboard uses template ladder/cost entities instead. Do not change
|
dashboard uses template ladder/cost entities instead. Do not change
|
||||||
`templates/csg_sensors.yaml` or the 电力监控 dashboard for an install.
|
`templates/csg_sensors.yaml` or the 电力监控 dashboard for an install.
|
||||||
|
|
||||||
|
**`templates/csg_sensors.yaml` hardened 2026-08-29 (W1N-239):** added
|
||||||
|
`availability` templates to all 12 `csg_*` sensors (numeric sensors can't
|
||||||
|
render `unknown`/`unavailable` in `state`; availability suppresses
|
||||||
|
rendering instead — native CSG down ⇒ derived sensors show `unavailable`,
|
||||||
|
no more fake zeros / "一档" / `0%`). `csg_yesterday_kwh` now falls back to
|
||||||
|
`last_month_by_day`'s last entry when `this_month_by_day` is empty (month
|
||||||
|
start); ladder constants (`t1/t2/p1/p2/p3`) deduped into per-block
|
||||||
|
`variables:` (Block B + Block D); `csg_mom_change` parses `date`
|
||||||
|
defensively. Backup:
|
||||||
|
`/homeassistant/.csg-backups/csg_sensors.yaml.bak-20260829-w1n239`.
|
||||||
|
**Verified:** `ha core check` OK; Core restart required (trigger-based
|
||||||
|
template blocks don't settle on `template.reload` — W1N-114 precedent);
|
||||||
|
post-restart all 12 entities numeric & consistent (302.47 kWh→180.28 元,
|
||||||
|
324.03 kWh→194.06 元, mom_change -3.6%, yesterday 7.66 kWh/2026-08-28),
|
||||||
|
no template errors in Core logs.
|
||||||
|
|
||||||
|
**`csg_sensors.yaml` off-by-one fixed 2026-08-29 (W1N-241):** CSG data
|
||||||
|
lags 1 day (`sum(this_month_by_day)` == `this_month_total_usage`, data
|
||||||
|
stops at yesterday), but templates used `now().day` as "days elapsed" →
|
||||||
|
`csg_predicted_usage` underestimated ~1 daily avg (~3%) and
|
||||||
|
`csg_mom_change` compared this-month 28 days vs last-month 29 days
|
||||||
|
(-3.6% vs true -0.3%). Both now derive the day number from
|
||||||
|
`this_month_by_day[-1].date` (fallback `now().day` when empty). Added
|
||||||
|
`sensor.csg_this_month_daily_avg` (month-to-date avg, 302.47/28=10.8) and
|
||||||
|
`sensor.csg_prediction_progress` (usage/predicted %, 90.3) in Block C
|
||||||
|
(trigger adds `csg_predicted_usage`). Backup:
|
||||||
|
`/homeassistant/.csg-backups/csg_sensors.yaml.bak-20260829-w1n241`.
|
||||||
|
**Verified (8/29):** predicted 324.03→334.81, mom_change -3.6→-0.3,
|
||||||
|
daily_avg 10.8, progress 90.3, predicted_cost 194.06→200.94 (334.81 kWh
|
||||||
|
ladder), ladder cost 180.28 unchanged, `ha core check` OK after restart,
|
||||||
|
no template errors; 14 csg_* entities total.
|
||||||
|
|
||||||
|
**电力监控面板(`lovelace.dashboard_unknown` / view `power-monitor`)
|
||||||
|
updated 2026-08-29 (W1N-240 + W1N-242):** 「本月累计」gauge 对齐夏季阶梯:
|
||||||
|
`max:650`、segments `0/260/600`(绿/橙/红 = 一/二/三档;冬季 11-01 需切
|
||||||
|
`max:450`、`0/200/400` — **seasonal switch point**,见下文)。「📊 统计
|
||||||
|
数据」卡新增本年/去年 4 行(原生传感器,口径标注「电费(账单)」、本年
|
||||||
|
「(至今)」)+ 本月日均/预测进度 2 行(`csg_this_month_daily_avg` /
|
||||||
|
`csg_prediction_progress`,W1N-242);面板共引用 **20** 个实体。改前备份:
|
||||||
|
`/homeassistant/.lovelace-backups/dashboard-unknown-power-monitor-20260829-204845.json`
|
||||||
|
(W1N-240)、`-20260829-210708.json`(W1N-242)
|
||||||
|
(改法:WS `lovelace/config/save`,参数 `url_path: dashboard-unknown` +
|
||||||
|
`config`;勿直改 `.storage/`)。验证:WS 读回 18→20 实体 diff ✓、gauge
|
||||||
|
配置一致 ✓、URL `http://hass.windy.lan:8123/dashboard-unknown/power-monitor`。
|
||||||
|
|
||||||
|
**`csg_sensors.yaml` W1N-242:** `csg_predicted_usage` /
|
||||||
|
`csg_mom_change` / `csg_this_month_daily_avg` 三处取 `days[-1]` 前补
|
||||||
|
`sort(attribute='date')`(与 `csg_yesterday_kwh` 一致,防上游乱序取错
|
||||||
|
数据日)。备份 `csg_sensors.yaml.bak-20260829-w1n242`。验证:Core
|
||||||
|
restart 后回归值不变(334.81 / -0.3 / 10.8 / 90.3 / 200.94 / 180.28)。
|
||||||
|
|
||||||
|
**CSG 长期归档(W1N-243, 2026-08-29):** scribe 库新增 `csg_history`
|
||||||
|
表(逐日 usage/cost/ladder/balance + 逐月累计;2026-07-01 起回填,永久),
|
||||||
|
由 TimescaleDB 每日任务 **1008** `csg_daily_snapshot()`(22:30
|
||||||
|
Asia/Shanghai;**TS job 非 pg_cron**)upsert 维护。日费用在原生
|
||||||
|
`latest_day_cost` 缺失时回退 = 昨日用电 × 当前档费率(模板
|
||||||
|
`csg_current_ladder_tariff` 0.639);月费用回退模板
|
||||||
|
`csg_this_month_ladder_cost`。**语义**:day 行 usage/cost 为该日值,
|
||||||
|
ladder/balance 为 22:30 快照值。详见 [hosts/pgdb.md](../hosts/pgdb.md)。
|
||||||
|
|
||||||
|
> **Seasonal gauge switch (W1N-240 已知事项):** 每年 **11-01** 把
|
||||||
|
> `power-monitor` 视图「本月累计」gauge 切到冬季 `max:450` /
|
||||||
|
> `0/200/400`,**5-01** 切回夏季 `max:650` / `0/260/600`(与模板
|
||||||
|
> `now().month` 季节逻辑对齐;模板常量在 Block B/D `variables`)。
|
||||||
|
|
||||||
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). `end0` 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`.
|
||||||
@@ -322,9 +387,12 @@ deployed 2026-08-18 from `216cc99` (backup
|
|||||||
### Scribe long-term history (verified 2026-08-29)
|
### Scribe long-term history (verified 2026-08-29)
|
||||||
|
|
||||||
- **Scribe 3.8.0** (`/homeassistant/custom_components/scribe/`), configured from
|
- **Scribe 3.8.0** (`/homeassistant/custom_components/scribe/`), configured from
|
||||||
`configuration.yaml` (`scribe:` block; config entry
|
`/homeassistant/scribe.yaml` — W1N-238 moved the block out of
|
||||||
|
`configuration.yaml` on 2026-08-29 (main config now carries
|
||||||
|
`scribe: !include scribe.yaml`; content moved verbatim; backup
|
||||||
|
`configuration.yaml.bak-20260829-201724-w1n238`). Config entry
|
||||||
`01KC2VFJWEQ3XDHY6TQKHPDVRB`, `source: import` — UI "Configure → Advanced"
|
`01KC2VFJWEQ3XDHY6TQKHPDVRB`, `source: import` — UI "Configure → Advanced"
|
||||||
edits are overridden by the YAML on restart; treat YAML as authoritative).
|
edits are overridden by the YAML on restart; treat YAML as authoritative.
|
||||||
- TimescaleDB at `192.168.55.15:5432/scribe` (DB user `hass`; host in inventory,
|
- TimescaleDB at `192.168.55.15:5432/scribe` (DB user `hass`; host in inventory,
|
||||||
see [hosts/pgdb.md](../hosts/pgdb.md)). Database re-initialized 2026-08-29 14:06 CST
|
see [hosts/pgdb.md](../hosts/pgdb.md)). Database re-initialized 2026-08-29 14:06 CST
|
||||||
(user-handled; earlier `relation "entities" does not exist` errors resolved).
|
(user-handled; earlier `relation "entities" does not exist` errors resolved).
|
||||||
@@ -342,11 +410,134 @@ 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** (upstream feature, newer versions
|
- **Scribe 3.8.0 has no retention option.** Retention ships only in the v4.x line,
|
||||||
only). Retention requires upgrading Scribe first; the DB is a fresh 3.x-schema
|
which as of 2026-08-29 has no stable release (v4.0.0rc1/v4.1.0rc1 are
|
||||||
so an upgrade is safe. Do not expect retention YAML keys to validate on 3.8.0.
|
prereleases; user declined RCs — data keeps growing until an upgrade). v4.x is
|
||||||
- Recorder stays external-Postgres with `purge_keep_days: 30` for daily UI
|
a major rewrite (writer.py largely rewritten, migration.py removed, TimescaleDB
|
||||||
history; Scribe is the permanent archive.
|
extension required): re-read release notes before upgrading. Do not expect
|
||||||
|
retention YAML keys to validate on 3.8.0.
|
||||||
|
- 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
|
||||||
|
native UI per-change history; Scribe is the permanent archive. Long-term
|
||||||
|
statistics stay permanent (not purged by `purge_keep_days`). Note:
|
||||||
|
extending retention does **not** recover pre-2026-08-29 raw history
|
||||||
|
(already purged); only `csg_history` day/month values cover that period.
|
||||||
|
|
||||||
|
### Config layout: scribe.yaml + templates/ merge (W1N-238, verified 2026-08-29)
|
||||||
|
|
||||||
|
- `configuration.yaml` line 29: `scribe: !include scribe.yaml`; line 9:
|
||||||
|
`template: !include_dir_merge_list templates`. No `packages/`.
|
||||||
|
- `scribe.yaml` (config root): the Scribe block, content identical to the
|
||||||
|
former inline one; import semantics unchanged.
|
||||||
|
- `templates/`: `csg_sensors.yaml` (12 template sensors, top-level **list**)
|
||||||
|
+ `quick_sensors.yaml` (scaffold for Quick-derived `quick_*` sensors, empty
|
||||||
|
list with convention header). **`!include_dir_merge_list` merges per-file
|
||||||
|
lists; non-list files are silently skipped** — every file in `templates/`
|
||||||
|
must be a top-level list (`- sensor:` blocks). Directory include only picks
|
||||||
|
up `*.yaml`, so the `.bak` / `.pre-*` backups in the dir are ignored. After
|
||||||
|
adding sensors, verify template-platform entity count = 12 + N (entity
|
||||||
|
registry `platform: template`).
|
||||||
|
- Convention (per review + W1N-233): pure sums/averages stay min_max helpers
|
||||||
|
(e.g. `sensor.dang_qian_zong_gong_lu`); only template-logic derivations
|
||||||
|
(ladder pricing, cross-entity conditions) go into `quick_sensors.yaml`.
|
||||||
|
- Post-change verification 20:19 CST: `ha core check` ok, 92 s restart
|
||||||
|
(2026.8.3), `binary_sensor.scribe_database_connection` on,
|
||||||
|
`scribe_states_written` 18581→19426 growing, template entities still 12,
|
||||||
|
csg sensors numeric, no scribe/template log errors.
|
||||||
|
|
||||||
|
### Timescale Plotly card + database reader (verified 2026-08-29)
|
||||||
|
|
||||||
|
Chart stack over the Scribe TimescaleDB archive. Upstream pair (no HACS;
|
||||||
|
manual copies): reader `remmob/timescale_database_reader` **v1.1.0** (main
|
||||||
|
`bb8776a`) + card `remmob/timescale-plotly-card` **2.2.0** (main `217961d`).
|
||||||
|
|
||||||
|
- **Reader integration**: `/homeassistant/custom_components/timescale_database_reader/`.
|
||||||
|
Config entry `01M165P77QT1FQEAVPNZHDT82W` ("Scribe", `source: user`): connects
|
||||||
|
`hass@192.168.55.15:5432/scribe` (credentials = `secrets.yaml` `scribe_url`),
|
||||||
|
`table: sensor_minute`. Exposes no entities/services — it serves WS command
|
||||||
|
`timescale/query` (window ≤ 365 d, ≤ 50 000 rows, `downsample` bucket seconds).
|
||||||
|
Benign startup warning `Error executing test query: column "time" does not
|
||||||
|
exist`: the self-test SQL assumes the LTSS column name; the scribe table uses
|
||||||
|
`minute` — real queries work (verified: 70 rows for a live power sensor).
|
||||||
|
- **Card**: `/homeassistant/www/community/timescale-plotly-card/timescale-plotly-card.js`
|
||||||
|
(root-owned, same convention as HACS dirs). Lovelace resource (storage)
|
||||||
|
id `2e360d17b5aa4ce59c2fd13c43b51215` →
|
||||||
|
`/hacsfiles/timescale-plotly-card/timescale-plotly-card.js`, type `module`.
|
||||||
|
Card config matches the entry by `database: scribe` (name from the reader
|
||||||
|
entry). Updates: replace the file, resource URL unchanged — browsers need a
|
||||||
|
hard refresh or a bumped `?v=` query on the resource URL.
|
||||||
|
- **pgdb side** (`sensor_minute_aggregate` cagg + `sensor_minute` hypertable +
|
||||||
|
every-minute refresh job): see [hosts/pgdb.md](pgdb.md) § Databases.
|
||||||
|
- **Agent-side HA WebSocket without a long-lived token** (verified 2026-08-29):
|
||||||
|
connect `ws://supervisor/core/websocket` with header
|
||||||
|
`Authorization: Bearer $SUPERVISOR_TOKEN`, then send
|
||||||
|
`{"type":"auth","access_token":"$SUPERVISOR_TOKEN"}` — the Supervisor proxy
|
||||||
|
swaps it for a core token (works as the internal Supervisor admin user). Note
|
||||||
|
`lovelace/resources/create` in HA 2026.8 takes `res_type` (NOT
|
||||||
|
`resource_type`).
|
||||||
|
- Scribe stores numeric sensor values in `states_raw.value` with `state` NULL,
|
||||||
|
so `sensor_minute.state` shows `'0'` for numeric sensors; the card plots
|
||||||
|
`avg_state` (from `value`) — expected, not a bug.
|
||||||
|
- **Quick 仪表盘(`dashboard-quick`)图表套件**(2026-08-29 创建,经 WS
|
||||||
|
`lovelace/config/save` 写入;W1N-230 修复 + W1N-231 round-2 改进):
|
||||||
|
5 张 timescale 卡——大功率电器/常驻负载功率(按量级拆图,避免尖峰压扁
|
||||||
|
<70 W 基线)、按插座用电量(`energy_mode` + cumulative/diff,数据质量前提
|
||||||
|
见 pgdb 的 refresh 过程补丁)、室内外温湿度(温度左轴/湿度右轴,4 位置同色
|
||||||
|
配对)、人体感应活动状态(3 个 `motion_state`,banded `state_map`
|
||||||
|
none/small/medium/large → 0-11,per-entity `line_color` 红/蓝/绿)。
|
||||||
|
空调实体引用为 `kong_diao_*`(`kong_tiao` 是笔误,W1N-230 修复;`grep -c
|
||||||
|
kong_tiao` 应为 0)。灯区:2×2 嵌套 grid(`grid_options: {columns: "full"}`,
|
||||||
|
内层 `columns: 2`)+ 4 卡统一 `mushroom-light-card`(显式 name、
|
||||||
|
`use_light_color: false`、内联亮度/色温控制),heading icon
|
||||||
|
`mdi:lightbulb-group`。heading badges:环境 4 温度(迷你/mini数显/数显/广州)、
|
||||||
|
大功率电器 空调/电脑当前功率、常驻负载 总功率
|
||||||
|
(`sensor.dang_qian_zong_gong_lu`,min_max **sum** helper,`round_digits: 0`,
|
||||||
|
任一源掉线 fail-closed → unknown)。常驻负载图卡级 `fill: 'tozeroy'` +
|
||||||
|
冰箱/主网络 per-entity `fill_color`(线色 20% 透明)+ 其余 5 条 `fill: false`
|
||||||
|
(per-entity fill 逐系列退出,卡 JS `seriesConfig.fill !== false`)。
|
||||||
|
布局:视图 `type: sections` + `max_columns: 4`;灯/用电/环境/人体感应
|
||||||
|
`column_span: 4`,功率两图拆两个 `column_span: 2` 分区**并排**(等高 280px,
|
||||||
|
桌面并排、手机回落堆叠;去卡内 title 省半宽图垂直空间)。
|
||||||
|
**分区/卡片是两套尺寸键,不可混用**:分区宽 = `column_span`
|
||||||
|
(`hui-sections-view.ts` 缺省按 1 列渲染,绝不省略);卡片宽 =
|
||||||
|
`grid_options: {columns: <n|"full">}`(`hui-card.ts` 只读 `config.grid_options`,
|
||||||
|
写在卡片上的 `column_span` 被静默忽略;缺省 12 列,分区内格 = 12 × 分区
|
||||||
|
span,故 span-4 分区里缺省卡片只有 1/4 宽)。
|
||||||
|
修改前备份:`/homeassistant/.lovelace-backups/dashboard-quick-*.json`
|
||||||
|
(W1N-230 修复: `20260829-190256`;round-2 改进: `20260829-194040`)。
|
||||||
|
|
||||||
|
### 地图仪表盘:CARTO keyed tiles via `custom:map-card` (verified 2026-08-30, W1N-261)
|
||||||
|
|
||||||
|
- **背景:** CARTO 自 2026-08-26 起对无 key 栅格瓦片打 "API KEY REQUIRED"
|
||||||
|
水印,内置地图卡/zone 编辑器全部受影响。Core 2026.8.3 的 `MapCardConfig`
|
||||||
|
**没有任何瓦片配置项**(frontend 20260729.7 源码核对:
|
||||||
|
`setup-leaflet-map.ts` 硬编码 CARTO voyager URL)。上游修复是 2026.9.0b1
|
||||||
|
起改用 OSMF 矢量瓦片(frontend PR #53816),stable 预计 2026-09-02 前后。
|
||||||
|
- **变更:** 「地图」仪表盘(url_path `map`,storage)唯一 map 卡替换为
|
||||||
|
`custom:map-card`([nathan-gs/ha-map-card](https://github.com/nathan-gs/ha-map-card)
|
||||||
|
**v1.16.0**,手动安装非 HACS):`tile_layer_url` =
|
||||||
|
`https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png?key=<CARTO_KEY>`
|
||||||
|
(配 `tile_layer_options: {subdomains: abcd, maxZoom: 20}` + OSM/CARTO
|
||||||
|
attribution)。实体不变:2 person + 4 zone(zone 用 `display: icon` +
|
||||||
|
`circle: auto`,circle 读实体 `radius` 属性画半径圈)。
|
||||||
|
- **CARTO key 是 secret**: 只存在于服务端 lovelace 存储(dashboard `map`
|
||||||
|
的卡片配置)和用户本人处;勿写入本仓库或 Linear。
|
||||||
|
- **文件/资源:** `/homeassistant/www/community/ha-map-card/map-card.js`
|
||||||
|
(root:root 644,678554 B,sha256
|
||||||
|
`f30dfb606e858d2216d5198d8cf758ce956d127006ebd7d66d4329153a247ec2`);
|
||||||
|
Lovelace resource(storage)id `9d2b50b52c60420d89ebd041f722cf60` →
|
||||||
|
`/hacsfiles/ha-map-card/map-card.js`,type module(WS
|
||||||
|
`lovelace/resources/create`,2026.8 参数名 `res_type`)。升级 = 手动替换
|
||||||
|
该文件(不在 HACS 管理下,浏览器需强刷)。
|
||||||
|
- **备份:** `/homeassistant/.lovelace-backups/dashboard-map-map-20260830-133714.json`
|
||||||
|
(还原 = 把备份里的 `views[0].cards[0]` 写回后再 WS `lovelace/config/save`
|
||||||
|
url_path `map`)。
|
||||||
|
- **验证 8/30:** 同瓦片无 key=水印 / 带 key=干净(256×256 PNG 视觉对比);
|
||||||
|
resource HTTP 200 text/javascript;WS 读回卡片配置(type/entities/key/
|
||||||
|
attribution/options)全部符合;HA 主机 `curl -4` 带 key 瓦片 200。
|
||||||
|
- **Follow-up:** Core 升 2026.9.0 stable 后内置地图/zone 编辑器自动切
|
||||||
|
OSMF 矢量瓦片;届时可保留 custom 卡(继续 keyed CARTO)或用备份还原
|
||||||
|
内置卡。zone 编辑器等其余内置地图的水印在 2026.9 前无解。
|
||||||
|
|
||||||
## Known issues
|
## Known issues
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -18,7 +18,7 @@
|
|||||||
| DB | Owner | Size | 用途 |
|
| DB | Owner | Size | 用途 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `hass` | hass | ~14 MB | HA recorder(states/events/statistics),客户端 HAOS `192.168.55.11` |
|
| `hass` | hass | ~14 MB | HA recorder(states/events/statistics),客户端 HAOS `192.168.55.11` |
|
||||||
| `scribe` | postgres | ~11 MB | HA scribe 集成(entities/areas/devices 注册表同步 + `states_raw` hypertable) |
|
| `scribe` | postgres | ~73 MB | HA scribe 集成(entities/areas/devices 注册表同步 + `states_raw` hypertable + `csg_history` 长期归档表) |
|
||||||
| `postgres` | postgres | ~9 MB | 默认库 |
|
| `postgres` | postgres | ~9 MB | 默认库 |
|
||||||
|
|
||||||
## Ops notes
|
## Ops notes
|
||||||
@@ -35,17 +35,23 @@
|
|||||||
- **备份**:由 `pg-backup` 容器接管(2026-08-29),宿主机 cron 与 `/opt/database/pg-backup.sh` 已退役。恢复用 `pg_restore`(custom format)——2026-08-29 已实测还原 hass 库 dump(states 10014 行)成功。
|
- **备份**:由 `pg-backup` 容器接管(2026-08-29),宿主机 cron 与 `/opt/database/pg-backup.sh` 已退役。恢复用 `pg_restore`(custom format)——2026-08-29 已实测还原 hass 库 dump(states 10014 行)成功。
|
||||||
- **认证**:外部连接 scram-sha-256(密码必填,改密码有效);容器内 loopback 为 trust(官方镜像默认)。
|
- **认证**:外部连接 scram-sha-256(密码必填,改密码有效);容器内 loopback 为 trust(官方镜像默认)。
|
||||||
- **回滚**:旧启动命令保留在 `/opt/database/run`(容器无状态,数据在 /srv/pgdata);旧匿名卷 `9375195843b950f4e04c34872409ca095e1136520dd019a8e86e2794be06c236`(根盘 ~82M)保留作兜底,确认稳定后可 `docker volume rm`。
|
- **回滚**:旧启动命令保留在 `/opt/database/run`(容器无状态,数据在 /srv/pgdata);旧匿名卷 `9375195843b950f4e04c34872409ca095e1136520dd019a8e86e2794be06c236`(根盘 ~82M)保留作兜底,确认稳定后可 `docker volume rm`。
|
||||||
|
- **开机自愈**(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)(只读健康检查)、[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)`。
|
||||||
|
|
||||||
## Known issues
|
## Known issues
|
||||||
|
|
||||||
- 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:**`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 是真实待机读数,保留。
|
||||||
- `hass` 库的 recorder 表仍为普通表(无 hypertable);`scribe` 集成负责时间序列历史(`states_raw` + `events` hypertable)。
|
- `hass` 库的 recorder 表仍为普通表(无 hypertable);`scribe` 集成负责时间序列历史(`states_raw` + `events` hypertable)。
|
||||||
|
|
||||||
## Verification history
|
## Verification history
|
||||||
|
|
||||||
|
- 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/`。
|
||||||
- 2026-08-29:**运维 runbook 落地**(W1N-228,已验收):新增 `runbooks/pgdb-health.md`(只读,8 项诊断全绿)、`pgdb-restore.md`(流程式,temp-DB 安全还原 + 审批门)、`pgdb-update.md`(门控命令式,回滚=/opt/database/run + 旧卷);README 索引与 validate-repo.sh 分类同步更新;runbook 命令已对活主机逐条实测(含 `pg_restore -l` 校验当日 dump)。同日修正:SSH key auth 可用(facts 原记"密钥未安装"已过时);scribe 新增 `events` hypertable。
|
- 2026-08-29:**运维 runbook 落地**(W1N-228,已验收):新增 `runbooks/pgdb-health.md`(只读,8 项诊断全绿)、`pgdb-restore.md`(流程式,temp-DB 安全还原 + 审批门)、`pgdb-update.md`(门控命令式,回滚=/opt/database/run + 旧卷);README 索引与 validate-repo.sh 分类同步更新;runbook 命令已对活主机逐条实测(含 `pg_restore -l` 校验当日 dump)。同日修正:SSH key auth 可用(facts 原记"密钥未安装"已过时);scribe 新增 `events` hypertable。
|
||||||
|
- 2026-08-29:**CSG 长期归档 + recorder 365d**(W1N-243):建 `csg_history` 表 + attributes 回填(逐日 59 + 逐月 19)+ 每日任务 1008(函数 v2:最新有值行读取、日费用阶梯回退);hass `purge_keep_days` 30→365(备份 `configuration.yaml.bak-20260829-purge365`)。见 Linear vps W1N-243。
|
||||||
|
|||||||
+19
-10
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
## Vaultwarden (Bitwarden-compatible)
|
## Vaultwarden (Bitwarden-compatible)
|
||||||
|
|
||||||
**Status: operational** (Postgres live, HTTPS 200, healthy containers, SMTP AUTH OK — last probe 2026-08-01 18:55 CST).
|
**Status: operational** (Postgres live, HTTPS 200, healthy containers, SMTP AUTH OK — last probe 2026-08-29).
|
||||||
|
|
||||||
Upstream docs: [docs/vaultwarden-upstream.md](../docs/vaultwarden-upstream.md)
|
Upstream docs: [docs/vaultwarden-upstream.md](../docs/vaultwarden-upstream.md)
|
||||||
|
|
||||||
@@ -21,9 +21,9 @@ Upstream docs: [docs/vaultwarden-upstream.md](../docs/vaultwarden-upstream.md)
|
|||||||
| Env file | `/opt/vaultwarden/.env` |
|
| Env file | `/opt/vaultwarden/.env` |
|
||||||
| Admin overrides | `/opt/vaultwarden/vw-data/config.json` (**wins over env**) |
|
| Admin overrides | `/opt/vaultwarden/vw-data/config.json` (**wins over env**) |
|
||||||
| Public URL / `DOMAIN` | `https://auth.wsvc.info` |
|
| Public URL / `DOMAIN` | `https://auth.wsvc.info` |
|
||||||
| Image | `vaultwarden/server:1.37.1` (pinned) |
|
| Image | `vaultwarden/server:1.37.2` (pinned) |
|
||||||
| Live DB | **Postgres 16** (`vw-db` / service `pg`) via compose `DATABASE_URL` |
|
| Live DB | **Postgres 16** (`vw-db` / service `pg`) via compose `DATABASE_URL` |
|
||||||
| Data (probe) | users=1, ciphers=1327 |
|
| Data (probe) | users=1, ciphers=1360 |
|
||||||
| Cold SQLite | `backups/sqlite-cold/db.sqlite3.pre-pg-20260801` (not used live) |
|
| Cold SQLite | `backups/sqlite-cold/db.sqlite3.pre-pg-20260801` (not used live) |
|
||||||
| Pre-migrate backup | `backups/pre-pg-migrate-20260801_161204/` |
|
| Pre-migrate backup | `backups/pre-pg-migrate-20260801_161204/` |
|
||||||
| Data dir | `./vw-data` → `/data` (attachments, rsa keys, `config.json`) |
|
| Data dir | `./vw-data` → `/data` (attachments, rsa keys, `config.json`) |
|
||||||
@@ -50,7 +50,7 @@ Upstream docs: [docs/vaultwarden-upstream.md](../docs/vaultwarden-upstream.md)
|
|||||||
|
|
||||||
| Container | Status |
|
| Container | Status |
|
||||||
|-----------|--------|
|
|-----------|--------|
|
||||||
| `vaultwarden` | Up (healthy), `vaultwarden/server:1.37.1` |
|
| `vaultwarden` | Up (healthy), `vaultwarden/server:1.37.2` |
|
||||||
| `vw-db` | Up (healthy) — **live** Postgres |
|
| `vw-db` | Up (healthy) — **live** Postgres |
|
||||||
| `vaultwarden-backup` | Up (`pg_dump`) |
|
| `vaultwarden-backup` | Up (`pg_dump`) |
|
||||||
| `vaultwarden-pgweb` | Exited (profile `debug`) |
|
| `vaultwarden-pgweb` | Exited (profile `debug`) |
|
||||||
@@ -80,7 +80,16 @@ ansible-playbook playbooks/compose-reconcile.yml --limit vaultwarden \
|
|||||||
|
|
||||||
| Container | Status | Image / notes |
|
| Container | Status | Image / notes |
|
||||||
|-----------|--------|---------------|
|
|-----------|--------|---------------|
|
||||||
| `soft-serve` | Up | `ghcr.io/charmbracelet/soft-serve:latest` (`repo.windy.me:2222`) |
|
| `soft-serve` | Up | `charmcli/soft-serve:v0.12.2` (`repo.windy.me:2222`) |
|
||||||
|
| `soft-serve-backup` | Up | alpine + sqlite3 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)):
|
||||||
|
- `/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)
|
||||||
|
- **镜像已固定** `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)
|
||||||
|
- **非 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.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`(未对外暴露)
|
||||||
|
- **备份**: 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)
|
||||||
| `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 |
|
||||||
|
|
||||||
@@ -88,11 +97,11 @@ Directories for `authelia`, `conduit`, `dendrite`, `mastodon`, `rustdesk`, `zita
|
|||||||
|
|
||||||
## Verified
|
## Verified
|
||||||
|
|
||||||
Last checked: **2026-08-01 18:55 CST** — operational.
|
Last checked: **2026-08-29** — operational.
|
||||||
|
|
||||||
- `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`)
|
||||||
- Identity wrong-password → **400** business error (DB readable, not 500)
|
- SMTP: container → `mx2:587` OK; **AUTH OK** with effective `config.json` password (synced with `.env` / `.smtp-credentials`, fingerprint match)
|
||||||
- SMTP: container → `mx2:587` OK; STARTTLS cert CN=`mx2.windy.me`; **AUTH OK** with effective `config.json` password (synced with `.env` / `.smtp-credentials`)
|
- PG counts: users=1, ciphers=1360
|
||||||
- LE cert CN=`auth.wsvc.info`
|
- Image `vaultwarden/server:1.37.2` (**upgraded 2026-08-29** from 1.37.1; required for Bitwarden clients 2026.8.0+); post-upgrade 404 fixed by Traefik restart, then 200
|
||||||
- PG counts: users=1, ciphers=1327
|
- vps-health local check **installed 2026-08-29** (`vps-healthcheck.timer` daily 06:15 + `/usr/local/lib/vps-health/run`); `health-report.yml --limit vaultwarden` now passes (**ok**, was failing due to missing check infra + script bugs fixed: trim_blocks render, pgweb debug-profile false positive, SMTP probe moved host-side since image lacks python3)
|
||||||
|
|||||||
@@ -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 / Core 2026.8.1 / Supervisor 2026.07.5 (verified 2026-08-14)
|
Upstream: HAOS 18.1 / Supervisor 2026.07.5 (verified 2026-08-14); Core 2026.8.3 (verified 2026-08-29)
|
||||||
|
|
||||||
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
|
||||||
@@ -284,8 +284,10 @@ print(urllib.request.urlopen(req, timeout=60).status)
|
|||||||
|
|
||||||
7. **Do not edit the dashboard or `templates/csg_sensors.yaml` for an
|
7. **Do not edit the dashboard or `templates/csg_sensors.yaml` for an
|
||||||
install.** Entity IDs did not change across v1.3.0/v1.3.1/v1.3.2.
|
install.** Entity IDs did not change across v1.3.0/v1.3.1/v1.3.2.
|
||||||
Template `| float(0)` turns native `unavailable` into fake zeros; that
|
(The old `| float(0)` fake-zero follow-up was resolved 2026-08-29 by
|
||||||
is a follow-up, not part of the zip install.
|
W1N-239: template sensors now carry `availability` templates and show
|
||||||
|
`unavailable` instead of fake zeros when native CSG sensors are down.
|
||||||
|
Template edits go through that issue, not the install path.)
|
||||||
|
|
||||||
#### Verify (CSG, after v1.3.2 / W1N-118)
|
#### Verify (CSG, after v1.3.2 / W1N-118)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user