# API reference

```
GET|POST /Admin/api/?action=<module>.<method>
```

- `GET` = reads, `POST` = mutations (JSON body + `X-CSRF-Token` header).
- Always `{"ok":true,"data":…}` or `{"ok":false,"error":…}`.
- Only methods listed in a CFC's `this.endpoints` are reachable.
- Public (no auth): `auth.login`, `auth.bootstrap`, `auth.status`.

The **Guard** column is the ACL section (see `ACCESS-CONTROL.md` for levels), or
a raw clearance level where the code uses `requireLevel` directly.

---

## `auth` → `Auth.cfc`

| Method | Guard | Purpose |
|---|---|---|
| `status` | public | Session probe; reports `bootstrapNeeded` when no accounts exist |
| `bootstrap` | public, POST | One-time creation of the first level-9 account |
| `login` | public, POST | Sign in; issues the CSRF token |
| `logout` | POST | Sign out |
| `me` | auth | Current admin + the resolved `sectionLevels` map |

## `dashboard` → `Dashboard.cfc`

| Method | Guard | Purpose |
|---|---|---|
| `summary` | `dashboard` | Overview: online series, per-server counts, queue sizes, MOTD, recent audit, recent events. Widgets inside are individually gated by the `dashboard.*` sections. Logs `dashboard.view`. |

## `users` → `Users.cfc`

Player search, detail, and every per-tab read/write.

| Method | Guard | Purpose |
|---|---|---|
| `search` | `players.view` | Player search; visible columns/filters follow the viewer's field ACLs |
| `typeahead` | `players.view` | Global search box suggestions |
| `detail` | `players.view` | Player page; returns a `perms` struct driving tab/field visibility |
| `update` | `players.view` + per-field | Profile save; each field re-checked (`players.realname.edit`, `.empirename.edit`, `.email.edit`, `.race.edit`, `.helplevel.edit`, `.password.edit`) |
| `fleet` / `fleetSave` | `players.fleet.view` / `.edit` | Fleet tab |
| `colony` / `colonies` / `colonySave` / `colonyAdd` | `players.colony.view` / `.edit` | Colonies. Both reads also return `minerals` (the `good` rows of type 1) so the Mineral picker needs no game-data clearance; `colonySave` / `colonyAdd` accept `goodid` only from that set |
| `artifacts` / `artifactsSave` | `players.artifacts.view` / `.edit` | Artifacts |
| `projects` / `projectSave` | `players.projects.view` / `.edit` | Projects |
| `resources` / `resourcesSave` | `players.resources.view` / `.edit` | Resources + minerals. `resourcesSave` also takes `silent` (1/0, default 0) — see below |
| `infraSave` | `players.infra.edit` | Infrastructure levels |
| `market` / `marketSave` | `players.market.view` / `.edit` | Market posts |
| `researchSave` | `players.research.edit` | Research / researched / ships |
| `events` | `players.events.basic.view`, upgraded by `players.events.view` | Player events; basic tier hides battles and artifact events |
| `record` | `players.records.view` | Legacy `user_pm_abuse` record (chat `ref:` rows excluded) |
| `changeLog` | `players.records.view` | Verbose before → after edit history |
| `notes` / `addNote` | `players.notes` | Case notes |
| `deleteNote` | level 3 | Delete a case note |
| `logins` | `players.ip.view` | Login history (shares the IP-view gate) |

### Every editor — `showreason`

Any endpoint that calls `logChange()` accepts **`showreason`** (1/0, default 0)
in its body. Ticked, the admin's reason is appended to the PM the player
receives about the change; left alone it stays an internal note visible only in
the audit log. The flag is read centrally inside `logChange()`, so it works for
every player-editing method here without the method itself doing anything.

