Skip to content

Audit log: provenance and change history across all entity types #56

Description

@DutchJaFO

Add a reusable audit log that tracks who changed what, when, and via which source — across quotes, characters, sources, and persons — without adding columns to those core tables.

Why a separate table

The core tables (Quotes, Characters, Sources, Persons) must stay simple. Audit data grows without bound and has different access patterns (append-only, queried by entity, never updated). A single polymorphic AuditLog table handles all entity types uniformly and can be extended to new entity types with no schema changes.

Table schema

CREATE TABLE AuditLog (
    Id          TEXT NOT NULL PRIMARY KEY,   -- UUID
    EntityType  TEXT NOT NULL,               -- 'quote' | 'character' | 'source' | 'person'
    EntityId    TEXT NOT NULL,               -- UUID of the affected record
    ActorType   TEXT NOT NULL,               -- 'user' | 'enrichment' | 'import' | 'system'
    ActorId     TEXT,                        -- user UUID, provider name ('tmdb'), import batch UUID, or null for system
    Action      TEXT NOT NULL,               -- see action vocabulary below
    Field       TEXT,                        -- nullable; field name for field-level actions
    OldValue    TEXT,                        -- nullable; previous value serialised as string
    NewValue    TEXT,                        -- nullable; new value serialised as string
    OccurredAt  TEXT NOT NULL                -- ISO 8601 UTC
);

CREATE INDEX IX_AuditLog_Entity ON AuditLog (EntityType, EntityId, OccurredAt DESC);

Action vocabulary

Action Trigger
created Record first written (manual, import, or seed)
updated Field changed by a user
enriched Field filled by an enrichment provider
completed isComplete set to true — signed off (#55)
verified_absent Field added to noValueKnown — confirmed no value exists (#55)
imported Record written via bulk import (#45); ActorId = import batch UUID

Actor model

ActorType ActorId Scenario
user User UUID Manual edit in the management UI
enrichment Provider name (tmdb, wikidata, etc.) Enrichment provider filled a field
import Import batch UUID Bulk import (#45); batch UUID groups all rows from one import run
system null Startup seeding, migrations

The actor model is intentionally forward-compatible: it works before users exist (auth #15) by using ActorType='system' or ActorType='import' as the actor.

Reusable service interface

public interface IAuditLogger
{
    Task LogAsync(AuditEntry entry, CancellationToken ct = default);
    Task<IReadOnlyList<AuditEntry>> GetHistoryAsync(string entityType, string entityId, CancellationToken ct = default);
}

Injected wherever writes happen: write endpoints (#16), import endpoint (#45), enrichment service (#19). All audit writes are fire-and-forget from the caller's perspective — a failed audit write must never roll back the primary operation.

Connections to existing issues

Blazor UI: audit history panel

Each entity detail page (quote, character, source, person) gains a collapsible History tab:

  • Entries sorted by OccurredAt desc
  • Shows: timestamp, actor (user display name or provider name), action, field, old → new value
  • completed and verified_absent entries are visually distinguished (e.g. a checkmark badge) — these are sign-off events, not just edits

API

GET /api/v1/quotes/{id}/history (and equivalents for other entity types) — returns the audit log for a single record. Paginated. No auth required for read; write endpoints require auth as usual.

Notes

  • Audit log is append-only — no UPDATE or DELETE on AuditLog rows
  • OldValue / NewValue are stored as strings; complex fields (e.g. genres array) are serialised as JSON
  • The audit log is not included in the export endpoint (Export endpoint: GET /api/v1/quotes/export #47) — it is instance-specific provenance data, not portable quote content

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions