# Architecture

How a request flows through the panel, and the contracts every layer relies on.

```
browser  ──►  index.cfm            SPA shell. Injects window.__ASSET_V__.
              assets/app.js        Router + API client + UI kit (ES module).
              assets/views/*.js    One lazy-loaded module per screen.
                   │
                   │  GET|POST /Admin/api/?action=<module>.<method>
                   ▼
              api/index.cfm        Router: auth, CSRF, module/method dispatch.
              api/components/*.cfc One handler CFC per module, all extend Base.
                   │
                   ▼
              gcc / gcc_log        Game data (read + write).
              gcc_admin            Panel auth, ACL overrides, audit trail, notes.
```

---

## 1. The shell (`index.cfm`)

Serves the single HTML page and computes a **cache-bust key**: it walks
`assets/` recursively and takes the newest file mtime, then emits

```html
<script>window.__ASSET_V__ = "<newest-mtime>"; </script>
```

`app.js` re-exports that as `VERSION` and appends it to every dynamic view
import. Consequence worth knowing: **any** asset edit invalidates **all** view
modules, so a panel change shows up on the next full load without a hard
refresh. If `__ASSET_V__` is somehow missing, `app.js` falls back to
`Date.now()` — a stale view can never linger.

## 2. `Application.cfc`

The panel is a **separate CFML application** from the game
(`this.name = "GCCAdminPanel-<version>"`), so its sessions never collide with a
player session. It declares its own `gcc`, `gcc_log`, and `gcc_admin`
datasources, so no Lucee admin datasource configuration is needed.

Session timeout is 60 minutes. An empty `onDebug()` is defined deliberately —
the local Lucee web context has debugging enabled globally and would otherwise
append ~22 KB of debug HTML to every response, which breaks JSON parsing.

## 3. The API router (`api/index.cfm`)

Every API call is `?action=<module>.<method>`. The router, in order:

1. `setting enableCfOutputOnly=true showDebugOutput=false` — nothing but the
   JSON envelope may reach the client.
2. Parses the JSON request body (POST only) into `request.apiBody`. A malformed
   body is a `400`.
3. Sanitises `url.action` to `[a-zA-Z0-9_.]` and splits it on `.`.
4. **Deletes `url.action`.** This matters: handlers read their own parameters
   from `url`, and a handler that wanted a filter parameter literally named
   `action` would otherwise collide with the routing key. (This is why
   `Audit.search` reads `actionFilter`, not `action`.)
5. Looks the module up in `moduleMap`. Unknown module → `404`.
6. `requireAuth()` unless the action is in `publicActions`
   (`auth.login`, `auth.bootstrap`, `auth.status`).
7. `requireCsrf()` for every POST except login/bootstrap.
8. Instantiates the handler and checks the method against that CFC's
   `this.endpoints` allowlist. Not listed → `404`.
9. `invoke(handler, method)`.

If control ever returns from the handler, the router emits
`500 "Handler produced no response"` — handlers are expected to exit through
`apiOut`/`apiError`, both of which `abort`.

### `this.endpoints` is a security boundary

Only methods named in a CFC's `this.endpoints` string are reachable over HTTP.
Every handler inherits dozens of helpers from `Base.cfc` (`qGame`, `auditLog`,
`playerPm`, …); the allowlist is what stops those being invoked directly. When
you add a public method you **must** add it to `this.endpoints`, and when you
delete one you must remove it.

## 4. `Base.cfc`

Every handler extends it. It owns:

| Area | Members |
|---|---|
| Response envelope | `apiOut(data)`, `apiError(status, msg)` — both `abort` |
| Parameters | `p`, `pInt`, `pTrim`, `csvIntList`, `requirePost` |
| Auth guards | `isAuthed`, `requireAuth`, `requireLevel`, `requireCsrf` |
| Access control | `aclRegistry`, `sectionLevel`, `requireSection`, `canSection`, `sectionLevelsFor`, `aclCacheBust` |
| Queries | `qGame`, `qLog`, `qAdmin`, `qRows` |
| Audit | `auditLog`, `auditView`, `logChange`, `gamePmAbuse` |
| Game helpers | IP conversion, `serverLabel`, `raceName`, `playerPm`, `playerEmail`, `fmtDate` |

Datasource constants: `DS_GAME = "gcc"`, `DS_LOG = "gcc_log"`,
`DS_ADMIN = "gcc_admin"`.