"Do not notify" outranks it — a silent edit sends no PM, so there is nothing to
put the reason in. Full behaviour table in
[`AUDIT-LOGGING.md`](AUDIT-LOGGING.md#sharing-the-reason-with-the-player).

### `resourcesSave` — `silent`

`silent=1` suppresses the PM to the player **and** the forced re-log, so a
correction or an admin check can be made without the player being logged out
onto "An administrator has updated your empire". Absent or `0` means notify,
which is the default — the checkbox in the editor starts unticked.

It carries no section of its own: an admin cleared for `players.resources.edit`
is already trusted to decide whether the player hears about it, and it is one
control inside that one editor. The response echoes `notified` so the client can
confirm which path ran.

The audit entry and the `user_pm_abuse` record are written either way, the audit
detail gaining `silent: true`. Skipping the re-log is safe for *resources
specifically* because `i_f_800.cfm` re-reads credit/food/power from the database
on every game page load; do not assume that of other editors.

## `useractions` → `UserActions.cfc`

All POST. Each writes the legacy game-side record **and** the panel audit entry,
and most force the player to re-log.

| Method | Guard |
|---|---|
| `comment` | `players.action.comment` |
| `freeze` | `players.action.freeze` |
| `refreshTech` | `players.action.refreshtech` |
| `suspend` / `unsuspend` | `players.action.suspend` |
| `blacklist` | `players.action.blacklist` |
| `disableTurns` | `players.action.disableturns` |
| `restart` | `players.action.restart` |
| `silence` | `players.action.silence` |

### `refreshTech` — paid projects

Rebuilding the research pool from `restree` would delete every paid project the
player holds. They are bought, not earned, so each one carries `r1`-`r6` = 0 and
`req1` = 253 *Special* — a row that lists itself as its own prerequisite and can
therefore never be satisfied. A bought-but-not-yet-researched project failed both
tests and vanished on every refresh.

`UserActions.PAID_RESEARCH` lists their ids (256-260 today) and is **maintained
by hand** — nothing in the schema marks a row as purchasable. A project in that
list is kept if, and only if, the player already had it in `r_research` or
`r_researched`, so a refresh can neither take one away nor hand one to someone
who never bought it. Same test the in-game race restart uses
(`f_option_race.cfm`). The ids kept are recorded in the audit detail as
`paidKept`.

255 *UnReverse Engineer* is deliberately **not** in the list: it is also bought
but carries `r5` = 1, so the ordinary race rules already keep it for Viral
empires and correctly drop it from anyone else's.

## `chat` → `Chat.cfc`

See `CHAT-MODERATION.md` for the full model.

| Method | Guard | Purpose |
|---|---|---|
| `feed` | `chat.view` | Live feed, newest first, with `actioned`/`reported`/`removed`/`original` flags |
| `report` | `chat.report`, **plus** `chat.removed.report` when the post is already removed | File a pending complaint |
| `act` | `chat.action` | Warn or silence (UC posts convert to a pending report) |
| `remove` | `chat.remove` | Scrub the post text; preserves the original |
| `history` | `players.chathistory.view` | One player's chat outcomes |
| `post` | `chat.post` | Say something in GC chat as staff `{ name, text }` — always chan 1, game `1`, server 4. `name` defaults to the admin, 1-20 of `A-Z a-z 0-9 space . _ -`; `text` up to 150. The admin's account id is stored in the name. `report` and `remove` work on staff posts; `act` refuses them |

## `moderation` → `Moderation.cfc`

| Method | Guard | Purpose |
|---|---|---|
| `chatQueue` / `chatDetail` / `chatResolve` | `moderation.chat` | Chat complaint queue. `chatDetail` fetches any complaint by id regardless of `readflag`, which powers the `?cid=` deep link, and returns `resolution` (who / when / reason) once resolved |
| `chatActions` | `moderation.chatlog`; PM rows only with `moderation.pm` | Every moderation action taken, newest first — chat warnings / silences / removals, plus PM warnings / freezes / suspends / blacklists. Each row carries `source` (`chat` / `pm`). Pages on `user_pm_abuse.rowid` via `before`; `limit` 1-500, default 100 |
| `dismissed` | `moderation.dismissed`; `kind=pm` also `moderation.pm` | Complaints closed with no action, newest first. `kind` = `chat` (pages on `chat_abuse.id`) or `pm` (pages on `user_pm_abuse.rowid`); `before`, `limit` as above |
| `pmQueue` / `pmDetail` / `pmResolve` | `moderation.pm` | PM complaints. `pmDetail` fetches any complaint by id regardless of `aflag`, and starts from the complaint rather than the message, which powers the `?pid=` deep link, and returns `resolution` once resolved — see `CHAT-MODERATION.md` |

### `chatResolve` / `pmResolve` — `showreason`

Both take **`showreason`** (1/0, default 0), the same "Show this reason to the
player" box the player editors carry. Ticked, the `reason` is added to the
verdict's message, under the same `Reason given:` heading and 200-character cap
(`Base.reasonForPlayer()`). Who gets it depends on the verdict — never both:

| Verdict | Reason goes to |
|---|---|
| chat **warn** / **silence**, PM **warn** / **freeze3** | the offender |
| **No action** (either queue) | the complainer's reply — the only message that verdict sends, and the reason is what explains it |
| PM **suspend** / **blacklist** | nobody — the offender is sent no message |

The audit detail gains `reasonShown: true` and `reasonShownTo`
(`offender` / `complainer`) only when the reason really went out.
| `ipBans` / `ipBanAdd` / `ipBanRemove` | `moderation.bans` | IP blacklist |
| `emailBans` / `emailBanAdd` / `emailBanRemove` | `moderation.email` | Email blacklist |

## `anticheat` → `AntiCheat.cfc`

All logged as views — every tool use is recorded, including filter changes.

| Method | Guard | Purpose |
|---|---|---|
| `sharedIp` | `anticheat` | Multi-account detection by player id, exact username + server, or IP. Decodes stored numeric IPs for display |
| `interAttacks` | `anticheat` | Attacks between a set of user ids |
| `turnTop` | `anticheat` | Turn burn over a day window, optionally one player |

## `economy` → `Economy.cfc`

| Method | Guard | Purpose |
|---|---|---|
| `totals` | `economy` | Per-server resource totals (all / 30d / 7d). Logs `economy.view` |
| `topOptions` | `economy` | Ranking options for the dropdown |
| `top` | `economy` | Richest players by resource/mineral/artifact-rarity. Logs `economy.topView` per resource |

## `federations` → `Federations.cfc`

| Method | Guard | Purpose |
|---|---|---|
| `list` | `federations` | Federation search. Logs `fed.listView` |
| `detail` | `federations` | Members, wars (`federations.atwar.view`), change log (`federations.edit`), faction (`federations.faction.view`, stripped entirely below it). Logs `fed.view` |
| `update` | `federations.edit`, plus `federations.faction.edit` for faction | Rename, reassign leader (must already be a member), set faction. Takes an optional reason recorded in both logs |
| `resetNotices` | `federations.edit` | Blank all fed notice fields |

## `content` → `Content.cfc`

Game-data reference. Each read logs a `gamedata.*` view.

| Method | Guard | Purpose |
|---|---|---|
| `races` | `gamedata.view` (+ `content.races.full` for hidden races 7+) | Race definitions |
| `shipTypes` | `gamedata.view` (+ `content.ships.full`) | Ship types |
| `planetTypes` | `gamedata.view` (+ `content.planets.full`) | Planet types |
| `goods` | `gamedata.view` (+ `content.artifacts.full`) | Artifacts / goods |
| `research` / `projects` | `gamedata.view` | Research tree, projects |
| `schema` | `gamedata.view` | Column metadata driving the editors |
| `update` / `addShip` / `changeLog` | `gamedata.edit` | Edit definitions; add a ship; view the edit history |

## `events` → `Events.cfc`

| Method | Guard | Purpose |
|---|---|---|
| `recent` | `events.basic`, upgraded by `events` | Global event watch. The basic tier hides battle reports and artifact events; the two tiers log different actions |

## `admins` → `Admins.cfc`

| Method | Guard | Purpose |
|---|---|---|
| `list` | `admins.view` | All panel accounts. Logs `admins.listView` |
| `create` / `update` / `setActive` | `admins.manage` | Account management, subject to the rank rules in `ACCESS-CONTROL.md` |
| `resetPassword` | self, else `admins.manage` + rank | Generates a password shown once |
| `loginLog` | `admins.logins` | Login history |
| `guideList` | `admins.guidelist` | In-game staff list, incl. rows whose account is gone |
| `guideSave` | `admins.guidelist` | Add or edit one listing `{ uid, guide, mod, admin }` |
| `guideRemove` | `admins.guidelist` | Take an empire off the list `{ uid }` |

### The in-game staff list — `gcc.user_guide`

A different thing from the panel accounts above, on the same module because it
answers the same question from the other side: `admin_account` rows are who can
use **this panel**, `user_guide` rows are who the **game** tells players its
staff are. A person can be on either, both, or neither.

| Flag | What it does today |
|---|---|
| `guideflag` | Printed under "Guides" on the Help Center main page (`f_he_main.cfm`, via `application.s_list_guide`); **Guides** on the Forum Staff page |
| `adminflag` | Printed under "Admins" in the same place; **Administrators** on the Forum Staff page |
| `modflag` | **Moderators** on the Forum Staff page — nothing else reads it |

Any flag also puts the player in `application.s_list_gma`.

**Forum Staff page** (`Forum/components/People.cfc` `staffDirectory()`): a person
with several flags is listed once, under the highest (Admin > Mod > Guide). Rows
with no flags, or pointing at a deleted account, are not listed. The
**Ownership** band is the hardcoded `STAFF_OWNERS` id list in `People.cfc`, not a
flag — this screen cannot grant or remove it. `guideSave`/`guideRemove` set
`server.gccStaffListAt`, and the Forum drops any cached copy older than that
stamp, so edits show there on the next page load.

The game's lists are built by `s_loadsystem.cfm` at application start and cached in the
game's application scope, so an edit reaches the Help Center page on the next **runtime
refresh**, not immediately. That is stated on the screen rather than worked
around — the alternative is re-running the game's whole bootstrap on every
tickbox. (Contrast the announcement banner, which has a cheap targeted
cache-bust; this one does not.)

`guideSave` requires the empire to exist. The legacy page did not, which is how
six of the twenty-seven rows came to point at deleted accounts — `guideList`
returns those with `exists: false` so they can be cleared.

## `audit` → `Audit.cfc`

| Method | Guard | Purpose |
|---|---|---|
| `search` | `audit` | Panel audit log. Filter param is **`actionFilter`**, not `action` — the router owns `action`. The screen takes `#/audit?username=<admin>` to open pre-filtered |
| `gameHistory` | `audit` | Legacy game-side `user_pm_abuse` stream, incl. old edmin actions |

## `server` → `ServerOps.cfc`

| Method | Guard | Purpose |
|---|---|---|
| `runtimeRefresh` | `server` | Re-runs the game's `s_loadvar.cfm` / `s_loadsystem.cfm` |
| `getSettings` / `setSetting` | `server` | Panel settings (MOTD, `send_player_emails`) |
| `onlineHistory` | level 1 | Concurrent-players series for the dashboard chart |
| `announcement` | `server.settings` | Current announcement banner + whether players can actually see it |
| `announcementSave` | `server.settings` | Post or take down the banner. `show` drives the stamp — far future to show, long past to hide — and `text` is stored either way |

The announcement pair backs the **Server Settings** screen, not Server. It is
gated on `server.settings` (6) rather than `server` (9) because posting a notice
is routine comms work, while server ops re-runs the game's bootstrap.

## `acl` → `Acl.cfc`

| Method | Guard | Purpose |
|---|---|---|
| `list` | `acl` (pinned 9) | Registry + current effective levels, grouped |
| `set` | `acl` (pinned 9) | Override one section's minimum level; busts the 60s cache |
