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
This commit is contained in:
jessikitty committed 2026-09-27 10:42:49 +10:00
1 parent 89f65e2d74
commit f7c20de5ac
27 files changed
+1914 -35

No files matched your search

+8 -4
View File
@@ -18,10 +18,13 @@ with `flask instructions run` instead.
Created by: Jess Rogerson (yelling commands at Claude.AI)
"""
import logging
import os
import sys
import threading
logger = logging.getLogger(__name__)
_thread = None
_stop = threading.Event()
_lock = threading.Lock()
@@ -38,7 +41,7 @@ def _is_cli_command():
def start(app):
"""Start the ticker unless it's disabled or already running."""
if not app.config.get('INSTRUCTION_FETCH_WORKER', True):
app.logger.info('Instruction fetch worker disabled by config')
logger.info('Instruction fetch worker disabled by config')
return False
# `flask instructions run`, `flask backup restore` and friends shouldn't
@@ -61,7 +64,8 @@ def start(app):
daemon=True)
_thread.start()
app.logger.info('Instruction fetch worker started')
logger.info('Instruction fetch worker started (tick %ss)',
app.config.get('INSTRUCTION_FETCH_TICK_SECONDS', 20))
return True
@@ -86,7 +90,7 @@ def _loop(app):
with app.app_context():
fetcher.run_one()
except Exception:
app.logger.exception('Instruction fetch worker tick failed')
logger.exception('Instruction fetch worker tick failed')
def run_once_async(app, force=False):
@@ -102,7 +106,7 @@ def run_once_async(app, force=False):
from app.services import instruction_fetcher as fetcher
fetcher.run_one(force=force)
except Exception:
app.logger.exception('Manual instruction fetch failed')
logger.exception('Manual instruction fetch failed')
thread = threading.Thread(target=_work, name='lim-fetch-once', daemon=True)
thread.start()