LIM configured no logging at all, so the instruction fetcher had no trail to follow. Adds a rotating file handler on the root logger plus two owner-only admin pages. - Admin > Logs: filter by level, source and text; tracebacks kept with their error; auto-refresh, raw download, rotated-file browsing and rotate-now - Admin > Logs > Brickset API calls: every outbound call recorded with method, params, caller, duration, result count and error, plus daily quota accounting (only getSets counts) and a comparison against getKeyUsageStats - New owner concept (services/ownership.py): a single account, distinct from is_admin, since these pages expose other admins activity. Defaults to the first admin; flask owner show/set to change - Services now log through module loggers; retention setting and pruning for call records - New CLI: flask owner show/set, flask logs path/prune - New table brickset_api_calls, created automatically at boot; LOG_* env vars, /data/logs in Docker
83 lines
2.8 KiB
Bash
83 lines
2.8 KiB
Bash
#!/usr/bin/env bash
|
|
# LEGO Instructions Manager - container entrypoint.
|
|
#
|
|
# Runs as root just long enough to fix volume ownership, then waits for the
|
|
# database, creates any missing tables, and drops to the unprivileged user.
|
|
#
|
|
# Created by: Jess Rogerson (yelling commands at Claude.AI)
|
|
set -euo pipefail
|
|
|
|
APP_USER=lim
|
|
|
|
log() { echo "[entrypoint] $*"; }
|
|
|
|
# --- volume ownership -------------------------------------------------------
|
|
# A freshly created named volume mounts as root:root, so the app user cannot
|
|
# write to it until we hand it over. Cheap to repeat on every start.
|
|
if [ "$(id -u)" = '0' ]; then
|
|
mkdir -p "${UPLOAD_FOLDER:-/data/uploads}" "${BACKUP_FOLDER:-/data/backups}" \
|
|
"${LOG_FOLDER:-/data/logs}"
|
|
chown -R "${APP_USER}:${APP_USER}" /data
|
|
fi
|
|
|
|
# --- wait for the database --------------------------------------------------
|
|
if [ "${DATABASE_URL:-}" != "" ] && [ "${SKIP_DB_WAIT:-0}" != "1" ]; then
|
|
log "Waiting for database..."
|
|
for attempt in $(seq 1 60); do
|
|
if python - <<'PY'
|
|
import os, sys
|
|
from sqlalchemy import create_engine, text
|
|
try:
|
|
engine = create_engine(os.environ['DATABASE_URL'], pool_pre_ping=True)
|
|
with engine.connect() as conn:
|
|
conn.execute(text('SELECT 1'))
|
|
except Exception as exc:
|
|
print(f' not ready: {exc.__class__.__name__}', file=sys.stderr)
|
|
sys.exit(1)
|
|
PY
|
|
then
|
|
log "Database is up."
|
|
break
|
|
fi
|
|
if [ "$attempt" = "60" ]; then
|
|
log "ERROR: database did not become available in time."
|
|
exit 1
|
|
fi
|
|
sleep 2
|
|
done
|
|
fi
|
|
|
|
# --- schema -----------------------------------------------------------------
|
|
# db.create_all() is additive: it creates missing tables and leaves existing
|
|
# ones untouched. It does NOT alter columns, so a schema change still needs a
|
|
# migration — see docs/DOCKER_DEPLOYMENT.md.
|
|
if [ "${SKIP_DB_INIT:-0}" != "1" ]; then
|
|
log "Ensuring schema exists..."
|
|
# No fetcher thread in this short-lived bootstrap process.
|
|
INSTRUCTION_FETCH_WORKER=0 python - <<'PY'
|
|
import os
|
|
from app import create_app, db
|
|
app = create_app(os.getenv('FLASK_ENV', 'production'))
|
|
with app.app_context():
|
|
db.create_all()
|
|
print('[entrypoint] Schema ready.')
|
|
PY
|
|
fi
|
|
|
|
# --- hand over --------------------------------------------------------------
|
|
# Re-chown: create_app() ran as root above and will have created the
|
|
# uploads subdirectories (pdfs/, images/, covers/, thumbnails/, extra_files/)
|
|
# owned by root. Without this the app user cannot write uploads.
|
|
if [ "$(id -u)" = '0' ]; then
|
|
chown -R "${APP_USER}:${APP_USER}" /data
|
|
log "Starting as ${APP_USER}: $*"
|
|
if command -v runuser >/dev/null 2>&1; then
|
|
exec runuser -u "${APP_USER}" -- "$@"
|
|
else
|
|
exec su -s /bin/bash -c 'exec "$0" "$@"' "${APP_USER}" -- "$@"
|
|
fi
|
|
fi
|
|
|
|
log "Starting: $*"
|
|
exec "$@"
|