# CMS Audit Log

Records who changed what in the CMS content manager, so that when a client
reports an issue that turns out to be caused by their own CMS change, there
is evidence of the change, the user who made it, and when it happened.

## What is logged

| Action | Trigger |
| --- | --- |
| `page_updated` | Page saved in the content manager (settings + content), with a field-level diff of page attributes. Preview (temp) saves are excluded |
| `version_saved` | Page saved as a version (versioning enabled) |
| `page_published` | Draft/version committed as the live page |
| `page_deleted` | Page deleted. History/temp housekeeping and the internal live-page swap performed by draft saves and commits are excluded |
| `content_updated` | A content field value changed, with old/new values (truncated at 5,000 characters) |
| `content_deleted` | A content field row deleted |
| `content_structure_saved` | Content lists/structure saved after rows are written (e.g. template setup, add pagedata) |

Versioned saves write to a history/draft copy of the page; entries are filed
under the live page's ID (with the actual saved copy's ID in the details) so
the log stays filterable by the page admins know about. Page copies are
deliberately not logged: the versioning handler copies pages internally on
every version save and preview, so an admin-initiated copy cannot be told
apart.

Each entry stores the acting admin user (ID and name, denormalised so the
record survives user deletion), IP address, timestamp, and a JSON details
payload. `admin_user_name`, `ip_address`, and `details` are encrypted at rest
using the `OptionalEncrypted` cast, and the model is registered in
`App\Src\Encryption::$modelsWithEncryption` so key-rotation tooling covers
the table.

## Viewing the log

Admin > Content > CMS Change Log (`/admin/cms-audit-log`), filterable by
action, user, page ID, and date range.

## Configuration

- `CMS_AUDIT_LOG_ENABLED` (default `true`) - master switch.

## Data handling

Entries contain staff/client-staff personal data (names, IP addresses).
Treat extracts as INTERNAL under the mtc. Information Classification Scheme.
No retention limit is applied by the plugin; agree retention with the client
if required.

## How it works

- `CmsAuditLogServiceProvider` listens to `HooksAdapter` actions fired at the
  end of the `ContentManager` save methods and to the
  `Page/delete:before_delete` event. Registration happens in the provider
  (not a plugin `hooks.php`) because hooks.php files are only loaded by the
  legacy web bootstrap, while providers boot in web, artisan, and phpunit
  alike.
- `PageListItemDataObserver` (registered by the service provider) diffs
  content field values on Eloquent `updated`/`deleted` events. `created`
  events are deliberately ignored because page versioning clones every
  content row on each version save.
- Page-row updates use a query-builder update (no Eloquent events), which is
  why page-level changes are captured at the `ContentManager` layer instead
  of a model observer.
