OOB-Dashboard/web/fonts
bahawal.baloch b7cc1bac71 Add hosted multi-user mode: accounts, MySQL, Docker, brand identity
Converts the single-user localhost tool into a hosted application while
leaving local mode intact.

The pipeline moves out of serve.py into app/services/{analysis,exporters}.py,
which import only the standard library and ppcbudget. serve.py becomes a thin
shim over them, so it still runs offline with no dependency beyond openpyxl and
there is one copy of the analysis rather than two. ppcbudget and run_report.py
are otherwise untouched.

app/ is a FastAPI application serving the same dashboard behind sign-in:

- Open signup with email confirmation, forgot/reset, argon2id hashing, and
  opaque DB-backed session cookies (HttpOnly, SameSite=Lax, Secure derived from
  APP_BASE_URL so TLS is later a config change, not a rewrite).
- Signup and forgot-password answer identically whether or not an address
  exists; login gives one generic message for unknown, wrong, locked and
  disabled alike. Email tokens are HMAC'd at rest and spent by a single atomic
  UPDATE, and /verify and /reset are inert pages that POST the token -- a mail
  scanner following the link cannot burn it.
- Routes are def, not async def: the pipeline is CPU-bound and would otherwise
  block the event loop. A semaphore bounds concurrent analyses.
- The process-wide SESSION global becomes a workspace per account, with
  per-workspace locks, size caps and idle eviction. The old code held one lock
  for the whole analysis; per-user locks fix that by construction.
- Accounts live in three oob_-prefixed tables. The connection URL is built with
  URL.create, since the password contains ? and # and a hand-built DSN
  truncates it there.
- Minimal /admin page for enabling, disabling and signing accounts out.

Hardening: server-side upload extension and size limits, an Origin guard on
writes, correlation-id 500s instead of echoed exceptions, and a CSP. Every page
script is external because script-src 'self' blocks inline blocks.

Restyled to the Utopia Brands guidelines. Brand swatches sit verbatim in the
--u-* properties and everything derives from them, including the Excel report.
--red and --amber are deliberately not from the guide: it covers identity, not
function, and has no warning colour, but this dashboard exists to show
campaigns going dark. The four data colours were checked for colour-vision
separation across every pair on both surfaces, and the day heatmap is now one
hue getting darker rather than a rainbow.

The claim that nothing is uploaded anywhere stays true only for local mode, so
the dashboard copy and the README now say so.

Tests: tests/test_auth_smoke.py covers the account lifecycle plus upload,
analyse and export against SQLite with mail captured, so it needs no MySQL and
no network. tests/synthetic.py builds a small stand-in export.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 13:42:00 +05:00
..
README.md Add hosted multi-user mode: accounts, MySQL, Docker, brand identity 2026-08-12 13:42:00 +05:00

README.md

Brand fonts

These files are not in the repository. Drop them here and the app picks them up with no code change — web/styles.css already declares the @font-face rules and the fallback stacks.

File Face Where it is used Source
Belleza-Regular.woff2 Belleza headings (h1, h2) Google Fonts, SIL Open Font License
NeueMontreal-Regular.woff2 Neue Montreal body copy, sub-headings Pangram Pangram, commercial licence
NeueMontreal-Medium.woff2 Neue Montreal Medium emphasis as above
NeueMontreal-Bold.woff2 Neue Montreal Bold strong emphasis as above

Belleza is already wired up. For Neue Montreal, uncomment the @font-face block at the top of web/styles.css once the files are here.

They have to be self-hosted

The app sends Content-Security-Policy: default-src 'self', so a stylesheet that pulls fonts from fonts.gstatic.com is blocked by the browser and the page silently falls back. Converting to .woff2 and serving them from this directory is the only route.

To convert from .ttf/.otf:

pip install fonttools brotli
python -c "from fontTools.ttLib import TTFont; f=TTFont('Belleza-Regular.ttf'); f.flavor='woff2'; f.save('Belleza-Regular.woff2')"

Until then

Headings fall back to Optima → Candara → Gill Sans → Trebuchet MS, and body copy to the system UI sans. Both are chosen to sit close to the real faces, so the layout does not shift when the files arrive.