# Development

Local environment, how to actually exercise a change, and the schema files.

## Local environment

The `gcc-local-app` container mounts the **main repo's** `app/` at `/var/www`,
served on `http://localhost:8888/`.

```
http://localhost:8888/Admin/
```

### Git worktrees are served too

Worktrees live under `app/.claude/worktrees/<name>/`, which is physically inside
that mount. So a worktree's copy of the panel is reachable at:

```
http://localhost:8888/.claude/worktrees/<name>/app/Admin/
```

Everything below works the same in a worktree — just prefix the path. **Watch
which tree you are editing.** Editing `app/theme.css` when you meant
`app/.claude/worktrees/<name>/app/theme.css` produces the maddening symptom of a
CSS change that never takes effect, because the served worktree file is
untouched.

### Environment quirks

- **Lucee debugging is on globally** in the local web context and appends ~22 KB
  of debug HTML to every response, which breaks JSON parsing. The panel's
  `Application.cfc` defines an empty `onDebug()` to suppress it (the game's root
  `Application.cfc` does the same).
- **`gcc.staff` is a stub locally** (only an `id` column) versus production's
  full schema. The player-detail endpoint isolates staff/fleet/colony/goods reads
  in `try/catch` so schema drift between dumps can't sink the whole response.
- `gcc_admin` already contained unrelated tables from other work
  (`AccessLevels`, `Admins`, `Empires`); the panel uses distinct names so they
  coexist.
- Local dev seed SQL lives in `dev/db/` and is **untracked**.

## Verifying a change

Static checks first — they are instant:

```bash
node --check app/Admin/assets/views/chat.js
```

`node --check` catches syntax, **not** bad imports. Only loading the route proves
an import.

### The API harness

To exercise an endpoint without logging in, drop a temporary CFM beside the panel
that fakes a session and invokes the handler. Because `Base.p()` reads the JSON
body **first and `url` second**, copying `url` into `request.apiBody` drives POST
endpoints exactly as the router would.

```cfml
<cfscript>
setting enableCfOutputOnly=true showDebugOutput=false;
session.adminAccountId = 1;
session.adminLevel     = val(url.lvl ?: 9);   // vary to test ACL gating
session.adminUsername  = "harness";
session.csrfToken      = "x";

function jout(x) { cfcontent(type="application/json"); writeOutput(serializeJSON(x)); abort; }

// Read-only diagnostics.
if ((url.mode ?: "") == "sql") {
    q = new query(); q.setDatasource(url.ds ?: "gcc"); q.setSQL(url.q);
    r = q.execute().getResult(); out = [];
    for (i = 1; i <= r.recordcount; i++) {
        row = {}; for (col in listToArray(r.columnList)) row[col] = r[col][i];
        arrayAppend(out, row);
    }
    jout(out);
}

request.apiBody = {};
for (k in url) request.apiBody[k] = url[k];

comp = new api.components.Chat();     // note the api.components. prefix
invoke(comp, url.method ?: "feed");
</cfscript>
```

Save as `app/Admin/_t.cfm`, hit it with `curl`, then **delete it**.

```bash
B="http://localhost:8888/.claude/worktrees/<name>/app/Admin/_t.cfm"
curl -s "$B?method=feed&limit=5" | node -e "…"
curl -s -X POST "$B?method=remove&chatId=123&reason=test"
```

Two things to get right:

- The component path is `api.components.<Name>` when the harness sits in
  `Admin/` — `components.<Name>` resolves relative to the wrong directory and
  produces a generic "something broke" page.
- Test the **gate**, not just the happy path: pass `lvl=` below and above the
  section's level and confirm you get `403` then `200`.

### Verifying the real thing

For UI work, the in-app browser can measure computed styles, which beats
eyeballing a screenshot:

```js
getComputedStyle(el).paddingLeft            // did my rule actually win?
details.offsetHeight                         // does the <details> collapse?
```

That is how the search-box specificity tie and the `<hr>` spacing were pinned
down objectively rather than guessed at.

## Touching live data during a test

The local database is real data that someone is using. Discipline that has been
learned the hard way here:

1. **Capture the original first**, into a shell variable or a file, before the
   mutating call.
2. **Delete precisely.** SQL `AND`/`OR` precedence is a live hazard:
   `WHERE a AND b OR c` is `(a AND b) OR c` and will match far more than you
   intended. Parenthesise, and prefer deleting by explicit `id IN (…)` captured
   from a prior `SELECT`.
3. **Verify the restore**, don't assume it. Shell quoting of values containing
   backticks, quotes, or `$` fails silently and can blank a row.
4. **Sweep for residue** afterwards: rows in `chat_abuse`, `user_pm_abuse`,
   `user_pm`, and `admin_audit_log` written under the harness username.

```bash
curl -s "$B?mode=sql&ds=gcc_admin&q=SELECT%20COUNT(*)%20n%20FROM%20admin_audit_log%20WHERE%20username='harness'"
```

If you cannot restore something, say so plainly rather than leaving it silently
wrong.

## Schema files (`sql/`)

This one folder holds migrations for **three different databases**.

