Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
43bb1c4
docs(structures): design for the structure damage monitor
guarzo Aug 24, 2026
dd19c19
docs(structures): apply codex review findings to the design
guarzo Aug 24, 2026
e61410d
docs(structures): implementation plan
guarzo Aug 24, 2026
75737b1
docs(structures): fix four pre-flight conflicts in the plan
guarzo Aug 24, 2026
32ec7ea
feat(structures): schema for the structure damage monitor
guarzo Aug 24, 2026
fd877b6
feat(structures): pure notification parsing and alert formatting
guarzo Aug 24, 2026
b8a87dc
refactor(esi): extract the paged-collection walk from getAllContacts
guarzo Aug 24, 2026
9455e8c
feat(esi): corporation structures and character notifications reads
guarzo Aug 24, 2026
5abcc21
feat(structures): dedicated alert webhook with explicit resolution
guarzo Aug 24, 2026
40ad404
feat(structures): holder designation, read state and roster reads
guarzo Aug 24, 2026
505758f
test(structures): cover findGrantableCharacter and toHolderView
guarzo Aug 24, 2026
c12143b
feat(structures): opt-in scope grant and audit vocabulary
guarzo Aug 24, 2026
facb07e
fix(auth): guard grant lookup against prototype-chain keys
guarzo Aug 24, 2026
1f7544b
docs(structures): fix the grant-lookup sketch that caused a critical
guarzo Aug 24, 2026
ef7ece0
feat(structures): hourly roster job
guarzo Aug 24, 2026
e620cea
feat(structures): ten-minute damage alert job
guarzo Aug 24, 2026
64e291e
feat(structures): monitor state cascade
guarzo Aug 24, 2026
449b478
docs(structures): fix an unreachable test case in the plan
guarzo Aug 24, 2026
59ab0ca
test(structures): cover all cascade branches and utility functions
guarzo Aug 24, 2026
2468e83
feat(structures): admin monitor page
guarzo Aug 24, 2026
b9fe152
fix(tests): satisfy the lint gate without weakening assertions
guarzo Aug 24, 2026
ee35355
test(structures): update the two expectation sets this feature grows
guarzo Aug 24, 2026
814650e
test(structures): e2e coverage and operational docs
guarzo Aug 24, 2026
d6fd7a2
fix(structures): guard the send phase and the alias lookup
guarzo Aug 24, 2026
9510a1c
fix(structures): address PR review findings
guarzo Aug 24, 2026
2cf4b4a
fix(structures): detect same-character re-designation in the CAS
guarzo Aug 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ DISCORD_ROLE_ID_ASSOCIATE=11
DISCORD_ROLE_ID_ALUMNI=12
# Optional. Suppressed anyway while SYNC_MODE=dry-run.
DISCORD_OPS_WEBHOOK_URL=
# Optional. Structure damage alerts. Falls back to DISCORD_OPS_WEBHOOK_URL when
# unset; when NEITHER is set, nothing is alerted and /admin/structures says so.
DISCORD_STRUCTURE_WEBHOOK_URL=

# --- Wanderer (fake) ---------------------------------------------------------
WANDERER_BASE_URL=https://wanderer.example
Expand Down
46 changes: 45 additions & 1 deletion docs/ops.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,17 @@ renders — this table is a copy for readers, not a source.
| `location` | `2,17,32,47 * * * *` | housekeeping |
| `membership-recheck` | `0 4 * * 0` | on-demand |
| `access-lists` | `25 * * * *` | on-demand |
| `structures` | `35 * * * *` | on-demand |
| `structure-events` | `3,13,23,33,43,53 * * * *` | on-demand |
| `token-health` | `0 3 * * *` | housekeeping |
| `purge` | `30 3 * * *` | housekeeping |

`location` is offset off the :00/:05/:10/:15 minutes on purpose: there is no
access-token cache, so it quadruples per-character SSO refreshes and would
otherwise race the contacts job for the same rows.
otherwise race the contacts job for the same rows. `structures` (:35) and
`structure-events` (:3/:13/:23/.../:53) land on minutes none of the above
already claim, for the same reason: two jobs sharing a minute race for the
same holder's token.

