News
What shipped.
Mostly auto-published from CHANGELOG.md in the Vigil repository. Each entry corresponds to a version bump and explains what changed and why.
- Site launch
This site is live. Marketing/docs for Vigil under studioholst.com/vigil. The /news feed mirrors CHANGELOG.md going forward.
- v1.17.1 — Auto-refresh fight_model on report ingest
v1.17.0 unified cloned encounter IDs but the fight_model only rebuilt when an operator explicitly POSTed `/api/encounters/{id}/fight-model/persist` (then `classify` + `annotate-ca…
- v1.17.0 — Canonical encounter unification (cloned-encounter merge)
FFLogs occasionally cuts the same logical encounter into a new `encounter_id` after a re-cut. DSR today is split across **1065 (legacy)** and **1076 (current)**. Before this ship,…
- v1.16.5 — Per-phase tabs use wipe counts; phase inference for "Unknown"; guessed-pill tooltip distinguishes mechanic vs phase
User feedback on v1.16.4: "The table all (X) should be for wipes not death" + "how are there unknowns? Use the timeline of the report and cactbot's timeline, taking into account d…
- v1.16.4 — Per-phase tabs on "What's killing us"
Frontend-only ([web/src/Home.jsx](web/src/Home.jsx) `WipeMechanicsSection`). Added a row of phase chips above the mechanic table: `All (N)`, `P0 (n)`, `P1 (n)`, …, plus `Unknown (…
- v1.16.3 — Cartography uses inference for non-attributable deaths
v1.16.1 wired non-attributable-death inference (cast proximity + cactbot drift) into `compute_fault_scores_for_fight`, but the cartography "What's killing us" view read death even…
- v1.9.0 — per-encounter mit-audit aggregate ("how mit usage is going")
Ship 2 of 3 in the consumer-side push. T-303 mit audit already computes per-fight planned-vs-actual; the consumer Home needed an encounter-level rollup so the user can see at a gl…
- v1.8.0 — consumer Home as per-encounter prog dashboard
Per the v1.7.1 dev/user split, non-dev users land on Home with a generic 5-stat grid (or onboarding for first-timers) — none of the wipe / fault / mit / DPS signal surfaces unless…
- v1.16.2 — Cross-report player_id collision bugfix
User-caught attribution bug. Their per-job breakdown showed PLD 336 / DRK 203 / WAR 13 for "Aoi Bomber", but they remembered being Paladin almost the whole prog. Empirical check a…
- v1.16.1 — Near-wall plateau + non-attributable inference + scoped contributors
[analysis/fault_attribution.py](analysis/fault_attribution.py). New `NEAR_WALL_TOLERANCE = 0.5` constant: when a wipe's prog distance is within half a unit of the running-best wal…
- v1.16.0 — Fault scoring refinement sweep + heal_failure attribution
v1.14.6 left the scoring stack at four multipliers (phase × within × prog × repeat) — defensible per factor but compounding wildly (a single freak wipe could land 23× a baseline).…
- v1.15.1 — Roster polish + sub-account merge in fault contributors
[analysis/roster_discovery.py](analysis/roster_discovery.py). Before: a member with ≥2 aliases had ALL of them classified as `sub` (so P1 marked core + P2 attached as sub of P1 ma…
- v1.15.0 — Roster discovery + classification (core / substitute / sub / ignore)
The existing Members tab was form-driven: type a name, then type each character + server by hand. For a static with sub-accounts + occasional substitutes + pugs that appear once i…
- v1.14.6 — repeat-offender amplifier for past-wall root deaths
After v1.14.5's prog-relevance de-weighting, a player who repeatedly causes wipes in early phases (already-cleared content) was getting a SCORE BREAK they shouldn't get. User aske…
- v1.14.5 — phase-weighted fault scoring (prog-aware)
The score was an absolute sum: a P3 root weighed the same as a P7 root. The user asked for a "mild nonlinear" penalty that weights late-phase wipes heavier, AND a relative weighti…
- v1.14.4 — per-wipe normalization default + Option B planning
- Per-wipe normalization toggle in the fault table now **defaults to ON**. Surfaces per-attempt fault density instead of absolute volume, which is the more useful comparison acros…
- v1.14.3 — group fault rows by character name, per-job expansion, polish sweep
The same person across multiple watched reports got a different FFLogs `player_id` each time (per-report assignment), so the fault aggregate showed them as **multiple rows**. Also…
- v1.14.2 — prog curve uses per-encounter trajectory + DSR test data loaded
- **`ProgSection` only showed manual prog points.** It embedded the generic `ProgPoints` component (queries `/api/prog-points` for manual entries) instead of the per-encounter `/a…
- v1.14.1 — dev "view as user" toggle
- Devs can now flip a `view as user` toggle in the header to render the consumer Home / hide dev-only surfaces (Abilities tab, FieldStats panel, "show all encounters") — without D…
- v1.14.0 — body-check fault by assigned role (strat-aware aoe_party)
aoe_party deaths were always classified as root — but for body-check / soak / spread mechanics, that misattributes fault. A DPS dying to a 4-tank-tower mechanic shouldn't be blame…
- v1.13.0 — fault drill-down: per-mechanic offenders + per-member top failures
The per-player aggregate ("Alice has 4 roots") and the per-mechanic histogram ("Cyclonic Break killed 6 people") were both top-level signals — but neither answered the actual diag…
- v1.12.0 — fault classifier overhaul (mit-aware primary, strict causality, continuous decay)
The death classifier had three structural weaknesses called out in the fault-improvement review: a binary 5s cliff for cascade detection (#9), over-eager cascade attribution that…
- v1.11.0 — fault signals expanded: avoidable damage, Damage Down, confidence
T-302 fault attribution was death-only. Per PLAN §3 Invariant 5 the spec was always "**killing-blow ability + avoidable damage-taken** as primary signals; **Damage Down** as a sec…
- v1.10.0 — job-filterable DPS comparison (your static vs the field)
Ship 3 of 3 in the consumer-side push. T-204 `dps_check_for_encounter` already aggregates per-phase raid DPS across kills, but it pools everything (ours + field together) and ther…
- v1.7.1 — dev mode vs user mode (two-password split)
The dashboard is shared with the static; the dev DB has FRU/TOP/DSR/M9–M12 backfill from development that real users shouldn't see. Splitting the experience: developers retain eve…
- v1.7.0 — UI redesign (dark mode, design system, no new deps)
- New [web/src/styles.css](web/src/styles.css) — ~500 lines of design-token + reset + base + utility + component-class CSS. CSS custom properties for everything (surfaces, borders…
- 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…
- v1.5.9 — wiki damage-reduction-% scrape (M-MIT prep)
- **Schema**: new nullable `abilities.mit_pct` column (Integer, 0..100). Alembic migration `cead7d264bf9_abilities_mit_pct`. 17 tables in dev DB. - **`ingest/wiki.py`**: extended…
- v1.5.8 — T-309 partial: click-to-add mit palette + window overlay
- **Click-to-add mit palette** ([web/src/StratEditor.jsx](web/src/StratEditor.jsx) → `MitPalette`): collapsible panel below the mit-plan table grouping labeled abilities by `mit_p…
- v1.5.7 — wiki-scraped ability durations feeding M-BURST
- **Schema**: new nullable `abilities.duration_ms` column. Alembic migration `fbbd9a93c108_abilities_duration_ms`. 16 tables in dev DB. - **`ingest/wiki.py`**: small scraper for F…
- v1.5.6 — abilities review queue: bulk-mark + kind/label filters
- **API**: `PATCH /api/abilities/labels/bulk` — applies the same label to many abilities in one transaction. Body: `{ability_ids: [...], label: "<label>", notes?: "..."}`. Returns…
- v1.5.5 — Hungarian multi-cast assignment
- New helper module [analysis/_assignment.py](analysis/_assignment.py): `min_cost_assignment(cost, *, skip_penalty)` — pure-Python O(n³) Jonker-Volgenant solver for rectangular bi…
- v1.5.4 — phase-index alignment between T-103 and cactbot
- New helper `_align_phases(fight_phase_count, cactbot_phase_count)` in [analysis/timeline_diff.py](analysis/timeline_diff.py) maps each fight phase index to a cactbot phase index…
- v1.5.10 — T-309 complete: drag-to-reorder mit slots
- The v1.5.8 SVG bars in `MitWindowOverlay` ([web/src/StratEditor.jsx](web/src/StratEditor.jsx)) are now draggable. Grab a bar with the mouse, slide it horizontally — `window_offs…
- v1.5.3 — cactbot variant collapsing
- `timeline_diff_for_fight` now runs a post-match variant-detection pass: any non-firing slot whose expected time is within ±1500 ms of a *firing* slot in the same phase — and whi…
- v1.5.2 — refresh-cactbot script
- One-liner to re-fetch all 7 vendored cactbot timeline files from upstream `raw.githubusercontent.com`. Pinned to verified-as-of-2026-05-24 paths in the OverlayPlugin/cactbot rep…
- v1.5.1 — top-level README + named-tunnel operator runbook
- Top-level project README, ~330 lines, covering the full operator surface so a teammate (or future-self after a long break) can set up + run + operate the project without spelunk…
- v1.5.0 — cactbot Stage 2.2 (slot-driven matching: multi-ID variants handled correctly)
- **`timeline_diff_for_fight` refactored.** Previously iterated `fight_model` rows and looked up matching cactbot entries per row. Now iterates **cactbot timeline slots** and look…
- v1.4.1 — cactbot Stage 2.1 polish (multi-cast assignment + comment-block fallback names)
- `timeline_diff_for_fight` previously had each fight_model row pick the closest cast independently — when ability X had N rows and M casts, multiple rows could pick the same cast…
- v1.4.0 — cactbot Stage 2 (per-pull expected-vs-actual timeline diff)
- **`analysis/timeline_diff.py::timeline_diff_for_fight(session, fight_id)`** computes per-pull expected-vs-actual drift. For each cactbot-annotated canonical mechanic in this enc…
- v1.3.0 — cactbot annotations Stage 1 (human-readable mechanic names + expected timings)
- **`vendor/cactbot/`** — 7 timeline files vendored from https://github.com/OverlayPlugin/cactbot (Apache 2.0, NOTICE included): `r9s.txt`, `r10s.txt`, `r11s.txt`, `r12s.txt` (cov…
- v1.2.0 — FFLogs user-OAuth (archived + private reports via Gold tier)
- **`fflogs_user_auth` table** (single-row, `id=1`) stores the connected user's refresh token + cached access token + scope. Alembic migration `267acc60ac83`. - **`FFLogsClient` u…
- v1.1.1 — encounter-name label fix
- Encounter IDs 101–105 are **M9S–M12S** (AAC Heavyweight, 7.3 tier), not M5S–M8S. Past PROGRESS notes had this wrong and the UI label tables in [Encounters.jsx](web/src/Encounter…
- v1.1.0 — single-origin prod + HTTP Basic auth (Cloudflare quick-tunnel ready)
- **Single-origin prod mode.** When `WEB_STATIC_DIR` env var is set, FastAPI serves the React `web/dist/` build at `/` via `StaticFiles(html=True)`. Mounted after every `@app.get`…
- v1.0.1 — T-109 combatant filter
- `ingest/events.py::prune_inactive_combatants(session, fight_id)` — deletes combatants whose `player_id` never appears as a `source_id` in this fight's `cast`/`damage`/`calculate…
- v1.0.0 — **PHASE 3 COMPLETE · 1.0.0 SHIPS**
- `analysis/optimization.py::post_clear_targets_for_encounter()` synthesizes the three optimization levers — burst alignment (T-105), GCD drops/min (T-008), raid DPS vs target (T-…
- v0.34.0 — ### Added — T-307 M-REPORT Discord session summary
- `analysis/session_report.py::generate_session_report(session, report_code)` assembles a single Discord-pasteable Markdown summary per session (= per report). Stitches together:…
- v0.33.0 — ### Added — T-306 M-CONS consistency per mechanic
- `analysis/consistency.py::consistency_for_encounter()` walks every canonical mechanic in `fight_model`, finds each occurrence in our pulls (boss cast event), and checks whether…
- v0.32.0 — ### Added — T-305 M-RECOV recovery/resilience
- `analysis/recovery.py::recovery_for_fight()` walks death events, then checks each player's post-death activity (`cast` / `damage` events sourced by them) to detect resurrection.…
- v0.31.0 — ### Added — T-304 fault disambiguation via mit audit
- `analysis/fault_disambiguation.py::disambiguate_for_fight()` reads existing `fault_scores` (T-302) + the T-303 mit-audit, walks each cascade death whose killing ability is a rai…
- v0.30.0 — ### Added — T-303 M-MIT mitigation audit
- `analysis/mit_audit.py::mit_audit_for_fight()` — for each raidwide cast in a fight, joins `strat_config.mit_plan` (T-301) and checks whether each planned mit ability fired withi…
- v0.29.0 — ### Added — T-302 M-FAULT strat-aware fault attribution
- `analysis/fault_attribution.py::compute_fault_scores_for_fight()` walks deaths in one fight and classifies each as **root** / **cascade** / **enrage** / **unknown** based on the…
- v0.28.0 — **Phase 3 begins**
- **`analysis/strat_config.py`** — canonical JSON shapes + validators + CRUD helpers. Decisions captured 2026-05-24: - **mit_plan = structured slots**: `{slots: [{ability_id, expe…
- v0.27.0 — **PHASE 2 COMPLETE**
- **T-205** Prog-vs-field curve (PLAN §10 Compare). `analysis/prog_trajectory.py::prog_trajectory_for_encounter()` returns three things for one encounter: - **Our sessions** — der…
- v0.26.0 — ### Added
- **T-208** Fight Map + Compare UI — the visualization layer over everything Phase 2 built. - New top-level **Encounters tab** (`web/src/Encounters.jsx`): left-side encounter pick…
- v0.25.0 — ### Added
- **T-207** M-GATE gated-vs-mechanics diagnostic (PLAN §9, §10 Home headline). `analysis/gate_diagnostic.py::gate_diagnostic_for_fight()` synthesizes T-204 (per-phase DPS verdict)…
- v0.24.0 — ### Added
- **T-204** Empirical DPS check (PLAN §9 M-INFER #4). `analysis/dps_check.py::dps_check_for_encounter()` aggregates per-phase raid-DPS across every ingested kill of an encounter;…
- v0.23.0 — ### Added
- **Poll-now button** on each watchlist row — calls `POST /api/watched-reports/{code}/poll` which wraps `ingest_report` + `ingest_events_for_report` with proper commits and per-ca…
- v0.22.0 — ### Added
- **T-206** M-CART failure cartography. `analysis/cartography.py::cartography_for_encounter()` walks all ingested fights for an encounter and aggregates deaths by **killing boss a…
- v0.21.0 — ### Added
- **T-203** Mechanic classification. `analysis/mechanic_classifier.py::classify_canonical_abilities()` walks `fight_model` rows for an encounter, scans damage events each canonica…
- v0.20.0 — ### Added
- **T-202** Cross-group consensus → `fight_model` persistence. `analysis/consensus.py::write_consensus_to_fight_model(session, encounter_id, *, version=1)` runs T-104, then replac…
- v0.19.0 — ### Added — Phase 2 begins
- **T-201** Field backfill job — `jobs/backfill_field.py::backfill_once()`. For each tracked encounter, queries `worldData.encounter.fightRankings` for the top public reports, the…
- v0.18.0 — ### Added
- **T-104** Cross-pull consensus timeline (boss-side only, PLAN §3 Invariant 3). `analysis/consensus.py::consensus_timeline_for_encounter(session, encounter_id)` reads every inges…
- v0.17.0 — ### Added
- **T-101** Live polling of the static's open reports — **manual watchlist** strategy (decision recorded 2026-05-24 in IDEAS.md). User pastes a report code or FFLogs URL; the poll…
- v0.16.0 — ### Added
- **T-106** M-PARSE per-phase damage trajectory. `analysis/parse_trajectory.py::parse_per_phase_for_fight(session, fight_id)` consumes T-103 phase boundaries and aggregates per-pl…
- v0.15.0 — ### Added
- **T-103** Phase segmentation (boss-side). `analysis/phases.py::detect_phase_boundaries(session, fight_id)` derives per-fight phase intervals from enemy-actor damage-activity win…
- v0.14.0 — ### Added
- **T-105** M-BURST 2-minute burst alignment. `analysis/burst.py::burst_alignment_for_report` defines shared **burst windows** from raid-buff cast events (merging overlapping inte…
- v0.9.0 — ### Added
- **T-011** Static roster + character aliases. `Member` and `CharacterAlias` ORM models (`db/models.py`); members are decoupled from job (job derived per fight via CombatantInfo i…
- v0.8.0 — ### Added
- **T-008** M-GCD gcd-drop detection (`analysis/gcd.py`). Per-player GCD interval estimated from inter-cast median, spine-based GCD identification (filters oGCD weaves at 0.9 × GC…
- v0.7.0 — ### Added
- **T-007** Mode-1 fault basics (`analysis/faults.py::mode1_faults_for_report`): per-fight deaths with `killing_ability_game_id` + per-player `damage_taken_total` (non-player → pl…
- v0.6.0 — ### Added
- **T-006** M-WIPE wipe-location histogram (`analysis/wipes.py`). Bucketed by `(last_phase, last boss-cast ability_game_id)` from a configurable lookback window (default 15s) befo…
- v0.5.0 — ### Added
- **T-005** Event normalization (`ingest/events.py`): `ingest_events_for_report` walks all 7 PLAN §7 dataTypes (DamageDone, DamageTaken, Casts, Buffs, Debuffs, Deaths, CombatantIn…
- v0.4.0 — ### Added
- **T-004** Delta ingestion + ingestion ledger (`ingest/delta.py`): `ingest_report` writes `reports` / `fights` / `combatants` and the ledger row in one transaction. Cache-first p…
- v0.3.0 — ### Added
- **T-003** Postgres schema + migrations for all PLAN §6 tables (`reports`, `ingestion_ledger`, `fights`, `combatants`, `events`, `fight_model`, `strat_config`, `fault_scores`, `p…
- v0.2.0 — ### Added
- **T-002** FFLogs OAuth client-credentials module (`ingest/fflogs.py`): `FFLogsClient` with in-memory token cache, 60s refresh margin, GraphQL helper, single-retry on 401 with fo…
- v0.13.0 — ### Added
- **T-107** Combatant → member resolution. `analysis/resolve_members.py::resolve_combatants_for_report` joins `combatants` to `character_aliases` by `(name, server)`, falls back t…
- v0.12.0 — ### Added
- **T-108** Ability DB + classifier + review queue (Phase 1 — unblocks T-105 M-BURST and T-303 M-MIT). - Two new tables (alembic `eb34cac3f775`): `abilities` (XIVAPI metadata keye…
- v0.11.0 — ### Added
- **T-009** Mode-1 dashboard — **Phase-0 gate cleared**. React shell with tab nav (Home / Reports / Roster) wired into URL hash; ingested-reports picker, wipe-histogram bar chart,…
- v0.10.0 — ### Added
- **T-010** Manual prog-point entry. `GET/POST/DELETE /api/prog-points` against the existing `prog_points` table (T-003). Creates default to `source='manual'`; `auto` reserved for…
- v0.1.0 — ### Added
- Repo scaffold per PLAN §4: `api/`, `db/`, `ingest/`, `analysis/`, `model/`, `jobs/`, `tests/`, `web/`. - FastAPI skeleton (`api/main.py`) with `/healthz` route; CORS wired to th…