| File | Target DB | Purpose |
|---|---|---|
| `gcc_admin.sql` | `gcc_admin` | Creates the database and all panel tables. Run first. |
| `admin_obfuscation.sql` | `gcc_admin` | Obfuscated-account screen. |
| `admin_obfuscation_pairs.sql` | `gcc_admin` | Obfuscation pairs (cross-DB join to `gcc`). |
| `admin_obfuscation_audit_purge.sql` | `gcc_admin` | Purges obfuscation audit rows. |
| `admin_pref.sql` | `gcc_admin` | Per-admin preferences. |
| `admin_todo.sql` | `gcc_admin` | Staff to-do board. |
| `admin_reset.sql` | **`gcc`** | Forced-logout flag table. Optional: `Base.ensureResetTable()` auto-creates it. |
| `chat_post_expand.sql` | **`gcc`** | Widens `chat.post` to `varchar(255)`. |
| `chat_name_expand.sql` | **`gcc`** | Widens `chat.name` and `chat_abuse.name` to `varchar(80)` for staff-styled chat names. |
| `fed_flag.sql` | **`gcc`** | Federation flag column. |
| `convergence_event.sql` | **`gcc`** | Convergence event: factions + projects 12/13. |
| `he_type_system_messages.sql` | **`gcc`** | `System > Messages` Help Center section. |
| `user_pm_userto_index.sql` | **`gcc`** | Inbox index on `user_pm`. |
| `forum_schema.sql` | **`gcc`** | All `forum_*` tables, indexes and section seed. |
| `forum_glyph_widen.sql` | **`gcc`** | Standalone `forum_section.glyph` widen. |
| `forum_acl.sql` | **`gcc`** | `forum_acl` — configurable clearance for the Forum's Access Control screen. The forum's own equivalent of `admin_acl`; separate table, separate ladder. |
| `gcc_event_artifact_type.sql` | **`gcc`** | `event (userid, type, id)` index, and self artifact uses retyped 2 → 5 in id-range batches. **Deploy the code first, then run it.** Fixes the per-page Events-indicator lookup. |
| `gcc_event_attack_indexes.sql` | **`gcc`** | `event_attack (source, id)` / `(target, id)` for the battle log's UNION query. Online build, any order. |
| `forum_lastpost_indexes.sql` | **`gcc`** | `hef (lastpost)` / `he (lastpost)` for the forum's Latest Activity rail. Also in `forum_schema.sql`. |
| `forum_archive_rename.sql` | **`gcc`** | Standalone rename of the Forum archive section to "The Archive" (drops the date range). |
| `forum_section_events.sql` | **`gcc`** | Adds the Forum section "Events" — `he_type` 106 plus its `forum_section` row, in The Galaxy below Federation. Also in `forum_schema.sql`. **Restart the app after applying.** |
| `forum_thread_staff_only.sql` | **`gcc`** | `forum_thread_meta.staff_only_at` / `_by` — the per-thread "no player replies" flag used by the Events section. Also in `forum_schema.sql`. |
| `05_api.sql` | **`gcc`** + `gcc_log` | Player API columns and log table. Fully qualified. |

### Every migration names its database — no exceptions

**Start every `.sql` file with a `USE`**, or fully qualify every object in it.
`USE` is preferred because it is one line at the top instead of a prefix on
every statement, and it cannot be half-applied.

```sql
-- ---------------------------------------------------------------------------
-- What this does, and why.
-- Target: gcc (the GAME database), NOT gcc_admin.
-- ---------------------------------------------------------------------------

USE `gcc`;

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

Two rules follow from it:

- **Place the `USE` after the header comment and before the first statement.**
  The one exception is `gcc_admin.sql`, where `CREATE DATABASE IF NOT EXISTS`
  necessarily comes first.
- **A file that legitimately spans two databases** (only `05_api.sql` today)
  fully qualifies every object *and* still carries a `USE` — so that anything
  added to it later without a qualifier lands somewhere deliberate rather than
  wherever the session happened to be pointing.

Why this matters here specifically: the folder's name says `Admin`, so the
default assumption is `gcc_admin`, and **ten of these fifteen files target
`gcc`**. Running a game migration against `gcc_admin` is a silent no-op at best
and a table created in the wrong database at worst — and the second one is
invisible until something reads from the right database and finds nothing.

With the `USE` present, the file no longer depends on the invoker remembering:

```bash
mysql -u WolfrenInd -p < app/Admin/sql/forum_schema.sql   # no DB argument needed
```

Write migrations to be **re-runnable**: widening a column, `CREATE TABLE IF NOT
EXISTS`, and `INSERT … ON DUPLICATE KEY` are all safe to apply twice. When you
add a migration, apply it locally and confirm with `information_schema` rather
than trusting the statement returned success:

```sql
SELECT COLUMN_TYPE FROM information_schema.columns
WHERE table_schema='gcc' AND table_name='chat' AND column_name='post';
```

## Working on the panel

- **Never** commit the harness CFM or scratch HTML. `git status` before every
  commit; the panel folder should only ever contain real files.
- Match the surrounding code's comment density — it is deliberately high, and
  the comments are what stop the traps in `CONVENTIONS.md` being re-introduced.
- Update `assets/views/changelog.js` ("What's New") when admin-visible behaviour
  changes — see the `admin-changelog` skill for the rules about what belongs
  there versus the public game changelog.