It also includes `Modules/Functions/Functions.cfm`, which brings in the shared
IPv4/IPv6 helpers and the Argon2 password helpers. **All** IP handling must flow
through those — never hand-roll parsing.

### Parameter precedence

`p(name, default)` reads the **JSON body first**, then `url`. That single rule
is why the verification harness (see `DEVELOPMENT.md`) can drive POST endpoints
by populating `request.apiBody` from `url` — the handler cannot tell the
difference.

### Response envelope

Always exactly one of:

```json
{ "ok": true,  "data": { … } }
{ "ok": false, "error": "message" }
```

The client's `api()` unwraps `data` and throws `ApiError` on `ok: false`.

## 5. The frontend core (`assets/app.js`)

No build step, no framework, no dependencies. It exports:

- **API** — `api(action, {method, body, params})`, `ApiError`, `state`
  (`state.me` holds the signed-in admin, including `sectionLevels`).
- **DOM** — `el(tag, attrs, ...children)`. String children become **text
  nodes**; `{ html: … }` sets `innerHTML` and is reserved for panel-authored
  markup only (see `CONVENTIONS.md`).
- **UI kit** — `card`, `table`, `modal`, `badge`, `statCard`, `timeChart`,
  `sparkline`, `toast`, `loading`, `errorBox`, `confirmDanger`,
  `searchableSelect`, `eventFeed`, `battleReport`, `ipCell`, `empireName`,
  `serverBadge`, `activeBadge`, `nameId`.
- **Formatting** — `fmtNum` (locale), `fmtAbbr` (K/M/B/T), `levelName`,
  `LEVEL_NAMES`.
- **Access** — `secLevel(section)`, `canSec(section)`.

### Routing

`routes` is an array of `{ path: RegExp, view, crumbs, params?, sec? }`.
`renderRoute()` matches `location.hash`, splits the query string, builds
`params` from both the query and the regex capture groups, then lazy-imports
`views/<view>.js` and calls its default export as `view(fragment, params)`.

A route may declare `sec: "<acl-section>"`. When present the router checks
`canSec()` and renders a "No access" panel instead of the view. This matters for
views with no server-side endpoint to enforce access — the static **What's New**
page is the reason it exists. Everything else is enforced server-side too.

### Navigation

`navDef` is the sidebar. Each item carries `sec`, and `buildSidebar` hides items
the signed-in admin can't reach (and hides a whole section heading when nothing
under it is visible). **UI gating is cosmetic only** — the server re-checks.

## 6. Views (`assets/views/*.js`)

One module per screen, default-exporting `async function(root, params)`. They
import helpers from `../app.js`, call `api(...)`, and append DOM to `root`.
Views are lazy-loaded, so a screen an admin never opens is never fetched.

## 7. Databases

**`gcc_admin`** — the panel's own database. Nothing here is game state.

| Table | Purpose |
|---|---|
| `admin_account` | Panel logins. Argon2id hashes only, no plaintext column. |
| `admin_login_log` | Every attempt; also drives the per-IP flood lockout. |
| `admin_audit_log` | Structured trail of actions **and views**. |
| `admin_note` | Free-form case notes pinned to a player. |
| `admin_setting` | Panel key/value settings (MOTD, `send_player_emails`). |
| `admin_acl` | Per-section clearance overrides. Empty = all defaults. |

**`gcc` / `gcc_log`** — the live game. The panel reads freely and writes only
through the deliberate paths described in `AUDIT-LOGGING.md` and the game-side
contracts in the README (PM + `user_pm_abuse` + mute flag + `lastaccess`).

One game table belongs to the panel: **`gcc.admin_reset`**, the forced-logout
flag. The panel inserts a row when an admin edits, suspends, or blacklists an
account; the game clears that player's session on their next page load, shows
the reason, and deletes the row. `Base.cfc` owns the write path
(`forceRelog` / `ensureResetTable`, which auto-creates the table); the game side
lives at the top of `i_f_800.cfm` with `z_admin_reset_notice.cfm`.

Schema files live in `sql/`. Note that **two of the three target `gcc`**, not
`gcc_admin` — each declares `USE` explicitly:

| File | Target DB | Purpose |
|---|---|---|
| `gcc_admin.sql` | `gcc_admin` | Creates the database and all six panel tables. Run first. |
| `chat_post_expand.sql` | `gcc` | Widens `chat.post` to `varchar(255)` for scrubbed-post markup. |
| `admin_reset.sql` | `gcc` | Creates the forced-logout table. Optional — the panel auto-creates it. |
