Files
jessikitty f7c20de5ac Add owner-only logging and Brickset API call tracking
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
2026-09-27 10:42:49 +10:00

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 "$@"