Web application¶
The Flask application lives in src/web/. It is created by create_app() in app.py using the application factory pattern.
Application factory — app.py¶
create_app(config=None) registers all blueprints, attaches a context processor, and starts the scheduler.
Context processor¶
inject_setup_status() runs on every request and injects two variables into every template:
| Variable | Type | Description |
|---|---|---|
setup_status |
dict |
Full output of get_setup_status(app) |
setup_complete |
bool |
Convenience alias for setup_status["complete"] |
base.html uses setup_complete to choose between the wizard navbar and the operations navbar without any per-route logic.
Authentication — auth.py¶
login_required is a decorator that:
- In development (
APP_ENV=development) — passes through unconditionally. - In production — redirects to
/loginifsession["logged_in"]is falsy.
Login is a simple username/password check against ADMIN_USER and ADMIN_PASSWORD from the Flask config (set via environment variables).
Blueprints¶
| Blueprint | Prefix | Gated by |
|---|---|---|
auth |
— | — |
setup |
/setup |
login_required only |
main |
— | login_required |
config_editor |
/config |
login_required |
connections |
/connections |
login_required |
ssh_keys |
/ssh-keys |
login_required |
schedule |
/schedule |
login_required + setup_complete_required (index, run-now, status only) |
dashboard |
/dashboard |
login_required + setup_complete_required |
progress |
/progress |
login_required + setup_complete_required |
schedule.save and schedule.remove are intentionally not gated by setup_complete_required — the wizard's Schedule step calls them before setup is complete.
Live progress — run_state.py and progress.py¶
RunState is a module-level singleton (state = RunState()) shared between:
scheduler.py(writer) — callsstate.start(),state.add_line(),state.step_running(),state.step_done(),state.finish().progress.py(reader) — callsstate.snapshot()andstate.lines_from(seq).
The SSE endpoint at GET /progress/stream generates an infinite stream that:
- Sends an initial
stateevent with the current snapshot. - Polls every 300 ms for new lines via
lines_from(seq). - Pushes
lineevents for each new log line, and astateevent after each batch. - Sends a final
state+doneevent when the run finishes, then closes. - Reconnects automatically after a 3-second back-off on network error.
The browser client uses EventSource and handles line, state, and done event types.
Scheduler — scheduler.py¶
get_scheduler() returns a lazily-initialised BackgroundScheduler (APScheduler) singleton configured for UTC. The scheduler is started once at application boot via init_scheduler(app).
_run_backup(app) is the job function. It:
- Calls
state.start(). - Attaches
_StateHandlerto thebackup_serverlogger to bridge log records intoRunState. - Calls
backup_server.main.main(). - Removes the handler and calls
state.finish().
_StateHandler matches known log phrases to infer step transitions:
| Phrase in log (case-insensitive) | Action |
|---|---|
start backup |
state.step_running("backup") |
step 'backup' succeeded |
state.step_done("backup", success=True) |
step 'backup' failed |
state.step_done("backup", success=False) |
(same for retention and sync) |
Dashboard data — dashboard_data.py¶
Read-only queries against log.db returning plain dicts and lists. The routes stay thin — they call these functions and pass the results directly to render_template.
Key functions:
| Function | Returns |
|---|---|
get_summary(db_path) |
Total/success/failed run counts, last run timestamp, task name list |
get_task_stats(db_path) |
Per-task run counts, success rate, last run timestamp |
get_task_history(db_path, task_name, limit) |
Paginated list of runs with step details |