architecture.md 3.8 KB

Architecture

Overview

                    ┌─────────────────────────── single process ───────────────────────────┐
   browser ──HTTP──▶│  Flask (app.py, waitress)                                              │
                    │    │                                                                    │
                    │    ├─ auth.py ......... Okta OIDC / static; login_required/admin_required
                    │    ├─ ConfigStore ..... immutable snapshots of config.toml (tomlkit)   │
                    │    ├─ PrefStore ....... per-user prefs (jobs.db)                        │
                    │    └─ /api/actions ──▶ JobQueue.enqueue() ──▶ SQLite jobs table         │
                    │                              ▲                      │                    │
                    │                    worker thread (daemon) ◀─────────┘                    │
                    │                              │                                           │
                    │                              └─ pipeline.py ──▶ PPSClient ──HTTPS──▶ PPS │
                    └────────────────────────────────────────────────────────────────────────┘

Everything runs in one process (see AGENTS.md invariant #1). The browser fires an action and gets an immediate 202; the daemon worker thread drains the SQLite queue and calls PPS. Job progress is polled via /api/jobs; it is cosmetic — closing the page does not affect processing, and JobQueue._recover() requeues interrupted jobs after a restart.

Request lifecycle

  1. wsgi.py builds ConfigStore, JobQueue, PrefStore, calls create_app, starts the worker, serves.
  2. Each request: auth.login_required/admin_required gate, then the view takes one store.snapshot() and reads all config from it.
  3. State-changing /api/* requests must carry X-CSRF-Token (checked in before_request).

Configuration hot-reload

config.toml is read once into an immutable Snapshot. The admin panel's PUT /api/admin/config validates a patch, atomically rewrites the file with tomlkit (comments preserved), rebuilds the PPSClient only if a connection key changed, and swaps in a new snapshot. Readers are lock-free (attribute rebind is atomic under the GIL). See docs/configuration.md for which keys apply live vs. need a restart.

Background jobs

  • Tables: jobs(id, action, folder, user, extra, status, processed, total, error, …) and job_items(job_id, localguid, guid, folder, subject, sender, recipient).
  • The worker claims the lowest pending job, snapshots config once, chunks the items (chunk_size), calls pipeline.run_pipeline (Report & Release) or a single PPS action, writes one audit line per message to worker.log, and marks the job done/failed.
  • retry(job_id) requeues a failed job.

Data stores

Store Where Contents
config.toml file (tomlkit) all configuration; 0600; .bak on each write
jobs.db SQLite (WAL) jobs, job_items, user_prefs
worker.log rotating file per-message audit trail
app.log rotating file application log (errors visible in admin panel)

Why single-process

The worker thread and both SQLite connections share process memory, and the config lock is an in-process RLock. Multiple processes would run competing queue workers and uncoordinated config writes. Scale vertically (threads), never horizontally. This is a hard constraint.