---
name: admin-audit-logging
description: Record Admin panel activity in the audit trail — auditLog for mutations, auditView for reads, logChange for before/after edits, plus target types that link, dedupe behaviour, and naming. Use when adding an endpoint or when someone asks "why isn't X showing in the audit log".
---

# Audit logging in the Admin panel

Everything an admin does — and most of what they *look at* — belongs in
`gcc_admin.admin_audit_log`. Reference: `docs/AUDIT-LOGGING.md`.

## Pick the right helper

| Helper | For | Detail written |
|---|---|---|
| `auditLog(action, targetType, targetId, targetLabel, detail)` | A mutation | your struct, as JSON |
| `auditView(action, targetType, targetId, targetLabel)` | Opening or reading a screen | `{"view": true}` |
| `logChange(action, uid, nic, entity, entityId, fields, reason)` | A field edit with before → after | `{entity, fields, reason}` |

## Mutations

Log **after** the write succeeds, so a failed action leaves no misleading trace.

```cfc
auditLog("fed.update", "fed", fid, toString(f.name),
         { "changes": changes, "reason": reason });
```

Log the **effective outcome**, not the request. A UC chat action that can't be
carried out here converts to a pending report and records
`"converted": "UC report"` — the log should never claim something happened that
didn't.

Include the `reason` whenever the UI collected one, and `left(…, 200)` any free
text so a single entry can't bloat the table.

## Reads

```cfc
auditView("economy.topView", "system", res, "Richest by #res#");
```

Placement: **after** the data is fetched, **before** `apiOut`. That way a
`403`/`404` path doesn't record a view that never happened. (`Audit.search` logs
after its own fetch specifically so the entry can't appear in the page it just
returned.)

`auditView` **dedupes** an identical `account_id` + `action` + `target_id`
within 15 seconds, so refreshes don't spam the log — and it never throws, so a
logging failure can't break a read.

### Make the target say what was looked at

The dedupe key is the target id, so choose it to control granularity:

| Want | Use |
|---|---|
| Each filter recorded separately | Put the filter in `targetId` — `economy.topView` uses the resource; `anticheat.turnTop` uses `7d` / `5d` |
| Refreshes collapsed into one | A constant — `dashboard.view` uses `overview` |
| A link back to the record | A real id + the matching `targetType` |

## Target types that become links

The Audit screen renders these as links, so pick deliberately:

| `targetType` | `targetId` | Renders as |
|---|---|---|
| `player` | numeric uid | link to `#/players/<id>` |
| `fed` | numeric fed id | link to `#/feds/<id>` |
| `chat`, `ip`, `email`, `admin`, `colony`, `system` | id or slug | plain text |

If you add a linkable type, extend the `Target` column renderer in
`assets/views/audit.js` too — the backend type alone does nothing.

`targetLabel` is what a human reads. Leave it empty for a `player` target and
the helper fills in the nic automatically, so rows read `player.note cagito`
rather than a bare id.

## Field edits

```cfc
logChange("player.fleetEdit", uid, nic, "Fleet", fleetId,
          [ { "f": "Land", "from": "2,000", "to": "2,001" } ], reason);
```

Feeds both the Audit screen's "Change" column and the per-player change logs. A
blank reason is stored as **"Quick Edit"** so every row reads
*"\<scope\> edited: \<reason\>"*.

## Naming

- `module.thing` for mutations: `fed.update`, `chat.remove`, `acl.set`.
- `module.thingView` for reads: `admins.listView`, `player.fleetView`.
- Tiered reads pair up: `events.watch` / `events.basicWatch`.
- Player actions are named dynamically as `"player." & actionName`, so a new
  `UserActions` method is logged automatically — the name comes from the method.

## The audit log as a data source

`chat.remove` stores the **original** post text in its detail JSON, and that
entry is the authoritative `chat.id → original text` link (`chat_abuse` has no
source-id column). `Chat.cfc`'s `removalOf()` reads it back.

If a feature needs to remember what something *used to be*, an audit entry is
often the right home. But treat the table as **append-only**: nothing in the
panel updates or deletes `admin_audit_log` rows, and nothing should start.

## Adding view logging to an existing screen

The common request is *"I want to see who looked at this"*. It is one line in
the endpoint — but check:

1. It's placed after the fetch, before `apiOut`.
2. The `targetId` gives the granularity you want (filter vs constant).
3. The label is human-readable.
4. If the target is linkable, the audit view renders it as a link.

Then verify: call the endpoint through the harness and read the row back.

```bash
curl -s "$B?module=economy&method=top&resource=ore" >/dev/null
curl -s "$B?mode=sql&ds=gcc_admin&q=SELECT%20action,target_id,target_label%20FROM%20admin_audit_log%20WHERE%20username='harness'"
```

Delete the harness rows afterwards (see `admin-verify`).

## Note on volume

View logging flows into the same table the Overview's "Recent Admin Activity"
card reads, so that widget will show view traffic too. That is intended — but if
it needs to show mutations only, filter `*.view`-style actions in the widget
query, not by dropping the logging.
