# Conventions and gotchas

House style for the panel, plus the traps that have actually cost time here.
Every item below is something that bit real code in this codebase.

---

## CFML (Lucee)

### Script CFCs only

New panel code is **script-based CFML**. Never write `<cfquery>` tags in
`api/components/*.cfc` — use the `Query` component through `Base.cfc`'s helpers.
(The *game* is tag-based CFML; that is a different world with different rules.)

### Query pattern

Pass parameters as `{ name: [ value, "type" ] }` and let the helper bind them:

```cfc
var u = qGame("
    SELECT id, nic FROM `user` WHERE nic = :nic AND server = :srv
", { nic: [ username, "varchar" ], srv: [ srv, "integer" ] });
```

Named `addParam` binding is used because **positional binding is unreliable on
Lucee**. When you need to build a query by hand:

```cfc
var q = new Query();
q.setDatasource(variables.DS_GAME);
q.addParam(name = "n1", value = someValue, cfsqltype = "varchar");
q.setSQL("SELECT … WHERE name = :n1");
var r = q.execute().getResult();
```

Only ever inline a value into SQL when it is **server-owned** — a constant, a
value already reduced to an integer, or an allow-listed identifier. `csvIntList()`
exists for exactly this: it reduces `"1,2,x,3"` to `"1,2,3"` so an id list is
safe to inline.

### `##` is an escaped `#` — and only inside CFML strings

Inside a CFML string, `#` starts an interpolation; `##` is a literal `#`.

```cfc
"Ref " & abuseId                          // fine
"(###abuseId#)"                           // literal "(#" + value + ")"
```

Unbalanced `#` is a **parse error**, not a runtime one — the whole file fails to
compile. When it gets hard to read, concatenate instead:

```cfc
"… pending report (##" & abuseId & ") — action it from the UC panel."
```

**The inverse trap:** in **JavaScript** template literals `#` is an ordinary
character. Writing `` `Complaint ##${id}` `` renders a literal `##47091`. This
CFML habit leaked into three view files before it was caught. In `.js`, always
single `#`.

### Reserved scopes cannot be local variable names

`server`, `application`, `session`, `request`, `url`, `form`, `client`,
`variables`, `arguments` are scopes. A local named `server` produces a
bewildering error like *"can't compare Complex Object Type Struct with a numeric
value"*, because you are comparing the whole server scope.

```cfc
var srv = pInt("server", 4);   // NOT: var server = …
```

### Other Lucee quirks

- `arrayFind(arr, val) == 0` means **not found** (there is no `-1`).
- `structKeyExists` before touching an optional key; a missing key throws.
- Wrap genuinely optional work in `try/catch` when a schema may drift between
  dumps — the player-detail endpoint isolates staff/fleet/colony/goods reads for
  exactly this reason.
- Never write `<cfquery>`, `<cfmail>`, `<cfhttp>` (with angle brackets) in CFC
  **comments** — say "the cfquery tag" in prose instead.

### Exit discipline

`apiOut()` and `apiError()` both `abort`, so they end the request. Even so,
early-exit branches should `return;` immediately after:

```cfc
if (suspect.recordcount == 0) {
    apiOut({ "done": true, "message": "…no longer exists…" });
    return;                       // explicit: never rely on abort alone
}
```

It costs nothing, it survives someone later replacing `apiOut` with something
that doesn't abort, and it makes the control flow readable.

### Numeric binding

`Content.update` binds every non-text column as `double` by default, which
silently truncates or mangles integer columns. `isIntKind(kind)` decides which
columns bind as `integer`. If you add a new column kind, classify it there.

---

## JavaScript

### `el()` — text vs HTML

```js
el("div", {}, m.post)                 // TEXT node — safe for player content
el("div", { html: trustedMarkup })    // innerHTML — panel-authored only
```

**Any** player-controlled string (chat post, empire name, note text) goes in as
a child, never through `html`. Where game-authored HTML must render (event feed,
battle reports) it passes through the allowlist sanitizer first.

### The event sanitizer

`sanitizeEvent()` parses into an inert `<template>` (images never load, scripts
never run), drops any tag outside `EVENT_ALLOWED_TAGS`, keeps its text, and
strips `on*` handlers, inline styles, and `javascript:`/`data:` URLs. It replaced
a version that only stripped `<script>` and therefore allowed
`<img onerror=…>`.

