---
name: admin-verify
description: Verify an Admin panel change end-to-end against the running local stack — drive the real endpoint with a temporary CFM harness, test the ACL gate, check the UI with computed styles, then clean up every row and file you created. Use before committing any Admin panel change.
---

# Verifying an Admin panel change

Static checks prove syntax. They do **not** prove an endpoint works, an ACL
gates, or a CSS rule wins. Drive the real thing.

## 1. Static first (instant)

```bash
node --check app/Admin/assets/views/<view>.js
```

`node --check` catches syntax only — **not** a bad import from `../app.js`. A
typo'd import fails at runtime on that route. CFML has no equivalent; a `##`
imbalance is a parse error that only shows when the file is hit.

## 2. Drive the endpoint

Write a temporary harness beside the panel. `Base.p()` reads the JSON body
first and `url` second, so 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 this to test the gate
session.adminUsername  = "harness";
session.csrfToken      = "x";
mode = url.mode ?: "";

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

if (mode == "sql") {                      // read-only diagnostics
    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);
}
if (mode == "exec") {                     // cleanup writes only
    q = new query(); q.setDatasource(url.ds ?: "gcc"); q.setSQL(url.q); q.execute(); jout({done:true});
}

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` (or the worktree equivalent) and call it:

```bash
B="http://localhost:8888/.claude/worktrees/<name>/app/Admin/_t.cfm"
curl -s "$B?method=feed&limit=5" | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>console.log(JSON.stringify(JSON.parse(s))))"
curl -s -X POST "$B?method=remove&chatId=123&reason=test"
```

Two things that waste time if you get them wrong:

- **Component path** is `api.components.<Name>` when the harness sits in
  `Admin/`. `components.<Name>` resolves from the wrong directory and returns a
  generic *"GCC Admin Panel — something broke"* HTML page.
- **URL prefix**: in a worktree it is
  `/.claude/worktrees/<name>/app/Admin/...`. Hitting `/Admin/...` tests the main
  repo's copy, not your edit.

## 3. Test the gate, not just the happy path

Any change touching an ACL must be proven in **both** directions:

```bash
curl -s -X POST "$B?lvl=3&method=remove&chatId=123&reason=x"   # expect 200
curl -s -X POST "$B?lvl=2&method=remove&chatId=123&reason=x"   # expect 403
```

Also exercise the failure branches you wrote: missing required field → 400,
unknown id → 404. A guard that was never fired is a guard you haven't tested.

## 4. Verify the UI objectively

For visual work, measure rather than squint. The in-app browser can evaluate
JavaScript against the served page:

```js
getComputedStyle(el).paddingLeft      // did my rule actually win the cascade?
el.getBoundingClientRect().right      // do header and cells share an edge?
details.offsetHeight                   // does the <details> actually collapse?
```

This is how a specificity tie (a base `input[type=text]` rule silently
overriding a component rule) and an `<hr>` spacing question were settled
definitively instead of by eye.

**Cache-bust when checking CSS.** Link the stylesheet as `admin.css?v=2` in a
scratch page; a stale sheet will show you the old rule and send you chasing a
bug you already fixed. Confirm the served file actually contains your rule:

```bash
curl -s "http://localhost:8888/<prefix>/app/theme.css?v=$(date +%s)" | grep -n "my-new-class"
```

If it's absent, you almost certainly edited the wrong tree.

## 5. Clean up — completely

The local database is real data someone is using. Before mutating anything:

1. **Capture originals first** into a shell variable, and echo it to confirm it
   is non-empty before you overwrite the row.
2. **Delete precisely.** SQL `AND`/`OR` precedence is a live hazard —
   `WHERE a AND b OR c` parses as `(a AND b) OR c` and deletes far more than you
   meant. Parenthesise, or delete by explicit `id IN (…)` captured from a prior
   `SELECT`.
3. **Verify the restore** — never assume it. Shell quoting of values containing
   backticks, `$`, or quotes fails silently and can blank the row you were
   protecting.
4. **Sweep for residue** in every table the feature writes:

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

Chat actions in particular fan out into **four** tables: `chat`, `chat_abuse`,
`user_pm_abuse`, `user_pm`, plus `admin_audit_log`.

5. **Delete the harness and any scratch HTML**, then confirm:

```bash
ls app/Admin/_t* 2>/dev/null || echo "clean"
git status --short
```

Never commit a harness. If you could not restore something, say so plainly in
your summary rather than leaving it quietly wrong.

## 6. Then commit

`git status --short` should show only real source files. If a file you don't
recognise appears, find out why before committing — and check you're in the tree
you think you are (`git rev-parse --abbrev-ref HEAD`).