The 90-minute freshness threshold used by `/api/health/sync` is a constant in
`src/core/health.ts`, compared with `<=`. If the most frequent job here ever
Expand Down Expand Up @@ -243,6 +248,11 @@ forever, so it is unbounded too — roughly an order of magnitude slower than
`audit_log`, and worth its own retention policy eventually. Noted here, not
fixed; it does not change this decision.

`structure_event` is unbounded for the same reason `audit_log` is: it is an
append-only record of fact — every structure notification this app has ever
seen, one row per `notification_id` — and `purge.ts` deliberately leaves it
alone. Same shelf life, same trigger to revisit it.

### Revisit when — not before

`audit_log` is the fastest-growing table with no retention policy — the `purge`
Expand Down Expand Up @@ -362,6 +372,7 @@ character already on the ACL.
| `DISCORD_GUILD_ID` | yes | the guild whose roles are managed |
| `DISCORD_ROLE_ID_MEMBER` / `_ASSOCIATE` / `_ALUMNI` | yes | the three managed role ids (distinct) |
| `DISCORD_OPS_WEBHOOK_URL` | no | ops alerts (final retry failures, config errors) |
| `DISCORD_STRUCTURE_WEBHOOK_URL` | no (falls back to `DISCORD_OPS_WEBHOOK_URL`) | structure notifications (see below). With neither set, nothing is alerted — events are still recorded, as `seeded` — and `/admin/structures` says so |
| `WANDERER_BASE_URL` / `WANDERER_API_KEY` | yes | Wanderer instance + the **ACL's own** API key (the map API key returns 401 on `/api/acls/*`) |
| `WANDERER_ACL_ID` | yes | the managed ACL — dedicated to authGD, reconciled destructively |
| `STANDINGS_LABEL` | no (default `authgd`) | in-game contact label the app OWNS — see the warning below |
Expand Down Expand Up @@ -429,6 +440,39 @@ URL is built, so at that moment there is no "the character" whose existing
scopes could be carried forward. `/admin/access-lists` detects the loss and asks
for a re-grant rather than failing silently.

### The structure scopes are opt-in

`esi-corporations.read_structures.v1` (the roster read) and
`esi-characters.read_notifications.v1` (structure notifications) are both
deliberately **absent** from `EVE_SSO_SCOPES`, for the same reason the
access-list scope is: putting either there would flip every existing character
to `needs_reauth` on the next token-health run, for a feature only one
character needs.

An admin grants both by visiting `/auth/eve/link?grant=structures`, the same
mechanism as `grant=access-lists`, and the grant is equally **not sticky** — any
ordinary re-authentication drops it, and `/admin/structures` detects the loss
and asks for a re-grant rather than re-authenticating silently into a monitor
that has quietly stopped reading anything.

Granting the scope is necessary but not sufficient. Two **in-game corporation
roles** gate what the holder character can actually see, and this app has no
way to grant either of them — they are assigned in-game, by someone who already
holds them, to the character this app designates as the structure holder:

- **Station_Manager** (or higher) is what ESI's structure-list endpoint itself
requires. Without it the roster read comes back forbidden and
`/admin/structures` reports it as such (`no-corp-roles`, in
`src/app/admin/structures/view.ts`), even though the scope grant succeeded.
- **Director or CEO** is what EVE requires before it will deliver structure
notifications to a character **at all** — this is CCP's own delivery rule,
not something this app enforces or can bypass. A holder below that rank sees
an empty notification stream forever, with no error to point at: the read
succeeds, it is simply never sent anything to read.

Designate a holder who already holds both roles, or have someone who does grant
them to the designated character before relying on this page.

## SYNC_MODE — the dry-run safety guard

`SYNC_MODE` is **required and has no default**. Every other arrangement has a
Expand Down
Loading