Vigil
← All news

v1.6.0 — multi-static (users + statics + N:M memberships)

User picked **N:M users-to-statics** + **scope-only-user-curated-data** (2026-05-25). The rest of the design is documented inline in [api/auth.py](api/auth.py) + the migration doc…

MINOR per CLAUDE.md §4 — first real scope expansion past PLAN §1's single-static premise.

User picked N:M users-to-statics + scope-only-user-curated-data (2026-05-25). The rest of the design is documented inline in api/auth.py + the migration docstring at alembic/versions/11cd54903d42_multi_static.py.

Schema (migration 11cd54903d42_multi_static)

  • New tables: users (id, username unique, current_static_id, created_at), statics (id, name, created_at), static_memberships (user_id, static_id PK, joined_at).
  • static_id added to watched_reports, members, strat_config, prog_points, fault_scores. All existing rows migrated to the seeded Default Static (id=1) so a pre-1.6.0 install upgrades without data loss.
  • PK changes:
    • watched_reports (code) → (static_id, code). Two statics can watch the same report; the ingestion ledger short-circuits redundant raw-data work.
    • strat_config (encounter_id, mechanic_ref) → (static_id, encounter_id, mechanic_ref).
    • fault_scores (fight_id, player_id) → (static_id, fight_id, player_id).
    • members unique(name) → unique(static_id, name).
    • character_aliases: global unique(character_name, server) dropped — scoping is via member → static.

Auth + identity (api/auth.py)

  • Existing HTTP Basic middleware loosened: AUTH_PASSWORD stays a single shared password, but the USERNAME is now free-form. The middleware no longer enforces provided_user == AUTH_USERNAME — anyone with the password can log in as any username, and that becomes their user record on first request.
  • New Context dataclass + get_context(request) FastAPI dependency. The dependency auto-provisions the user record on first sighting and auto-joins them to the Default Static so a fresh deploy with AUTH_USERNAME=aoi keeps working out of the box.
  • Authorization via require_static_membership(session, user_id, static_id). Cross-static access returns 404 (not 403) to avoid leaking existence of statics the user can't see.
  • Dev/test mode (no AUTH_* env): username extracted from the Authorization header if present; otherwise falls back to a dev user. Tests use this hook to simulate N users without enabling the password layer.

API

  • New endpoints:
    • GET /api/me — user + current_static_id + statics list (drives the switcher).
    • GET /api/statics, POST /api/statics (creator auto-joins + auto-switches if they were on Default Static).
    • PATCH /api/me/current-static (404 if not a member).
    • GET /api/statics/{id}/members, POST /api/statics/{id}/members (404 if username doesn't exist — they must log in once first), DELETE /api/statics/{id}/members/{user_id} (409 on last-member removal).
  • Existing endpoints scoped — all ~21 endpoints that touch watched_reports / members / character_aliases / strat_config / prog_points / fault_scores now take ctx: Context = Depends(get_context) and filter by ctx.current_static_id. Analytical endpoints (prog-curve, consistency, post-clear-targets, mit-audit, fault-scores/{compute,disambiguate}, session-report, fault-aggregate) pass the static_id through to their analysis modules.
  • Shared raw-data endpoints untouched (reports, fights, events, fight_model, abilities, ability_labels, dps_check, gate-diagnostic, cartography, recovery, wipe-type — all static-agnostic since the underlying tables aren't scoped).

Analysis modules updated to take static_id

React

  • New web/src/StaticSwitcher.jsx in the App header: shows {username} · <static dropdown> + a members button (opens modal: list + add by username + remove) + a + static button (inline create-and-switch).
  • App tabs remount on static switch via a staticKey prop so they refetch scoped data without page reload.

Tests

  • 10 new in tests/test_multi_static.py: /api/me shape, cross-static isolation for watched_reports / members / strat_config, 404 on access to other-static rows, 404 on switching to non-member static, add-member then they see the data, unknown-username add returns 404, last-member-removal blocked, member listing. Each test uses unique disposable usernames + tears down its own users/statics.
  • Updated existing tests (test_consistency, test_fault_attribution, test_fault_disambiguation, test_mit_audit, test_optimization, test_prog_trajectory, test_session_report, test_resolve_members, test_schema, test_roster_api, test_poll_watched, test_auth_middleware) to seed static_id=1 and pass static_id through to updated function signatures. test_api_route_rejects_wrong_username retired; replaced with test_api_route_accepts_any_username_with_right_password documenting the new multi-user-shared-password model.
  • 396 tests passing (379 → 396, +17).

Migration ergonomics

  • Pre-1.6.0 installs upgrade with no manual steps. Existing single-user AUTH_USERNAME=aoi deploys: first request creates user "aoi", joins Default Static (which now holds all migrated data), sets current_static_id = 1. Existing behavior preserved.
  • Single shared password is conscious tech debt — adequate for the friend-group case. For a real public deploy this should move to Cloudflare Access (which the named-tunnel runbook in README already covers) and the HTTP Basic middleware unsets.