wger: clearer Postgres connection errors in the PowerSync import, reuse SECRET_KEY, doc fixes (#3123)

- target_database_state() reports the real connection error, fails at once
  on errors waiting cannot fix (wrong password, missing role/database,
  pg_hba) and fails closed on any unexpected probe output.
- 89-powersync-run.sh reuses SECRET_KEY from /data/secrets.env, so its
  manage.py calls no longer print upstream's missing-key warning.
- README: internal Postgres hostname in the example; safe password
  characters (PowerSync does not percent-decode the URI).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
BirdNET-Go Addon BuilderandClaude Opus 5.5 committed 2026-10-21 15:08:15 +02:00
1 parent 09ca012050
commit 876729f0fb
4 files changed
+76 -17

No files matched your search

+6
View File
@@ -1,3 +1,9 @@
## 2.8.0-dev.1.4 (2026-10-21)
- Show the PostgreSQL connection error once while the PowerSync import check waits, retry transient connection failures, and fail immediately for errors such as a wrong password or missing role/database (#3123).
- Reuse the persistent `SECRET_KEY` before PowerSync runs wger management commands, so the session-persistence warning no longer appears after the add-on has started once (#3123).
- README: use the Postgres 17 add-on's internal hostname in the PowerSync example, so port 5432 can stay closed, and say which characters a database password can safely contain (#3123).
## 2.8.0-dev.1.3 (2026-10-20)
- Fix the PowerSync SQLite import failing with `CommandError: Unknown model: admin.logentry`, which kept wger from starting. wger does not install Django's admin app, so the import no longer asks `dumpdata` to exclude its log table (#3134).
+11 -2
View File
@@ -168,9 +168,9 @@ option (the `CONFIG_LOCATION` config.yaml file documented above works for these
```yaml
env_vars:
- name: PS_DATABASE_URI
value: postgres://wger:yourpassword@192.168.1.10:5432/wger
value: postgres://wger:yourpassword@db21ed7f-postgres-latest:5432/wger
- name: PS_STORAGE_PG_URI
value: postgres://powersync_storage:anotherpassword@192.168.1.10:5432/wger
value: postgres://powersync_storage:anotherpassword@db21ed7f-postgres-latest:5432/wger
- name: SITE_URL
value: https://wger.yourdomain.com
```
@@ -184,6 +184,15 @@ env_vars:
add-on creates. It must be set, it has no useful default.
- `SITE_URL` must be an absolute `http://` or `https://` URL with no path.
`db21ed7f-postgres-latest` is the internal hostname of this repository's Postgres 17
add-on, so it works without exposing port 5432. A LAN IP also works when PostgreSQL is
running on an external server.
Use passwords made only of letters, digits and `-._~`. Characters such as `@`, `:`,
`/`, `?`, `#` or `%` break the URI, and percent-encoding them is no way out: wger
decodes `%40` back to `@`, but PowerSync passes it to PostgreSQL unchanged, so its
connections fail to authenticate.
Set these through the `env_vars` option rather than the `CONFIG_LOCATION` file if the
password contains a `$` or a backtick. Both routes work, but the config.yaml route
writes the value unquoted into the add-on's environment file, where the shell expands
+1 -1
View File
@@ -27,5 +27,5 @@ schema:
slug: wger
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "2.8.0-dev.1.3"
version: "2.8.0-dev.1.4"
webui: "[PROTO:ssl]://[HOST]:[PORT:80]"
+58 -14
View File
@@ -24,6 +24,7 @@ NGINX_PS_DIR=/etc/nginx/ps
JWT_DIR=/data/powersync
JWT_FILE="$JWT_DIR/jwt.env"
ENV_FILE=/.env
SECRETS_FILE=/data/secrets.env
# A fresh install runs bootstrap, collectstatic, migrations and the exercise sync
# before gunicorn answers, which upstream and the add-on README both put in the
@@ -118,6 +119,19 @@ django_manage() {
(cd /home/wger/src && sudo -u wger -E python3 manage.py "$@")
}
# On the first boot 90-run.sh has not generated SECRET_KEY yet, so one warning is expected.
prepare_secret_key() {
local secret_key=""
[ -n "${SECRET_KEY:-}" ] && return 0
if [ -f "$SECRETS_FILE" ]; then
secret_key="$(sed -n 's/^SECRET_KEY=//p' "$SECRETS_FILE" | head -n 1)"
fi
if [ -n "$secret_key" ]; then
export SECRET_KEY="$secret_key"
fi
}
# The same, pointed at a SQLite file instead. PS_DATABASE_URI has to go: wger's
# settings/main.py checks it before the DJANGO_DB_* block, so it would win.
sqlite_manage() {
@@ -244,16 +258,27 @@ prepare() {
# Mirrors wger's own database_initialised() in wger/tasks.py: a database that cannot be
# reached is an error rather than an empty database, and "no users at all" is upstream's
# definition of a database that was never set up, because the admin user is created as
# the last step of its bootstrap. Prints unreachable, fresh (reachable, no tables yet),
# empty (tables but no users) or populated.
# the last step of its bootstrap. Only connection failures that waiting cannot fix are
# fatal; every other OperationalError is retried. Prints a state, a tab and a one-line
# detail.
target_database_state() {
django_manage shell 2> /dev/null << 'PROBE' | tail -n 1
from django.db import DatabaseError, connection
from django.db import DatabaseError, OperationalError, connection
try:
connection.ensure_connection()
except Exception:
print('unreachable')
except Exception as error:
# psycopg can spread one failure over several lines, one per address tried.
message = ' '.join(str(error).split()) or type(error).__name__
fatal_messages = (
'password authentication failed',
'does not exist',
'no pg_hba.conf entry',
)
fatal = not isinstance(error, OperationalError) or any(
text in message.lower() for text in fatal_messages
)
print(f"{'fatal' if fatal else 'retryable'}\t{message}")
else:
from django.contrib.auth.models import User
@@ -277,7 +302,7 @@ PROBE
# pointed at is the live one.
import_sqlite_data() {
local sqlite_db="${DJANGO_DB_DATABASE:-/data/database.sqlite}"
local waited=0 state=""
local waited=0 probe="" state="" detail=""
local dump="$IMPORT_DIR/dump.json" copy="$IMPORT_DIR/database.sqlite"
if [ ! -s "$sqlite_db" ]; then
@@ -288,16 +313,33 @@ import_sqlite_data() {
bashio::log.info "Checking whether the Postgres database already holds wger data"
while true; do
state="$(target_database_state)"
[ "$state" != "unreachable" ] && break
if [ "$waited" -ge "$POSTGRES_WAIT_SECONDS" ]; then
bashio::log.error "PowerSync: the Postgres database is still unreachable after ${POSTGRES_WAIT_SECONDS}s"
probe="$(target_database_state)"
IFS=$'\t' read -r state detail <<< "$probe"
case "$state" in
fresh | empty | populated)
break
;;
retryable)
if [ "$waited" -ge "$POSTGRES_WAIT_SECONDS" ]; then
bashio::log.error "PowerSync: the Postgres database is still unreachable after ${POSTGRES_WAIT_SECONDS}s: ${detail:-no error detail}"
bashio::log.error "PowerSync: the SQLite database itself is untouched at $sqlite_db"
return 1
fi
[ "$waited" -eq 0 ] && bashio::log.info "PowerSync: waiting for the Postgres database: ${detail:-no error detail}"
sleep 5
waited=$((waited + 5))
;;
fatal)
bashio::log.error "PowerSync: the Postgres connection error cannot be fixed by waiting: ${detail:-no error detail}"
bashio::log.error "PowerSync: the SQLite database itself is untouched at $sqlite_db"
return 1
fi
[ "$waited" -eq 0 ] && bashio::log.info "PowerSync: waiting for the Postgres database"
sleep 5
waited=$((waited + 5))
;;
*)
bashio::log.error "PowerSync: the Postgres probe returned an unexpected result: ${probe:-no output}"
bashio::log.error "PowerSync: the SQLite database itself is untouched at $sqlite_db"
return 1
;;
esac
done
if [ "$state" = "populated" ]; then
@@ -371,6 +413,8 @@ import_sqlite_data() {
return 0
}
prepare_secret_key
if ! prepare; then
# The marker armed above is deliberately left in place; see the comment there.
exit 0