`battleReport()` deliberately parses the **raw** HTML instead: it needs the
`<table>` elements the sanitizer strips, it uses `DOMParser` (also inert), and it
only ever reads `textContent`. Passing sanitized HTML there broke battle
rendering once — the tables were gone, so it fell through to a flattened text
fallback.

### `replaceChildren` traps

```js
node.replaceChildren(maybeNull);      // renders the string "null"
node.replaceChildren(arrayOfNodes);   // renders "[object HTMLDivElement]"
```

Both have shipped as visible bugs here. Use:

```js
if (x) node.replaceChildren(x); else node.replaceChildren();
node.replaceChildren(...arrayOfNodes);
```

### Tables

`table(cols, rows, opts)` — a column with `num: true` right-aligns **both** the
cell and the header. (The header used to be left-aligned above right-aligned
figures; the `num` class is now emitted on the `<th>` too, while the mono/tabular
font stays cell-only so headers keep their uppercase styling.)

`sortable: true` adds click-to-sort; `nosort` opts a column out. `onRow` makes
rows clickable but ignores clicks that land on `a`, `button`, `input`, `.ipcell`.

### Numbers

`fmtNum` for normal display; `fmtAbbr` (K/M/B/T) when the container is too narrow
for the full figure. The player detail cards use a `bigNum()` that falls back to
`fmtAbbr` past ~11 characters and puts the exact value in a `title`.

### Imports

There is no build step. Only import identifiers `../app.js` actually exports —
a typo is a runtime failure on that route, not a compile error. `node --check`
catches syntax but not bad imports; load the route to be sure.

---

## CSS

The panel's design system is `assets/admin.css`; the game's modern theme is
`app/theme.css`. Both are hand-written, no preprocessor.

### Specificity ties bite

`admin.css` has a base reset:

```css
input[type="text"], input[type="password"], … { padding: 8px 11px; }
```

That selector scores the same as `.gsearch input` (0,1,1) and comes later, so it
**overrode** `padding-left: 32px` back to 11px — putting the search icon on top
of the placeholder. The fix was to raise specificity deliberately:

```css
.gsearch input[type="text"] { width: 260px; padding-left: 34px; }
```

When a component rule mysteriously doesn't apply, check for a later base-element
rule with equal specificity before adding `!important`.

### Theme-scoped rules

`theme.css` rules are scoped per theme:

```css
body:is([data-theme="nebula"], [data-theme="daylight"]) .thing,
body[data-theme="classic"] .thing { … }
```

Follow the existing double-selector shape so a new rule applies in every theme.

### Shared class names across surfaces

`.removed` is defined in three places on purpose (`admin.css`, `theme.css`,
`i_chatt.cfm`) because the same stored markup renders in three independent
surfaces. If you add another such class, document it — a class defined in only
one of them looks like it works until someone views the other surface.

---

## Naming and layout

- One CFC per module, one view module per screen, both named after the module.
- Endpoint names are lowerCamel verbs (`resourcesSave`, `chatResolve`).
- ACL sections are dotted and hierarchical (`players.fleet.edit`).
- Audit actions mirror the module (`fed.update`, `player.fleetEdit`).
- Comments explain **why**, not what. The codebase is dense with short "this is
  why it looks wrong" notes — keep that up; they are the reason these traps
  don't get re-introduced.

## SQL migrations declare their database

Every `.sql` file in `sql/` **starts with a `USE`** (after its header comment,
before the first statement), or fully qualifies every object it touches. `USE`
is preferred: one line at the top rather than a prefix on every statement, and
it cannot be half-applied.

```sql
-- Target: gcc (the GAME database), NOT gcc_admin.
USE `gcc`;

ALTER TABLE `chat` MODIFY `post` varchar(255) DEFAULT NULL;
```

This folder holds migrations for `gcc_admin`, `gcc` **and** `gcc_log`, and its
name biases the reader toward `gcc_admin` — while ten of the fifteen files
target `gcc`. A migration run against the wrong database is a silent no-op, or
creates the table somewhere nothing will ever read it.

The rule also protects future edits: a file that already selects its database
cannot grow a new unqualified `CREATE TABLE` that lands in the wrong place.

See [`DEVELOPMENT.md`](DEVELOPMENT.md#every-migration-names-its-database--no-exceptions)
for the full file-by-file table.
