Activity log
See also: Architecture overview · Sections · section_record · db
The user-activity audit trail: one structured row per state-changing action,
appended to matrix_activity. It is what answers "who changed what, and when".
src/core/api/handlers/activity_log.ts is the whole subsystem — one function, one
sink. There is no logging facade, no severity scale and no pluggable-backend
registry: diagnostic output is plain console logging, and the audit trail is a
matrix write.
Role
An activity record is an ordinary matrix record, not a parallel storage path.
logActivity(entry) builds the typed-column payload and inserts it into
matrix_activity (section tipo dd542), through the same
db layer as any other write.
Responsibilities
- Audit write — append one row to
matrix_activityfor a state-changing API action, an authentication event, or a media/time-machine operation. - WHAT mapping — map the action name to its
dd42event code. An unmapped action is skipped, never guessed with a wrong code. - Self-log guard — refuse to audit an action performed on the activity section itself (see the self-log guard).
- Never break the action — the write is wrapped in
try/catchand swallows its own errors toconsole.error. An audit write must never fail the user's save.
A silent skip is a real outcome
logActivity never throws and returns without writing for three
reasons: an unmapped what, an empty tipo, or a self-log. If you add an
emitter and no row appears, check those three before suspecting the
database — nothing is logged when a write is skipped.
The audit model
Each action is recorded as six typed columns:
| field | component tipo | matrix column | source |
|---|---|---|---|
| IP / host | dd544 |
string |
the resolved client host (::1 / 127.0.0.1 → localhost) |
| WHO | dd543 |
relation |
a locator into the Users section (dd128) for the acting user |
| WHAT | dd545 |
relation |
a locator into dd42 — the event code |
| WHERE | dd546 |
string |
the tipo being acted on |
| WHEN | dd547 |
date |
the virtual-calendar instant |
| DATA | dd551 |
misc |
the action payload ([{lang:'lg-nolan', value: …}]) |
The event codes
The WHAT column stores a locator into dd42, whose section_id is the event
code. WHAT_CODES maps the full set:
| code | event | code | event |
|---|---|---|---|
| 1 | LOG IN |
9 | UPLOAD |
| 2 | LOG OUT |
10 | DOWNLOAD |
| 3 | NEW |
11 | UPLOAD COMPLETE |
| 4 | DELETE |
12 | DELETE FILE |
| 5 | SAVE |
13 | RECOVER SECTION |
| 6 | LOAD EDIT |
14 | RECOVER COMPONENT |
| 7 | LOAD LIST |
15 | STATS |
| 8 | SEARCH |
16 | NEW VERSION |
Four of them — SEARCH (8), UPLOAD (9), DOWNLOAD (10) and STATS (15) —
have no emitter: nothing in the engine writes a row with those codes. They
stay in the map because matrix_activity holds older records that carry them,
and the read side must still resolve a code it finds in the data. Treat them as
readable-but-not-written rather than as free code points; if you add an emitter
for one, it needs its own call site, not a new map entry.
LOAD EDIT and LOAD LIST are distinct events
They are codes 6 and 7, not one LOAD. The action name passed to
logActivity is matched verbatim against WHAT_CODES — an action name that
is not an exact key is skipped silently.
The section_id comes from the sequence, not the counter
The insert omits section_id and lets the
matrix_activity_section_id_seq Postgres sequence allocate it. The dd542
matrix counter is deliberately not the allocator here — do not "fix" this by
routing the insert through the counter.
Where it fires
The audit is written from the API dispatch path, inline within the request. There is no deferred queue and no shutdown flush.
| action | emitted from |
|---|---|
LOG IN |
src/core/security/auth.ts — login(), on both outcomes |
LOG OUT |
src/core/api/handlers/dd_utils_api.ts — the quit action, before the session is destroyed |
NEW |
src/core/api/handlers/dd_core_api.ts — the create action |
SAVE |
src/core/api/handlers/dd_core_api.ts — after a component save |
DELETE |
src/core/api/handlers/dd_core_api.ts — after a record delete, per deleted record |
LOAD EDIT / LOAD LIST |
src/core/api/handlers/dd_core_api.ts — logReadActivity, on a section or area read |
UPLOAD COMPLETE |
tools/tool_upload/server/index.ts — after the uploaded file is processed and persisted |
NEW VERSION |
tools/tool_media_versions/server/media_versions.ts — build_version and conform_headers |
DELETE FILE |
tools/tool_media_versions/server/media_versions.ts (delete_quality, delete_version) and the posterframe deletes in dd_component_av_api.ts / dd_component_3d_api.ts |
RECOVER SECTION / RECOVER COMPONENT |
tools/tool_time_machine/server/ — apply_value, and one row per reverted component in bulk_revert.ts |
Emitters live at the door — the API handler or tool action — not inside the
engine that does the work. A door knows both the acting user and the client
host; createSectionRecord, deletePosterframe and their peers know neither,
and several of them are reached from more than one door.
Diffusion keeps a separate trail: src/core/diffusion_bridge/diffusion_delete.ts
logs unpublish outcomes to the diffusion activity log (dd1758). It is not part of
the core audit trail.
The self-log guard
An audit row is itself a record, so logging an action performed on the
activity section would append a row describing the appending of a row.
logActivity refuses when the WHERE tipo is one of dd542 (the Activity
section) or its own components dd543, dd544, dd545, dd546, dd547,
dd550, dd551. It also refuses an empty tipo, which would produce a row
naming nothing.
The guard lives in logActivity, not at the call sites, so every emitter —
including any added later — inherits it.
The surface
src/core/api/handlers/activity_log.ts:
| symbol | purpose |
|---|---|
logActivity(entry, now?) |
Append one matrix_activity row. Returns early — no write — when entry.what has no mapped dd42 code, when the tipo is empty, or when the tipo is the activity section's own. Never throws. |
ActivityEntry |
The input shape: { what, tipo, userId, host, data }. |
hostFromClientIp(clientIp) |
Resolve the dd544 host value: loopback becomes localhost, a missing address becomes unknown. Use it rather than re-deriving the rule. |
ANONYMOUS_USER_ID |
The WHO used when no one is authenticated (-666). |
ActivityEntry fields:
| field | purpose |
|---|---|
what |
The action name, mapped to a dd42 code. |
tipo |
The WHERE tipo — the component tipo for a save, the section tipo for a delete or a create. |
userId |
The acting user (the WHO locator into dd128). |
host |
The resolved client host string — from hostFromClientIp. |
data |
The dd551 payload: a message plus action-specific fields. |
Auditing an unauthenticated action
Most actions run behind the auth gate, so the actor is principal.userId. A
denied login has no principal — and is exactly the event worth recording.
Those rows carry ANONYMOUS_USER_ID as the WHO and name the attempted account
in the payload instead:
await logActivity({
what: 'LOG IN',
tipo: LOGIN_ACTIVITY_TIPO, // dd229, fixed for login events
userId: ANONYMOUS_USER_ID, // nobody is authenticated yet
host: hostFromClientIp(clientIp),
data: {
msg: `Denied login attempted by: ${username}. ${cause}`,
result: 'deny',
cause, // 'wrong password', 'User does not exist', …
username,
},
});
A successful login writes the same shape with result: 'allow' and the real
userId. Both outcomes land in the same trail, so a burst of deny rows
against one account is visible in the Activity section without any extra
tooling.
What the audit feeds
The activity rows are read back by two consumers:
- the area dashboard —
metricActivity(src/core/area/dashboard.ts) queriesmatrix_activitydirectly with JSONB operators to build the per-day, per-section, per-user activity chart. See area → the dashboard payload. - user statistics —
src/core/area_maintenance/user_stats.tsaggregates the rows into per-user, per-day totals for the maintenance area.
Diagnostics
Diagnostic output is plain console logging. There is no severity scale, no
verbosity threshold and no per-user debug gate.
console.error('activity log write failed (swallowed):', error);
Example
import { hostFromClientIp, logActivity } from './activity_log.ts';
await logActivity({
what: 'SAVE', // → dd42 code 5
tipo: source.tipo, // the WHERE tipo
userId: principal.userId, // the WHO locator into dd128
host: hostFromClientIp(context.clientIp),
data: {
msg: 'Saved component data',
lang: source.lang ?? 'lg-nolan',
tipo: source.tipo,
},
});
Related
- Architecture overview — where the work-system server sits relative to data and diffusion.
- Sections · section_record — the matrix storage the activity write uses.
- db — the
sqlhandle the audit row runs through. - Locator — the pointer type stored in the WHO and WHAT columns.
- area — the dashboard that charts the activity rows.