---
name: audit-activity-log
slug: audit-activity-log
status: active
updated: 2026-06-29
plans:
  - https://moonui.elbaset.com/system-audit-log-report.html
  - https://moonui.elbaset.com/activity-log-feature-plan.html
related:
  - lis-discount
---

# Audit / Activity Log (سجل التحركات)

## Context — "who changed what?"
Owner couldn't tell who modified records. Investigation (2026-06-29): the system DOES record everything — it just had no screen, and the one API endpoint excluded the lab.

## How it works
- **Engine:** `spatie/laravel-activitylog`. `App\Models\BaseModel` uses `App\Traits\Auditable` → `LogsActivity` with `logAll()->logOnlyDirty()->dontSubmitEmptyLogs()`. So **every model extending BaseModel auto-logs create/update/delete** to `activity_log` with causer (auth user) + before/after `properties` ({attributes:new, old}). elmadina had ~16k entries, ~74% with a causer (the 26% NULL = onboarding imports/seeders/console — not user edits).
- **Endpoint:** `GET /api/core/activity-log` (`ActivityLogController`, perm `core.activity-log.view`) — filters: `subject_type`, `subject_id`, `causer_id`, `event`, `from`, `to`; paginated; `/export` → CSV (perm `.export`). Tenant-scoped via `whereHasMorph($subjects, company_id)` + Role OR-branch (Role is global).
- **Other audit surfaces (already had UIs):** LIS result audit (`lab_result_audit_logs`, Compliance screen), patient-portal access logs (`lab_patient_portal_access_logs`, worklist badges), accounting period-lock/compliance. Plus `created_by`/`updated_by` columns (last editor only) on 129 models.

## Decisions & why
- **🔴 The endpoint whitelist was accounting-only** (`SUBJECT_TYPES` + `whereHasMorph`) → LIS edits (prices, tests, doctors…) were logged but NEVER returned. **Fix (BE `4f3e0c6e1`):** extended `SUBJECT_TYPES` with 16 LIS models + 5 core (product/partner/setting/unit/branch) — all company-scoped so tenancy holds. Verified: `subject_type=lab_price_list` → 30 entries (was empty).
- **🔇 Machine heartbeat muted:** the analyzer middleware pinged `LabMachine.last_communication_at` (+ connection_status/last_result_at/total_results_today) every minute → **~57% of all activity (9,134/16,139)**, burying real edits. `LabMachine::getActivitylogOptions()` overrides BaseModel to `logExcept([those fields])`; with logOnlyDirty+dontSubmitEmptyLogs a heartbeat-only update → empty (skipped) log. Real config changes still log.
- **New FE screen** `/core/activity-log` (Core→System nav, perm-gated) — investigative filter-first table + **row-expansion git-style before→after diff** (signature). Design synthesized from 3 frontend-architect agents (timeline / table / hybrid → table won for targeted lookup). Deliberately NO summary strip (no cheap aggregate endpoint → would fake numbers). Files: `features/core/activity-log/` (component + `activity-log-entities.ts` registry: subject_type⇄friendly label+icon), `core/services/activity-log.service.ts`, `core/models/activity-log.model.ts`. FE commit `0f936f3`.

## Gotchas
- `shortType()` returns the **lowercased class basename** (`labpricelist`), while the filter `subject_type` param uses the SUBJECT_TYPES **key** (`lab_price_list`) = basename with underscores. The FE entity registry keys on `key.replace(/_/g,'')` to match the returned value; unmapped → humanize (never show a raw FQCN).
- A model added to SUBJECT_TYPES MUST have `company_id` or the `whereHasMorph` company closure breaks/leaks. All current ones do (BaseModel + TenantAware).
- The 26% NULL-causer rows are system/import actions — not "ghost" user edits.

## ✅ Enhancement (2026-06-30) — readable detail + noise cleanup
Owner: "I want to know what the user did exactly — what it was and what it became — and the middleware log is huge in the void." Hard numbers from the LIVE lab `elmadina_db`: 23,897 entries, **15,637 (65%) machine heartbeats, +7,719/day STILL growing** (the mute is on moonui but NOT yet released to elmadina). moonui dev had 87,415 (mute already stopped them). Plan: `public_html/activity-log-detail-and-noise-plan.html`.

**Built (on `hazemdev`, deployed moonui `/app`, ⏳ owner release):**
- **Record NAME, not #id** — NEW `Modules/Core/app/Support/ActivityLogEnricher.php`. `subjectLabel()` resolves the affected record's name from the eager-loaded `subject` morph (controller `with(['causer','subject'])`), falling back to the log's `properties.attributes/old` snapshot for deleted records. Verified live: investigation #22 → "Red Blood Cell Count".
- **FK values → names** — `valueLabels()` maps FK fields in the diff (section/specimen/category/price-list/external-lab/doctor/patient/partner/cost-center/account/role/branch/user) old+new ids → names via a **batched** `buildLookup()` (no N+1, resolved once per page). Emitted as `value_labels: {field:{old,new}}`. 🔑 Ambiguous ids (`unit_id` Core-vs-Lab, `parent_id` category-vs-account) are deliberately UNMAPPED — name-only resolution would risk showing the WRONG record.
- **Arabic field labels + plain summary (FE)** — `FIELD_LABELS_AR/EN` dict (السعر/القسم/نوع العينة…) with humanize fallback; `summary()` one-liner ("أحمد عدّل التحليل «CBC»"); record name shown in the entity cell; resolved values in the diff (`showVal`).
- **Hide system noise** — new `actor` filter (`user`=whereNotNull causer_id / `system`=whereNull) + FE Users/System select-button. CSV export gained a `subject_label` column.
- **Heartbeat cleanup** — NEW command `lis:prune-machine-heartbeats` (`Modules/LIS/app/Console/`, dry-run default, `--force`, chunked). Deletes ONLY `LabMachine`+`updated` rows whose changed keys ⊆ {last_communication_at, connection_status, last_result_at, total_results_today, updated_at, created_at} — a real config change is kept. **Ran on moonui (dev) after table backup: 130,638→43,247 (−87,391, kept 24 real changes).** 🔴 elmadina runs it via owner after backup (DB not binlogged; test on `selmadina_stg` first).
- Tests: +2 Pest (subject_label + actor filter), full ActivityLog suite 18 green. Commits: **BE `5e8ab10a5` / FE `aa29b1d`** (hazemdev, pushed). Native code-reviewer ran (0 critical; HIGH `config.json` NOT committed; MEDIUM applied: actor+causer_id 422 guard, created_by/updated_by aligned as noise both sides, withTrashed FK resolution, deleted-record name limitation documented).

## Open / next
- ⏳ Owner: release to elmadina (priority 0 — stops the +7.7k/day heartbeats + makes LIS edits visible there) → then run the prune on elmadina after backup (rehearse on staging).
- Optional later: a cheap `/activity-log/summary` aggregate endpoint to enable the summary strip; "group by user/entity" view; deep value search; context-aware FK resolution for `unit_id`/`parent_id` (by subject_type).

## Links
- BE `Modules/Core/app/Http/Controllers/ActivityLogController.php` · `Modules/LIS/app/Models/LabMachine.php`
- FE `features/core/activity-log/*` · `core/services/activity-log.service.ts`
- Reports: `system-audit-log-report.html` (status) · `activity-log-feature-plan.html` (build plan, 3-agent design)
