Skip to content

Show "created by / when" and "last changed by / when" inline on record pages #494

Description

@mforce

Ask

Admins/managers should be able to see, directly on a record's own page (Flocks, Daily Entries, Sales Orders, Expenses, Egg Grades, ...) — not by navigating away — who created it and when, and who last changed it and when.

Companion to #493, which covers navigating to a record's full history in the audit log. This issue is the inline summary on the record's own page; keep them separate (#493's "View history" link is a natural affordance to add once this summary exists, but each ships independently).

Approach (confirmed): derive from the existing audit log, no schema change

Considered and rejected: adding real CreatedBy/CreatedAt/UpdatedBy/UpdatedAt columns to every aggregate. That would touch nearly every aggregate + a migration + every write handler, for data the audit log (epic #93) already captures.

Instead: AuditEvent (src/Cluckwork.Domain/Auditing/AuditEvent.cs) is append-only and written inside the same transaction as every change. For a given EntityId, its earliest event is intended to be the creation event, and its latest event is the last change.

Gap found during scoping, folded into this issue's work (still zero schema change): none of the 5 in-scope entity types currently write an AuditEvent on creation — checked CreateFlockHandler, RecordDailyEntryHandler, CreateSalesOrderHandler, CreateExpenseHandler, CreateEggGradeHandler directly, zero IAuditWriter calls in any of them, and AuditActions.cs (src/Cluckwork.Application/Common/AuditActions.cs:20-52) has no *.Create action for any of the 5 (only UserCreate/ProductCreate exist, neither in scope). Without this, "earliest event = created" silently resolves to the first correction instead, or to nothing at all for a never-touched record — the majority case. This issue's scope now includes instrumenting all 5 create handlers with audit.WriteAsync(...), same pattern as their existing Update/Adjust calls, plus the 5 new vocabulary entries and their AuditVocabularyCoverageTests coverage. Still no schema change — AuditEvent/IAuditWriter/AuditActions all already exist; this is new call sites, not new tables. A record created before this ships has no synthetic backfill — it falls back to its earliest correction event, or shows nothing if it's never been touched, which is acceptable (matches how the rest of the audit log already behaves for pre-#93 history).

RBAC: inherits the record's own read access, not a blanket admin gate

A Worker can already read their own Daily Entries (that's core to their job — recording and reviewing them); if they can already see the entry, they should be able to see who created/last changed it too (e.g. "corrected by a manager on ..."), with nothing new exposed beyond what that screen already shows. Sales Orders / Expenses stay effectively admin-only because those screens already are — provenance visibility should never be a stricter or looser gate than the underlying record's own read access, so it needs no separate authorization decision of its own. (Where #388's open question about flock-scoped read gaps applies to an entity type, this issue inherits that gap/fix rather than deciding it here.)

Actor display: shown as the plain actorEmail, same as the existing admin Audit page, for any viewer who can read the record — including a Worker seeing a manager's email on their own Daily Entry. A separate name field was considered (join ActorUserId → User.Name, or snapshot a name onto AuditEvent the way ActorEmail already is) and explicitly rejected for now: a live join was ruled out outright, since it would let a later rename/email-change (#357) or disable (#356) retroactively rewrite what old audit entries display, which defeats the point of an audit trail. Snapshotting a name too was the reasonable safe version of that idea, but decided against for this issue — email-only is enough to start.

Approach (revised): extend the existing list endpoints directly — no new endpoint

Originally scoped as a new batched GET /api/v1/audit/summary endpoint, to avoid leaking admin-only data through worker-readable list endpoints. That risk is now moot: since visibility inherits the record's own read access (above), there's nothing left to protect by keeping this behind a separate call. A second endpoint is strictly worse once that's true — it costs an extra HTTP round-trip and forces the SPA to correlate two responses by id, for no remaining benefit.

Instead: extend each in-scope list endpoint's own query and response DTO directly (e.g. GET /api/v1/daily-entries, GET /api/v1/flocks, ...) to include createdByEmail, createdAtUtc, lastChangedByEmail, lastChangedAtUtc per row, joined/subqueried against AuditEvent within that endpoint's own existing query — bounded by whatever pagination that endpoint already has. To avoid five copies of the same join logic, factor it into one small reusable helper (e.g. a query extension or a lookup service taking a batch of entity ids) that each of the five handlers calls, rather than reimplementing per handler.

Note on where the join goes: all 5 build their response DTO inline in the endpoint file (ToResponse/list-handler method) — there is no separate Application-layer read-handler class for any of them, so the helper is called from the endpoint file itself, fed the page's already-fetched entity ids.

Note on pagination: Flocks/DailyEntries/Sales/Expenses all have limit/offset (default 100, max 500, each endpoint re-declaring its own constants). Egg Grades has no pagination at all — it returns its whole active/all set. The batch lookup for Egg Grades keys off however many rows that returns, not a bounded page.

Scope

  1. API — instrument the 5 create handlers with audit.WriteAsync(AuditActions.<X>Create, ...), matching their existing correction-path calls; add the 5 vocabulary entries + coverage.
  2. API — extend existing list endpoints. For each in-scope entity type's list endpoint, add the four provenance fields to its response DTO, sourced via the shared earliest/latest-AuditEvent-per-id helper, scoped to the ids already being returned on that page.
  3. SPA — inline display. On the list/table pages for entities already in the audit vocabulary (Flocks, Daily Entries, Sales Orders, Expenses, Egg Grades), render "Created by X on Y" / "Last changed by Z on W" directly from the now-extended row data (omit the second when it equals the first — never modified since creation, or when creation predates this feature and there's no event to show).
  4. i18n + Help/glossary sync per AGENTS.md's user-visible-change rule.

Explicitly out of scope

  • Any schema/migration change to the aggregates themselves, or to AuditEvent.
  • The "View history" link to the full per-entity audit feed — that's Audit: entity-scoped "View history" — who created/changed a specific record, and when #493.
  • A separate name field for the actor (live-joined or snapshotted) — decided against above; email only.
  • A new /api/v1/audit/summary-style endpoint — superseded by extending the existing list endpoints directly.
  • Backfilling creation provenance for records that predate this change.

Acceptance

  • Each of the 5 create handlers writes a Create AuditEvent, covered by AuditVocabularyCoverageTests.
  • Each in-scope record's own list page shows who created it, when, and (if changed since) who last changed it and when — no navigation required, no second API call — for every viewer who can already read that record.
  • Sourced entirely from the existing AuditEvent log via a shared lookup helper reused across the extended endpoints — no new domain fields, no new endpoint.
  • Each extended endpoint's provenance fields are visible to exactly whoever could already read that endpoint's rows — never broader, never narrower.
  • i18n + Help/glossary updated per AGENTS.md.

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions