Skip to content

Logs & audit

Transcripts and the audit trail. The daemon archives Margonem session logs under nerthus.logs/, parses transcript text into structured lines, and keeps three rolling operational streams (operational, request, audit) plus two derived audit views — the transaction ledger and the notification log. This page covers fetching, parsing, refreshing a session's logs, reading a stream, and reading the audit views.

Reads need log.read; the transaction ledger needs currency.read; fetching and refreshing need log.fetch and are Write routes (they write the committed archive, so they pass the schema/write gate). The examples stay offline: the fixture session 2026-07-01, Eraster rozmawia z Tussalem, Anward carries no @Logi, so nothing reaches the network. Every example runs against a live daemon — see how the reference is tested.

Routes

Method Path Cmdlet Cap Write
POST /logs/fetch Invoke-NerthusSessionLogFetch log.fetch
POST /logs/parse Get-NerthusSessionLogParsed log.read
POST /sessions/{header}/logs/refresh Get-NerthusSessionLog -Refresh / -FetchMissing log.fetch
GET /logs/{stream} Get-NerthusLog (operational/request/audit) log.read
GET /audit/changes Get-NerthusChangeLog log.read
GET /audit/ledger Get-NerthusTransactionLedger currency.read
GET /audit/notifications Get-NerthusNotificationLog log.read

Cap is the required capability ( = public, no token); Write () marks routes that pass the write gate; a in Cmdlet is reached directly, no wrapper. Paths are relative to /v1/api; the cross-cutting contract — middleware, envelopes, status codes — is on the API reference index.

The log-fetch routes carry the Write flag because every fetch writes the committed nerthus.logs/ transcript archive (Logs).

Fetch logs into the archive

POST /v1/api/logs/fetch pulls transcripts into nerthus.logs/. A target is chosen from the body (request, url, or a session header); with none given it mass-fetches every session's @Logi. The summary counts each URL by outcome. No session carries @Logi here, so the mass run has nothing to pull and stays offline. This is a Write route.

POST /v1/api/logs/fetch
Content-Type: application/json
Authorization: Bearer <token>

{ "retryFailed": false }
await fetch("https://evocation.nerthus.pl/v1/api/logs/fetch", {
  method: "POST",
  headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
  body: JSON.stringify({ retryFailed: false }),
}).then((r) => r.json());

Response 200:

{ "Total": 0, "Fetched": 0, "Unchanged": 0, "Archived": 0, "Failed": 0, "Skipped": 0, "FailedUrls": [] }

Parse transcript text

POST /v1/api/logs/parse parses inline content (or fetches a url) into a structured transcript: the detected Format, a LineCount, resolved Lines, and any LocationSegments. Passing content keeps it offline and deterministic.

POST /v1/api/logs/parse
Content-Type: application/json
Authorization: Bearer <token>

{ "content": "[19:04] Eraster: Pozdrawiam Lorda Tussala." }
await fetch("https://evocation.nerthus.pl/v1/api/logs/parse", {
  method: "POST",
  headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
  body: JSON.stringify({ content: "[19:04] Eraster: Pozdrawiam Lorda Tussala." }),
}).then((r) => r.json());

Response 200 (trimmed):

{ "Format": "Plain", "LineCount": 1, "Lines": [], "LocationSegments": [] }

Rejected: nothing to parse

With neither content nor url, parse is 400.

POST /v1/api/logs/parse
Content-Type: application/json
Authorization: Bearer <token>

{}
await fetch("https://evocation.nerthus.pl/v1/api/logs/parse", {
  method: "POST",
  headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
  body: JSON.stringify({}),
});

Response 400:

{ "error": "content or url required" }

Refresh a session's logs

POST /v1/api/sessions/{header}/logs/refresh re-pulls and re-parses the session's @Logi, returning the header and each log's fetch state. { "mode": "missing" } fetches only the uncached logs; otherwise it forces a full re-pull. The fixture session has no @Logi, so Logs comes back empty and nothing hits the network. This is a Write route.

POST /v1/api/sessions/2026-07-01,%20Eraster%20rozmawia%20z%20Tussalem,%20Anward/logs/refresh
Content-Type: application/json
Authorization: Bearer <token>

{}
const header = "2026-07-01, Eraster rozmawia z Tussalem, Anward";
await fetch(`https://evocation.nerthus.pl/v1/api/sessions/${encodeURIComponent(header)}/logs/refresh`, {
  method: "POST",
  headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
  body: JSON.stringify({}),
}).then((r) => r.json());

Response 200:

{ "Header": "2026-07-01, Eraster rozmawia z Tussalem, Anward", "Logs": [] }

Read a log stream

GET /v1/api/logs/{stream} returns the last 200 lines of a rolling stream as the { stream, count, items } envelope. The stream is one of operational, request, or audit. Here, the operational stream.

GET /v1/api/logs/operational
Authorization: Bearer <token>
await fetch("https://evocation.nerthus.pl/v1/api/logs/operational", {
  headers: { "Authorization": `Bearer ${token}` },
}).then((r) => r.json());

Response 200 (trimmed):

{ "stream": "operational", "count": 0, "items": [] }

The request stream logs each HTTP request:

GET /v1/api/logs/request
Authorization: Bearer <token>
await fetch("https://evocation.nerthus.pl/v1/api/logs/request", {
  headers: { "Authorization": `Bearer ${token}` },
}).then((r) => r.json());

The audit stream logs each mutation:

GET /v1/api/logs/audit
Authorization: Bearer <token>
await fetch("https://evocation.nerthus.pl/v1/api/logs/audit", {
  headers: { "Authorization": `Bearer ${token}` },
}).then((r) => r.json());

Rejected: unknown stream

A stream name outside the three is 404.

GET /v1/api/logs/nieznany
Authorization: Bearer <token>
await fetch("https://evocation.nerthus.pl/v1/api/logs/nieznany", {
  headers: { "Authorization": `Bearer ${token}` },
});

Response 404:

{ "error": "Unknown stream" }

The change audit

GET /v1/api/audit/changes is the audit stream under an audit-facing path — the { stream, count, items } envelope of recorded mutations.

GET /v1/api/audit/changes
Authorization: Bearer <token>
await fetch("https://evocation.nerthus.pl/v1/api/audit/changes", {
  headers: { "Authorization": `Bearer ${token}` },
}).then((r) => r.json());

Response 200 (trimmed):

{ "stream": "audit", "count": 0, "items": [] }

The transaction ledger

GET /v1/api/audit/ledger returns the dated ledger of applied @Transfer moves, newest first, as { count, items } — needs currency.read. No transfers have been applied, so it is empty.

GET /v1/api/audit/ledger
Authorization: Bearer <token>
await fetch("https://evocation.nerthus.pl/v1/api/audit/ledger", {
  headers: { "Authorization": `Bearer ${token}` },
}).then((r) => r.json());

Response 200:

{ "count": 0, "items": [] }

The notification log

GET /v1/api/audit/notifications reconstructs @Intel notification intent from the sessions as { count, items }; filter with ?operation=. The fixture session records no @Intel, so it is empty.

GET /v1/api/audit/notifications
Authorization: Bearer <token>
await fetch("https://evocation.nerthus.pl/v1/api/audit/notifications", {
  headers: { "Authorization": `Bearer ${token}` },
}).then((r) => r.json());

Response 200:

{ "count": 0, "items": [] }