# SENTINEL — DEVELOPER REFERENCE

Canonical internal reference for the Sentinel WordPress plugin shipped by
Scarlett Media Lab. It maps the codebase, admin UI, and public docs into a
single document so a future developer or AI agent can orient quickly without
spelunking.

| Item | Value |
|------|-------|
| Product | Sentinel – WordPress Activity & Security Logging Plugin |
| Vendor | Scarlett Media Lab |
| Plugin slug / folder | `sentinel` |
| Main bootstrap file | `sentinel.php` |
| Current version | **1.1.3** (see `sentinel.php` header + `SENTINEL_VERSION`) |
| Tested up to | WordPress 6.9 (`readme.txt`) |
| Requires | WP 5.0+, PHP 7.4+ |
| Premium SKU | Sentinel+ (£9/mo or licence) |
| Stack | PHP + jQuery + Chart.js (CDN). No build pipeline. |
| Public docs | <https://getsentinel.dev/docs/> |
| Live admin (production) | <https://getsentinel.dev/wp-admin> |
| Local admin (workspace) | <https://getsentinel.local/wp-admin> — creds in `tools/.env` (`SENTINEL_PLUGIN_*`) |
| Discovery script | `tools/scripts/sentinel-explore.mjs` (Playwright, read-only) |
| Captured admin/docs dumps | `tools/sentinel-explore/` |

> **Scope note.** This file documents the **Sentinel plugin** within the
> Scarlett Media Lab workspace. It is unrelated to the SML marketing site.
> The implementation source of truth is the plugin codebase under
> `GetSentinel/app/public/wp-content/plugins/sentinel/`.
>
> **Live admin caveat.** The `.env` credentials in `tools/.env` are for the
> local Sentinel install (`https://getsentinel.local/wp-admin`,
> `admin/admin`). The same `admin/admin` pair does **not** work on the live
> production admin at `getsentinel.dev/wp-admin`. The live admin would
> require separate credentials; this reference therefore documents the
> live UX based on the **local** install (same plugin version 1.1.3) plus
> the public docs. Anywhere production behaviour cannot be verified from
> code or the local UI, it is marked **Needs manual verification**.

---

## QUICK REFERENCE

| Need | Use | Location |
|------|-----|----------|
| Plugin bootstrap | Plugin header, constants, `plugins_loaded` wiring | `sentinel.php` |
| Constants | `SENTINEL_VERSION`, `SENTINEL_PLUGIN_DIR`, `SENTINEL_PLUGIN_URL`, `SENTINEL_PLUGIN_BASENAME`, `SENTINEL_SITE_URL` | `sentinel.php` (top of file) |
| Log an event | `sentinel_log_event($event_key, $data = [], $user_id = null)` | `sentinel.php` |
| Premium gate | `sentinel_is_premium()` | `sentinel.php` |
| Default event catalogue | `sentinel_get_default_event_definitions()` | `includes/event-definitions.php` |
| Event registry / disable map | `Sentinel_Events` static class + `sentinel_disabled_events` option | `includes/class-sentinel-events.php` |
| Logger / batch queue | `Sentinel_Logger::log()`, `add_to_queue()`, `process_queue()` | `includes/class-sentinel-logger.php` |
| Rate limiting | `Sentinel_Logger::check_rate_limits()` + `apply_rate_limiting_behavior()` | `includes/class-sentinel-logger.php` |
| Email throttling | `sentinel_email_rate_limiter()` + `sentinel_should_send_granular_email_notification()` | `sentinel.php` |
| Database install / upgrades | `Sentinel_Migration::create_tables()`, `set_default_options()` | `includes/class-sentinel-migration.php` |
| Retention / archive / anonymise | `Sentinel_Cleanup` | `includes/class-sentinel-cleanup.php` |
| Memory monitor | `Sentinel_Memory_Monitor` | `includes/class-sentinel-memory-monitor.php` |
| File integrity | `Sentinel_File_Monitor` | `includes/class-sentinel-file-monitor.php` |
| Auth counters (Premium) | `Sentinel_Auth_Counters` | `includes/class-sentinel-auth-counters.php` |
| Incidents (Premium) | `Sentinel_Incidents` | `includes/class-sentinel-incidents.php` |
| Privacy / GDPR | `Sentinel_Privacy` + `[sentinel_data_deletion_request]` shortcode | `includes/class-sentinel-privacy.php` |
| Digest emails | `Sentinel_Digest_Reports` (daily/weekly cron) | `includes/class-sentinel-digest-reports.php` |
| Email templates | `Sentinel_Email_Templates` | `includes/class-sentinel-email-templates.php` |
| REST API | `Sentinel_Rest_API`, namespace `sentinel-plugin/v1` | `includes/class-sentinel-rest-api.php` |
| Integrations (WC, CF7, WPForms, Gravity, Yoast) | `Sentinel_Integration_Registry` | `includes/class-sentinel-integrations.php` |
| Admin menu & widgets | `Sentinel_Admin::init_menu()`, `register_dashboard_widgets()` | `admin/class-sentinel-admin.php` |
| Dashboard view | `admin/views/admin-page.php` (rendered via `Sentinel_Admin::render_dashboard()`) |
| Logs view | `admin/views/logs.php` |
| Events Registry | `admin/views/events-page.php` |
| Incidents view | `admin/views/incidents-page.php` (Premium gated) |
| Settings view | `admin/views/settings-page.php` |
| Cron hook authority list | `sentinel_get_cron_hook_names()` | `sentinel.php` |
| Activation / deactivation | `register_activation_hook`/`register_deactivation_hook` | `sentinel.php` |
| Uninstall (drops tables/options) | `uninstall.php` |
| Admin JS (entry points) | `admin/js/sentinel-admin.js`, `sentinel-dashboard.js`, `sentinel-settings.js`, `sentinel-wp-dashboard.js` |
| Admin CSS | `admin/css/sentinel-admin.css` |
| Frontend privacy assets | `assets/js/sentinel-privacy.js`, `assets/css/sentinel-privacy.css` |
| Charts | Chart.js 3.9.1 from `cdn.jsdelivr.net` (no bundling) |

---

## PRODUCT OVERVIEW

Sentinel is a WordPress activity-logging and operational-awareness plugin.
It records what happens inside a WordPress site — user actions, content
changes, plugin and theme updates, PHP errors, HTTP errors, file changes,
memory usage, security events — and presents it in a dedicated admin area
plus optional email digests.

**Who it is for.** Site administrators, freelance developers, and small
agencies who maintain WordPress sites and want an internal audit trail and
early-warning system without sending data to a third-party SaaS. The free
tier covers logging, dashboards, retention, exports, GDPR features and
basic file monitoring. The Premium tier (Sentinel+) adds incident
detection, an Auth Counters subsystem, extended file monitoring,
memory-leak detection, and custom event creation.

**What it solves (verified from code and admin UI).**

- Site activity audit — login/logout, post/page/comment changes, profile
  edits, option changes, plugin & theme updates, core upgrades.
- Error capture — PHP fatals/warnings/notices/deprecated, WP database
  errors, WP HTTP request errors, 404/500s, cron failures, file upload
  failures, plugin activation errors.
- Security awareness — failed logins, bad auth cookies, nonce failures,
  permission denials, optional file modification alerts. Premium layers
  brute-force / enumeration / XML-RPC flood detection plus an
  observe / throttle / block "action engine".
- Privacy — IP and user-data anonymisation after a configurable window,
  Right to be Forgotten shortcode/form, daily anonymisation cron.
- Notifications — per-category and per-priority real-time emails plus
  daily and weekly digests, with v1.1.2 transient-based rate limiting.
- Operational read-outs — dashboard widgets for memory insights and
  open incidents.

**How Scarlett Media Lab positions it** (per the public docs):
"Sentinel" with the tagline _"Knowledge is the foundation of vigilance."_
The free download is offered via `getsentinel.dev`, and Premium licences
are sold for £9/month with a hosted licence-validation endpoint at
`getsentinel.dev/wp-json/sentinel/v1/validate-license`.

**Where docs, admin and code disagree** is captured in §
[PUBLIC DOCUMENTATION ALIGNMENT](#public-documentation-alignment).

---

## DESIGN PRINCIPLES

These are principles actually expressed by the code, not aspirational
copy.

1. **Log to the database, but never at the cost of the request.** The
   logger has a write-batch path, recursive-call protection, rate
   limiting with graceful degradation, a memory threshold short-circuit,
   and falls back to a transient-stored "fallback log" if `$wpdb`
   writes fail.
2. **Events are first-class.** Every event is declared in a single
   registry (`event-definitions.php`) with `category`, `priority`,
   `auto_capture`, `description`. Admins can disable any event via
   `sentinel_disabled_events`.
3. **Critical never gets dropped.** Role exclusions, rate limiting and
   email cooldowns all have escape hatches for `critical` priority
   events.
4. **Privacy is configurable, not retrofitted.** IP logging, data
   anonymisation, Right-to-be-Forgotten, log access auditing, and
   role-based log filtering are all in the settings UI and back-end.
5. **Premium is a feature gate, not a fork.** All Premium classes load
   regardless of licence; their behaviours guard internally with
   `sentinel_is_premium()`. The codebase remains one build artefact.
6. **Use WordPress-native admin patterns.** `add_menu_page` /
   `add_submenu_page`, nonces, `current_user_can()`, dashboard widgets,
   `dbDelta`, cron events, transients. No custom admin framework.
7. **Fail open, not silent.** The v1.1.2 email rate limiter is
   deliberately fail-open if transients break, so admins still see
   alerts when WordPress's caching layer misbehaves
   (`docs/Version 1.1.2 Changelog`, "Fail-Open Design").
8. **Clean uninstall.** `uninstall.php` drops every table, deletes every
   `sentinel_*` option/transient by name, and clears every Sentinel cron
   hook.

---

## PROJECT CONVENTIONS

| Kind | Pattern | Example |
|------|---------|---------|
| PHP class prefix | `Sentinel_` | `Sentinel_Logger`, `Sentinel_Incidents` |
| PHP function prefix | `sentinel_` (snake_case) | `sentinel_log_event`, `sentinel_is_premium`, `sentinel_get_cron_hook_names` |
| Option prefix | `sentinel_` | `sentinel_log_management`, `sentinel_security_settings` |
| Transient prefix | `sentinel_` | `sentinel_log_queue`, `sentinel_email_hourly`, `sentinel_auth_counters_*` |
| Custom table prefix | `{$wpdb->prefix}sentinel_` | `wp_sentinel_logs`, `wp_sentinel_logs_archive`, `wp_sentinel_incidents` |
| Event key | lowercase snake_case, dot-segmented for security incidents | `user_login`, `php_fatal_error`, `security.bruteforce` |
| Cron hook | `sentinel_*` verb | `sentinel_cleanup_logs`, `sentinel_process_log_queue` |
| AJAX action | `wp_ajax_sentinel_<verb>_<noun>` | `wp_ajax_sentinel_export_logs_json`, `wp_ajax_sentinel_add_custom_event` |
| Nonce action | `sentinel_<scope>_nonce` / `sentinel_<context>` | `sentinel_admin_nonce`, `sentinel_dashboard_nonce`, `sentinel_settings`, `sentinel_ajax_nonce` |
| REST namespace | `sentinel-plugin/v1` | `/wp-json/sentinel-plugin/v1/logs` |
| Admin page slug | `sentinel-<name>` | `sentinel-dashboard`, `sentinel-logs`, `sentinel-events`, `sentinel-incidents`, `sentinel-settings` |
| Capability for editors | `edit_published_posts` (dashboard + logs only) | `Sentinel_Admin::init_menu()` |
| Capability for admins | `manage_options` (settings, incidents, event registry) | `Sentinel_Admin::init_menu()` |
| CSS namespace | `sentinel-*` class names | `sentinel-dashboard`, `sentinel-stat-card`, `sentinel-tab-content` |
| JS globals | `sentinel_admin_vars`, `sentinel_dashboard_vars` (`wp_localize_script`) | `admin/class-sentinel-admin.php` |
| File naming | `class-sentinel-<thing>.php` for classes; `<thing>-page.php` for admin views | `includes/class-sentinel-logger.php`, `admin/views/logs.php` |

---

## ARCHITECTURE

### Bootstrap flow

```
WordPress loads sentinel.php
  ├─ define plugin constants
  ├─ require_once includes/* and admin/*
  ├─ register activation / deactivation / uninstall hooks
  ├─ add_filter('cron_schedules', …) for sentinel_batch_interval
  ├─ add_action('plugins_loaded', 'sentinel_init')
  │     ├─ Sentinel_Migration::check_version()
  │     ├─ Sentinel_Memory_Monitor->init()
  │     ├─ Sentinel_File_Monitor::init()
  │     ├─ Sentinel_Integration_Registry init
  │     ├─ Sentinel_Auth_Counters::init() (Premium guarded inside)
  │     ├─ Sentinel_Incidents::init()       (Premium guarded inside)
  │     ├─ Sentinel_Digest_Reports::init()
  │     ├─ Sentinel_Privacy init
  │     ├─ Sentinel_Rest_API init
  │     ├─ Sentinel_Cleanup::init()
  │     └─ Sentinel_Admin::get_instance()->init() (admin context only)
  └─ add_action('wp_login', …, …, …) and ~40 other event-capture hooks
```

All bootstrapping happens directly inside `sentinel.php`; there is no
PSR-4 autoloader. The legacy `Sentinel` core class
(`includes/class-sentinel-core.php`) is small and largely unused — most
of what would normally live in a "Core" class lives in the procedural
top of `sentinel.php`.

### Logging pipeline

```
caller → sentinel_log_event($event_key, $data, $user_id)
  ├─ short-circuit if event is in sentinel_disabled_events (except critical)
  ├─ short-circuit if current user's role is in excluded_user_roles
  │       (excluded for non-critical events only)
  ├─ Sentinel_Logger::log(...)
  │     ├─ recursive-call guard
  │     ├─ memory threshold guard (memory_limit_threshold %)
  │     ├─ rate limit check  (per minute + per hour, transient counters)
  │     │     ├─ graceful_degradation  → keep 1/N
  │     │     ├─ hard_blocking        → stop
  │     │     └─ priority_only        → keep critical/high only
  │     ├─ if batch_logging_enabled: append to sentinel_log_queue option
  │     └─ else                    : $wpdb->insert wp_sentinel_logs now
  ├─ Pathway A — Security Alert email (security|system|critical)
  │     └─ sentinel_email_rate_limiter('security_alert_'.$event_key)
  │           ├─ per-event-key cooldown (default 1800 s)
  │           └─ global hourly cap     (default 5/hour)
  └─ Pathway B — Granular email (category/priority toggles)
        └─ same rate limiter, mutually exclusive with Pathway A
```

The mutual-exclusion-then-throttle design is explained in
`docs/Version 1.1.2 Changelog` — both pathways used to fire freely; v1.1.1
added mutual exclusion, v1.1.2 added the transient rate limiter.

### Batch / queue flow

```
log call (batch_logging_enabled = true)
  └─ Sentinel_Logger::add_to_queue() → option sentinel_log_queue
                                       (capped at 1000 entries)

cron sentinel_process_log_queue (every sentinel_batch_interval seconds,
default 60)
  └─ Sentinel_Logger::process_queue()
        ├─ check tables exist
        ├─ batch insert up to batch_size rows
        ├─ on failure: keep entries in queue
        └─ emit logs_cleaned / queue_overflow events as needed
```

### Notification pipeline

```
log event → Pathway A or B fires → wp_mail() with Sentinel_Email_Templates
            └─ subject/body HTML built per category & priority

cron sentinel_daily_digest  (daily, ideally 09:00 local)
cron sentinel_weekly_digest (weekly, ideally Mon 09:00 local)
  └─ Sentinel_Digest_Reports gathers stats from wp_sentinel_logs
     and posts a digest, gated by sentinel_email_*_summary toggles.
```

### Dashboard rendering

```
GET /wp-admin/admin.php?page=sentinel-dashboard
  └─ Sentinel_Admin::render_dashboard()
       └─ include admin/views/admin-page.php
             ├─ direct $wpdb queries against wp_sentinel_logs for KPIs
             ├─ inline JSON payloads for Chart.js
             └─ HTML for Recent Activity table
```

### File structure

```
sentinel/
├─ sentinel.php                            ← main bootstrap, event hooks, helpers
├─ readme.txt                              ← WordPress.org-style readme + changelog
├─ uninstall.php                           ← drops tables / options / crons on delete
├─ SENTINEL-REFERENCE.md                   ← this file
│
├─ includes/                               ← back-end systems (no UI)
│  ├─ event-definitions.php                  ← canonical event catalogue
│  ├─ class-sentinel-core.php                ← legacy/minimal core class
│  ├─ class-sentinel-events.php              ← event registry + disabled list helpers
│  ├─ class-sentinel-logger.php              ← write path + queue + rate limiting
│  ├─ class-sentinel-migration.php           ← dbDelta schema + default options
│  ├─ class-sentinel-cleanup.php             ← retention, archive, anonymise, optimise
│  ├─ class-sentinel-memory-monitor.php      ← per-request memory tracking + leak detection
│  ├─ class-sentinel-file-monitor.php        ← hash-based file integrity
│  ├─ class-sentinel-auth-counters.php       ← Premium: brute-force / enum / XML-RPC counters
│  ├─ class-sentinel-incidents.php           ← Premium: incident lifecycle + auto-resolve
│  ├─ class-sentinel-digest-reports.php      ← daily/weekly email digests
│  ├─ class-sentinel-email-templates.php     ← HTML + plain-text email layout
│  ├─ class-sentinel-privacy.php             ← Right-to-be-Forgotten shortcode + AJAX
│  ├─ class-sentinel-rest-api.php            ← REST endpoints + auth
│  └─ class-sentinel-integrations.php        ← WC / CF7 / WPForms / Gravity / Yoast registry
│
├─ admin/                                  ← admin-only UI
│  ├─ class-sentinel-admin.php               ← singleton, menus, asset enqueue, AJAX
│  ├─ css/sentinel-admin.css                 ← shared admin styles
│  ├─ js/sentinel-admin.js                   ← shared admin behaviour
│  ├─ js/sentinel-dashboard.js               ← chart rendering + dashboard widgets
│  ├─ js/sentinel-settings.js                ← settings page tabs + AJAX (API key, custom events)
│  ├─ js/sentinel-wp-dashboard.js            ← lightweight WP-dashboard widget logic
│  └─ views/
│      ├─ admin-page.php                      ← Sentinel Dashboard page
│      ├─ logs.php                            ← Activity Logs page
│      ├─ events-page.php                     ← Event Registry page
│      ├─ incidents-page.php                  ← Incidents page (Premium-only)
│      └─ settings-page.php                   ← Settings page (all tabs)
│
├─ assets/                                 ← frontend (public-facing) assets
│  ├─ js/sentinel-privacy.js                 ← deletion-request form behaviour
│  └─ css/sentinel-privacy.css               ← deletion-request form styling
│
└─ docs/                                   ← internal release/architecture notes
   ├─ CODEBASE_ANALYSIS_AND_ROADMAP.md
   ├─ Version 1.1.3 Changelog
   └─ Version 1.1.2 Changelog
```

---

## DATABASE SCHEMA

All custom tables are created by `Sentinel_Migration::create_tables()`
using `dbDelta()`. `Sentinel_Migration::check_version()` runs on
`plugins_loaded` and re-applies them on a version bump.

### `wp_sentinel_logs` (main event log)

| Column | Type | Notes |
|--------|------|-------|
| `id` | BIGINT UNSIGNED AUTO_INCREMENT | PK |
| `event_key` | VARCHAR(100) | matches `event-definitions.php` key, or a custom event |
| `category` | VARCHAR(50) | `authentication`, `content`, `system`, `security`, `errors`, `privacy`, `ecommerce`, `user`, `seo`, custom |
| `priority` | VARCHAR(20) | `low`, `medium`, `high`, `critical` |
| `user_id` | BIGINT | 0 for guest / unauthenticated events |
| `ip_address` | VARCHAR(45) | IPv4 or IPv6; nullable after anonymisation |
| `user_agent` | VARCHAR(500) | nullable |
| `url` | VARCHAR(500) | request URL where event fired |
| `data` | LONGTEXT | JSON-encoded structured context |
| `created_at` | DATETIME | indexed; queries filter on `created_at` ranges |

Key indexes: `event_key`, `category`, `priority`, `created_at`,
`user_id`. The dashboard, logs page and digests all use this table.

### `wp_sentinel_logs_archive`

Same schema as `wp_sentinel_logs`. Old rows are moved here by
`Sentinel_Cleanup::archive_logs()` once they exceed the configured
retention. Anonymisation and exports can target either table or both
(see the **Export Source** segmented toggle in Settings → Export &
Integration).

### `wp_sentinel_incidents` (Premium)

Managed by `Sentinel_Incidents`.

| Column | Type | Notes |
|--------|------|-------|
| `id` | BIGINT UNSIGNED AUTO_INCREMENT | PK |
| `type` | VARCHAR(40) | `bruteforce`, `enumeration`, `xmlrpc_flood` |
| `status` | VARCHAR(20) | `open`, `resolved` |
| `confidence` | VARCHAR(10) | `low`, `medium`, `high` (escalates after burst-then-success) |
| `ip` | VARBINARY(16) | packed via `inet_pton()` → printed with `inet_ntop()` |
| `username` | VARCHAR(100) | target username (if known) |
| `source` | VARCHAR(40) | e.g. `wp-login`, `xmlrpc`, `rest-api` |
| `event_count` | INT | aggregated counter for this incident |
| `first_seen` | DATETIME | initial detection time |
| `last_seen` | DATETIME | most recent qualifying attempt |
| `data` | LONGTEXT | JSON snapshot from `Sentinel_Auth_Counters` |
| `created_at` | DATETIME | row insert time |
| `updated_at` | DATETIME | row update time |

Auto-resolution is run by `sentinel_autoresolve_incidents` cron;
trust-listing via the `Trust IP` action calls
`sentinel_mark_ip_trusted($ip, 86400)` (24-hour temporary trust).

### Why no separate queue/performance tables?

The plugin deliberately keeps the schema minimal:

- Batch queue → `sentinel_log_queue` option (capped at 1,000 entries).
- Memory history → `sentinel_memory_analysis_history` option.
- File hashes → `sentinel_file_hashes` option (and
  `sentinel_file_baselines`).
- Rate-limit counters → transients.
- Email cooldowns → transients (`sentinel_email_cd_*`,
  `sentinel_email_hourly`).
- Trusted/blocked IPs and auth counter snapshots → transients keyed by
  IP / username (15-minute TTL).

This is fine at typical site volumes but is a **watch item** for
high-traffic sites — see [KNOWN ISSUES / RISKS / WATCH ITEMS](#known-issues--risks--watch-items).

---

## WORDPRESS OPTIONS / SETTINGS

| Option | Purpose | Default | Where used |
|--------|---------|---------|------------|
| `sentinel_db_version` | Schema version sentinel; drives `dbDelta` re-run on upgrade | matches `SENTINEL_VERSION` | `Sentinel_Migration::check_version` |
| `sentinel_log_management` | Main settings array: retention, batch, rate limit, memory, file monitoring, role exclusion, notifications, export | array of defaults from `Sentinel_Migration::set_default_options` | Logger, Cleanup, settings UI |
| `sentinel_security_settings` | Premium security: thresholds, IP allowlist, action mode, admin trust, email rate limiter knobs (`email_per_event_cooldown`, `email_hourly_cap`) | bruteforce_threshold=5, enumeration_threshold=9, xmlrpc_threshold=21, throttle_delay=1, action_mode=throttle, etc. | `Sentinel_Auth_Counters`, `Sentinel_Incidents`, email throttle |
| `sentinel_premium_license` | Stored licence key, last validated timestamp, status | empty / inactive | `sentinel_is_premium`, settings UI |
| `sentinel_api_key` | API key for REST authentication | empty until generated | `Sentinel_Rest_API`, settings UI |
| `sentinel_custom_events` | Premium custom event definitions added via Event Registry UI | `[]` | `Sentinel_Events`, events page |
| `sentinel_disabled_events` | Array of event keys whose logging is suppressed | `[]` | `sentinel_log_event` |
| `sentinel_file_hashes` | MD5 hashes per monitored file, last seen | `[]` | `Sentinel_File_Monitor` |
| `sentinel_file_baselines` | Original (pre-change) file metadata used for diffs | `[]` | `Sentinel_File_Monitor` |
| `sentinel_memory_analysis_history` | Rolling memory samples used by leak detection | `[]` | `Sentinel_Memory_Monitor` |
| `sentinel_log_queue` | In-DB queue used when batch logging is enabled (cap 1000) | `[]` | `Sentinel_Logger::process_queue` |
| `sentinel_failed_logs` / `sentinel_fallback_logs` (transient) | Transient fallback when `$wpdb` writes fail | empty | `Sentinel_Logger` retry path |
| `widget_sentinel_*` | WordPress dashboard widget visibility | per WP UX | `Sentinel_Admin::register_dashboard_widgets` |

`sentinel_log_management` (verified from `settings-page.php` form) holds
keys such as:

```
cleanup_enabled, cleanup_age_days (default 90), cleanup_max_entries (10000),
cleanup_schedule (daily), archive_retention_type (1year), archive_retention_days,
db_optimization_enabled, db_optimization_schedule (weekly),
batch_logging_enabled, batch_size (50), batch_frequency (60 s),
rate_limiting_enabled, rate_limit_per_minute (100), rate_limit_per_hour (1000),
rate_limit_behavior (graceful_degradation|hard_blocking|priority_only),
memory_monitoring_enabled, memory_limit_threshold (90), memory_leak_detection_enabled,
excluded_user_roles[],
log_ip_addresses, anonymize_data, anonymize_schedule (30/60/90/180/365),
role_based_access, audit_log_access,
right_to_be_forgotten, data_portability,
file_monitoring_enabled, monitor_custom_files, custom_file_paths,
monitor_theme_files, monitor_plugin_files, realtime_monitoring,
file_monitoring_frequency (hourly|twicedaily|daily|weekly),
exclude_log_files, custom_exclusions, file_size_threshold (10 bytes),
notification_email, admin_notifications,
sentinel_email_notifications_enabled,
sentinel_email_realtime_critical / _security / _error / _high,
sentinel_email_daily_summary / _error / _user,
sentinel_email_weekly_health / _performance / _security,
sentinel_email_cat_authentication / _content / _system / _error / _security / _user,
sentinel_email_priority_high / _medium / _low (critical is always on),
export_format (csv|json|xml), export_date_range, api_access, export_source (main|both|archive)
```

`sentinel_security_settings` (verified from the Privacy & Security tab):

```
action_mode (observe|throttle|block),
bruteforce_threshold, bruteforce_window (60..3600),
enumeration_threshold, enumeration_window,
xmlrpc_threshold, xmlrpc_window,
throttle_delay, temp_block_duration,
trust_admin_ips, admin_trust_duration,
incident_auto_resolve (1800..86400), notification_cooldown (300..3600),
ip_whitelist (newline list of IPs and CIDR ranges),
email_per_event_cooldown (1800 default), email_hourly_cap (5 default)
```

> The `sentinel_api_key` (visible at Settings → Database Status → API
> Key) and `sentinel_premium_license` value are sensitive. Treat them
> as secrets — never commit, never log in plaintext events.

---

## CORE SYSTEMS

### Logging System

- **Entry point.** Every event flows through
  `sentinel_log_event($event_key, $data, $user_id)`.
- **Data captured per row.** `event_key`, `category`, `priority`,
  `user_id`, `ip_address` (smart-detected from `REMOTE_ADDR` /
  `X-Forwarded-For` / Cloudflare headers / `X-Real-IP`), `user_agent`,
  `url`, JSON `data`, `created_at`.
- **Event metadata.** Sourced from `Sentinel_Events::get_event()` which
  defers to `sentinel_get_default_event_definitions()` plus
  `sentinel_custom_events`. If an event key is unknown, the logger
  still writes a row but falls back to `category=general`,
  `priority=low`.
- **Categories observed in the live Event Registry (74 events).**
  `authentication`, `content`, `system`, `security`, `errors`,
  `privacy`, `seo` (via Yoast integration), `ecommerce` (WooCommerce
  integration), plus user-defined `custom`.
- **Priorities.** `low`, `medium`, `high`, `critical`. The Event
  Registry filter dropdown lists all four. Some legacy code uses the
  string `error` (e.g. `sentinel_email_cat_error`) where the actual
  category in events is `errors` — see naming mismatches below.
- **Duplicate prevention.** Not strictly de-duplicated at the row
  level; instead noise is controlled by rate limiting, event disabling,
  role exclusion, and per-event email cooldowns. The Yoast bulk-update
  integration uses a `sentinel_yoast_bulk_tracker` transient to collapse
  bulk operations into a single event.
- **Context capture.** Each hook callback in `sentinel.php` constructs a
  small associative array of useful fields (e.g. plugin name, old/new
  status, role list, error file/line). Stack traces are included for
  PHP fatals via `error_get_last()` on shutdown.
- **Where logs are displayed.** Activity Logs page (`sentinel-logs`),
  the Sentinel Dashboard "Recent Activity" panel, the WP Dashboard
  widgets, the daily/weekly digest emails, and REST endpoints.
- **Retention / cleanup.** `Sentinel_Cleanup::cleanup_old_logs()` and
  `archive_logs()` run on the `sentinel_cleanup_logs` cron. Active rows
  older than `cleanup_age_days` (default 90) move to the archive;
  archived rows older than `archive_retention_type` are deleted.
  Anonymisation runs daily via `sentinel_anonymize_logs`.

### Error Monitoring

Hooks in `sentinel.php` capture:

| Source | Hook / API | Event key |
|--------|-----------|-----------|
| PHP errors (E_WARNING, E_NOTICE, E_DEPRECATED) | `set_error_handler` | `php_warning`, `php_notice`, `php_deprecated` |
| PHP fatals | `register_shutdown_function` + `error_get_last()` | `php_fatal_error` |
| `wp_die()` | `wp_die_handler` filter | `wp_error` |
| WordPress DB errors | `$wpdb->error` polling | `wp_database_error` |
| 404s | `wp` action (`is_404()`) | `http_404_error` |
| 500s / HTTP errors from `wp_remote_*` | `http_api_debug` action | `http_500_error`, `wp_remote_error` |
| Cron failures | `wp_cron` filter callbacks | `wp_cron_error` |
| File upload errors | `wp_handle_upload_prefilter` | `file_upload_error` |
| Plugin activation errors | `activated_plugin` + checks | `plugin_activation_error` |
| Memory limit (PHP `memory_limit`) | `Sentinel_Memory_Monitor` | `wp_memory_error`, `memory_warning`, `memory_critical` |
| Permission failures | `Sentinel_Admin` AJAX guards | `wp_permission_error`, `permission_denied` |
| `wp-config.php` problems | activation + file monitor | `wp_config_error` |
| Generic WP errors | catch-all | `wp_error` |

### Batch Logging

- **Toggle.** `sentinel_log_management.batch_logging_enabled` (default
  off — the dashboard / live admin confirms the row exists but defaults
  to immediate writes).
- **Queue storage.** `sentinel_log_queue` option (associative array,
  capped at 1,000 entries by `Sentinel_Logger`).
- **Batch size.** `batch_size` (default 50, range 1–500).
- **Cron frequency.** `sentinel_process_log_queue` runs on the custom
  `sentinel_batch_interval` schedule, default 60 s (range 30–3600 s)
  added in `sentinel_add_batch_cron_interval()`.
- **Failure handling.** If `$wpdb->insert` fails during queue
  processing, items remain in the queue. A separate
  `sentinel_retry_database` cron (hourly) handles retries.
- **Overflow / cap protection.** Queue insertions stop appending when
  the cap is reached and emit a logging warning. Combined with the
  rate-limiting behaviour, this protects against unbounded growth.
- **Manual processor.** The Settings page exposes
  "Run Cleanup Now" and "Optimize Database Now" but does **not**
  currently expose a "Process Queue Now" button — queue draining is
  cron-driven only. **Suggestion**, not a defect, see Open Items.
- **Performance tradeoff.** Immediate writes minimise data loss risk;
  queueing reduces request-time DB writes but introduces a small
  window where logs can be lost on a crash before the queue cron fires.

### Dashboard

- **URL.** `/wp-admin/admin.php?page=sentinel-dashboard`
- **Class / view.** `Sentinel_Admin::render_dashboard()` →
  `admin/views/admin-page.php`.
- **KPIs.** Logs Today, Active Users Today, Top Event Today, Database
  Size (computed from `information_schema.tables`), Retention Policy,
  Oldest Log Date (Premium gates the "Oldest Log Date" stat).
- **Charts (Chart.js 3.9.1, loaded from CDN).**
  - Activity Timeline (last 7 days, line chart).
  - Event Distribution (last 7 days, doughnut/bar).
  - Error Distribution (last 7 days, admins only).
  - Hourly Activity Heatmap.
- **Recent Activity.** Direct `$wpdb` SELECT against
  `wp_sentinel_logs` with admin vs editor scoping.
- **Top-of-page "Monitor Buttons".** Three cards — Rate Limiting Status
  / Memory Insights / Performance Status — that link to the
  corresponding admin areas. Performance Status is **Always On**;
  Rate Limiting and Memory Insights show **Active** when their feature
  toggles are on.
- **Test / demo data.** `Sentinel_Admin::handle_generate_test_data` is
  wired to AJAX action `wp_ajax_sentinel_generate_test_data`. There is
  no top-level "Generate Test Data" button on the rendered Dashboard
  in v1.1.2 — the handler exists for development use.

### Email Notifications

Two real-time pathways, both gated by the v1.1.2 rate limiter
(`sentinel_email_rate_limiter`):

1. **Security Alert (Pathway A).** Auto-fires for any event whose
   category is `security` or `system`, or whose priority is `critical`.
2. **Granular Notification (Pathway B).** Fires when the event matches
   admin-configured toggles in Settings → Notifications:
   - Real-time Alerts: critical, security, error, high priority.
   - Category filters: authentication, content, system, error,
     security, user.
   - Priority filters: critical (always on, locked), high, medium, low.

Plus two cron pathways (separate, not subject to the rate limiter):

3. **Daily Digest** (`sentinel_daily_digest`, scheduled 09:00 local).
4. **Weekly Digest** (`sentinel_weekly_digest`, scheduled Monday 09:00
   local), each driven by their toggles
   (`sentinel_email_daily_summary` / `_error` / `_user`,
   `sentinel_email_weekly_health` / `_performance` / `_security`).

Implementation specifics:

- **Recipient.** `sentinel_log_management.notification_email`. Single
  address only — the admin UI does not currently support multiple
  recipients (see [KNOWN ISSUES](#known-issues--risks--watch-items)).
- **Cooldown defaults.** 30 minutes per event key,
  5 emails/hour globally. Both are configurable via
  `sentinel_security_settings.email_per_event_cooldown` and
  `email_hourly_cap`.
- **Mutual exclusion.** Only one of Pathway A or Pathway B fires per
  event call (v1.1.1 change).
- **Fail-open.** If `get_transient()` returns `false`, the cooldown
  check passes and the email still sends.
- **Templates.** HTML + plain-text via `Sentinel_Email_Templates`.
- **What is NOT throttled.** Daily and weekly digests, incident
  detection itself, log writes, and the security action engine
  (observe / throttle / block).

### Memory Monitoring

`Sentinel_Memory_Monitor` hooks into request lifecycle checkpoints
(`init`, `wp_loaded`, `template_redirect`, `shutdown`) and tracks
memory at each step.

- **Thresholds.** `early_warning`, `warning`, `critical` — defined as
  percentages of the PHP `memory_limit`. The "Memory Threshold" admin
  setting (default 90%) is the highest of these, used to short-circuit
  the logger before a fatal.
- **Memory hog detection.** Identifies functions/hooks with the largest
  delta and includes them in `memory_*` log events.
- **History.** Stored in the `sentinel_memory_analysis_history` option.
- **Leak detection (Premium).** `detect_memory_leaks()` looks at the
  rolling history and flags consistent upward drift with confidence
  scoring.
- **Auto-optimisation (Premium).** Can clear expired transients, flush
  object cache, and emit a `memory_auto_optimization` event.
- **Admin display.** Memory Insights dashboard widget on the WP
  Dashboard plus a card on the Sentinel Dashboard.

### Performance Monitoring

Light by design — not a APM replacement.

- Captures `memory_peak_usage`, `db_query_count`, `response_time_ms`
  events on shutdown (controlled by `memory_monitoring_enabled`).
- Samples are not stored in a custom table; small rolling buffers are
  kept in transients (`sentinel_performance_samples`) and recent rows
  in `wp_sentinel_logs`.
- The "Performance Status" Dashboard card is always shown and
  surfaces basic runtime info.

### Role Exclusion

- **Source of truth.** `sentinel_log_management.excluded_user_roles`
  (array of role slugs).
- **UI.** Settings → Performance tab → "Excluded User Roles", with a
  per-role checklist and user counts (verified live: Administrator,
  Editor, Author, Contributor, Subscriber, Customer, Shop Manager,
  SEO Manager, SEO Editor).
- **Multi-role users.** If any role on the user matches the exclusion
  list, the event is skipped — i.e. exclusion wins.
- **Critical bypass.** Helper checks in `sentinel_log_event()` ensure
  `critical` events and system errors are still logged regardless of
  role exclusion. Admin copy on the page mirrors this:
  _"System errors and security events may still be logged regardless
  of user role."_

### Security / Login / Abuse Events

- Failed logins emit `login_failed` (free) and feed
  `Sentinel_Auth_Counters` (Premium) which evaluates against the
  brute-force / enumeration / XML-RPC thresholds.
- Successful logins after a burst can escalate an existing incident
  via `Sentinel_Incidents::escalate_on_success_after_burst()`.
- Bad auth cookies → `auth_cookie_bad`.
- Nonce failures → `nonce_verification_failed`.
- Capability/permission denials → `permission_denied`.
- IP detection is layered: respects Cloudflare's
  `CF-Connecting-IP`, falls back to `X-Forwarded-For` and
  `X-Real-IP`, finally `REMOTE_ADDR`.
- User-agent is captured (capped at 500 chars).
- The IP allowlist (Settings → Privacy & Security → Sentinel+ Security
  Controls → IP Allowlist) bypasses counters, throttle and block paths
  but **does not** stop activity logging.

### Cron / Scheduled Tasks

Authoritative list from `sentinel_get_cron_hook_names()`:

| Hook | Frequency | Purpose | Registered / Handled in |
|------|-----------|---------|--------------------------|
| `sentinel_cleanup_logs` | `daily` (default; configurable to `hourly`/`weekly`) | Archive then delete old logs | `Sentinel_Cleanup` |
| `sentinel_optimize_database` | `weekly` (configurable to daily/monthly) | `OPTIMIZE TABLE` for both log tables | `Sentinel_Cleanup` |
| `sentinel_process_log_queue` | `sentinel_batch_interval` (default 60 s) | Drain `sentinel_log_queue` to DB | `Sentinel_Logger` |
| `sentinel_retry_database` | `hourly` | Retry queued failed inserts | `Sentinel_Logger` |
| `sentinel_autoresolve_incidents` | `hourly` | Auto-close stale Premium incidents | `Sentinel_Incidents` |
| `sentinel_anonymize_logs` | `daily` | Mask IPs and user data per schedule | `Sentinel_Cleanup` |
| `sentinel_daily_digest` | `daily` (~09:00 local) | Send daily digest email | `Sentinel_Digest_Reports` |
| `sentinel_weekly_digest` | `weekly` (~Mon 09:00 local) | Send weekly digest email | `Sentinel_Digest_Reports` |
| `sentinel_check_files` | `hourly`/`twicedaily`/`daily`/`weekly` (per setting, default `daily`) | Run hash-based file integrity check | `Sentinel_File_Monitor` |
| `sentinel_optimize_tables` | _legacy_ — no longer scheduled, but cleared on activation for older installs | — | `sentinel_get_cron_hook_names` |

Custom schedule: `sentinel_batch_interval` is registered via
`cron_schedules` filter in `sentinel_add_batch_cron_interval()`.

### Admin Tools / Test Utilities

| Tool | Where | What it does |
|------|-------|--------------|
| 🧹 **Run Cleanup Now** | Settings → Log Management | Manually triggers `cleanup_now` action (calls `Sentinel_Cleanup::cleanup_old_logs()`). |
| ⚡ **Optimize Database Now** | Settings → Log Management | Manually runs `OPTIMIZE TABLE` on both log tables. |
| 🕵️ **Anonymize Logs** | Settings → Log Management | Forces anonymisation pass via `anonymize_logs` action. |
| 🔄 **Retry Failed Logs** | Settings → Log Management | Reprocesses the transient fallback queue. |
| 📤 **Export / Quick Export Last 30 Days** | Settings → Export & Integration | AJAX-driven export in JSON/CSV/XML for main/archive/both. |
| 🔑 **Generate / Regenerate / Clear API Key** | Settings → Database Status | Manages the REST API key. |
| 🧪 **Test Custom Event** | Event Registry (Premium custom events row) | AJAX-fires a custom event to verify wiring. |
| 🧪 **Test File Check** | (AJAX action `sentinel_test_file_check`) | Forces an immediate file integrity scan. Not currently exposed as a visible button in v1.1.2 admin. |
| Trust IP (24h) | Incidents page | Calls `sentinel_mark_ip_trusted($ip, 86400)`. |
| Resolve incident | Incidents page | Marks an incident `resolved`. |
| Generate test data | `wp_ajax_sentinel_generate_test_data` | Dev-only handler; not exposed as a visible button. Reserved for the user to invoke explicitly. |

**Destructive controls — do not click without explicit instruction:**
"Run Cleanup Now", "Optimize Database Now", "Anonymize Logs",
"Clear Key (Debug)", "Regenerate" (API key), "Resolve" (incident),
"Trust IP".

---

## EVENT TYPES / ENUMS / ALLOWLISTS

All system events are listed in `event-definitions.php`. The Event
Registry UI in v1.1.2 reports **74 total system events** (verified from
the live admin). The full table below was captured from the live
registry; categories and priorities match those returned by
`sentinel_get_default_event_definitions()`.

### Authentication

| Event Key | Priority | Trigger |
|-----------|----------|---------|
| `user_login` | low | Successful WordPress login (`wp_login` action) |
| `user_logout` | low | `wp_logout` action |
| `user_registered` | low | `user_register` action |
| `login_failed` | medium | `wp_login_failed` action |
| `auth_cookie_bad` | medium | `auth_cookie_bad_username` / `auth_cookie_bad_hash` |
| `auth_success` | low | Authentication success (REST / WP-login path) |
| `auth_fail` | medium | Authentication failure feeding `Sentinel_Auth_Counters` (Premium) |

### Security

| Event Key | Priority | Trigger |
|-----------|----------|---------|
| `security_incident_opened` | high | New incident created |
| `security_incident_updated` | medium | Incident escalated or updated |
| `security_incident_resolved` | low | Incident auto-resolved or manually resolved |
| `rate_limit_warning` | medium | Approaching the rate limit |
| `rate_limit_exceeded` | high | Crossed the hard rate limit |
| `security_action_triggered` | medium | Action engine activated |
| `security_throttle_applied` | medium | Throttle delay added to a login |
| `security_ip_blocked` | high | IP temporarily blocked |
| `security_blocked_attempt` | high | Blocked request from a restricted IP |
| `file_modified` | high | File integrity check found a change |
| `nonce_verification_failed` | medium | WordPress nonce verification failed |
| `permission_denied` | medium | Cap check failed |
| `email_spam_detected` | medium | Outbound spam-like email detected |

### Content

| Event Key | Priority | Trigger |
|-----------|----------|---------|
| `profile_updated` | low | `profile_update` action |
| `post_published`, `post_updated`, `post_deleted` | low | `transition_post_status` + `before_delete_post` |
| `comment_posted`, `comment_approved`, `comment_deleted` | low | Comment hooks |

### System

| Event Key | Priority | Trigger |
|-----------|----------|---------|
| `plugin_activated` / `plugin_deactivated` / `plugin_updated` / `plugin_events_cleaned` | low | Plugin lifecycle hooks |
| `theme_switched` / `theme_updated` | low | Theme lifecycle hooks |
| `core_upgraded` | low | `_core_updated_successfully` |
| `maintenance_mode_enabled` / `maintenance_mode_disabled` | medium / low | Maintenance flag change |
| `api_key_generated` | low | AJAX regenerate / generate |
| `option_updated` | low | Watched `update_option` filter |
| `logs_cleaned`, `tables_optimized` | low | Cron cleanup / optimisation outcomes |
| `admin_test` | low | Reserved for QA / test event harness |
| `log_accessed` | low | When "Audit Log Access" is enabled and an admin opens the logs page |
| `memory_peak_usage`, `db_query_count`, `response_time_ms` | low | Per-request perf metrics on shutdown |
| `memory_early_warning`, `memory_warning` | medium | Memory threshold breached at lower levels |
| `memory_critical` | critical | Memory threshold at the highest level |
| `memory_auto_optimization` | low | Premium auto-optimisation ran |
| `memory_leak_warning` | medium | Premium leak detector fired |
| `email_sent` | low | Sentinel sent an email |

### Errors

| Event Key | Priority | Trigger |
|-----------|----------|---------|
| `php_fatal_error` | critical | Shutdown handler caught fatal |
| `php_warning` | medium | `set_error_handler` E_WARNING |
| `php_notice` | low | `set_error_handler` E_NOTICE |
| `php_deprecated` | medium | E_DEPRECATED |
| `wp_database_error` | high | `$wpdb->last_error` polling |
| `wp_memory_error` | high | Memory monitor crossed `critical` |
| `wp_permission_error` | high | Capability check failed during a core flow |
| `wp_config_error` | high | wp-config / .htaccess problem detected |
| `http_404_error` | low | 404 hit |
| `http_500_error` | high | 500 hit |
| `wp_remote_error` | medium | `wp_remote_*` HTTP error |
| `wp_cron_error` | medium | Cron callback raised |
| `plugin_error`, `theme_error`, `plugin_activation_error` | high | Plugin / theme failures |
| `file_upload_error` | medium | Upload pre-filter rejection |
| `file_permission_error` | high | File system permission failure |
| `wp_error` | high | Generic WP_Error captured via `wp_die_handler` |
| `email_failed` | medium | `wp_mail` returned false |
| `email_delivery_error` | high | SMTP-level failure surfaced via filters |

### Privacy

| Event Key | Priority | Trigger |
|-----------|----------|---------|
| `user_deletion_request` | low | Visitor submits the Right-to-be-Forgotten form |
| `user_data_anonymized` | low | Anonymisation cron processed a user / IP |
| `admin_deletion_request_processed` | low | Admin processed a deletion request via AJAX |

### Integrations (registered when host plugin is active)

| Event Key | Category | Priority | Trigger |
|-----------|----------|----------|---------|
| `yoast_meta_updated`, `yoast_score_changed`, `yoast_schema_updated`, `yoast_bulk_action` | seo | medium / low / medium / high | Yoast SEO hooks (collapsed via `sentinel_yoast_bulk_tracker` for bulk ops) |
| WooCommerce order lifecycle events | ecommerce | low–high | `Sentinel_Integration_Registry` WC integration |
| Contact Form 7 submissions | content | low | CF7 integration |
| WPForms submissions | content | low | WPForms integration |
| Gravity Forms submissions | content | low | Gravity Forms integration |

### Incident enums (Premium)

| Enum | Values |
|------|--------|
| `type` | `bruteforce`, `enumeration`, `xmlrpc_flood` |
| `status` | `open`, `resolved` |
| `confidence` | `low`, `medium`, `high` |

### Rate-limit behaviours

| Value | Effect |
|-------|--------|
| `graceful_degradation` | Sample 1-in-10 over the limit, preserve visibility |
| `hard_blocking` | Drop all events past the limit until the window rolls |
| `priority_only` | Keep `critical` and `high`, drop the rest |

### Known naming inconsistency

The settings UI uses `sentinel_email_cat_error` (singular) while the
event-registry category is `errors` (plural). Both refer to the same
bucket. See [Naming mismatches](#naming-mismatches-between-docs-code-and-live-admin-ui).

---

## ADMIN PAGES / UI

| Page | URL / slug | Purpose | Capability |
|------|-----------|---------|------------|
| Sentinel Dashboard | `admin.php?page=sentinel-dashboard` | KPIs, charts, monitor cards, recent activity | `edit_published_posts` |
| Activity Logs | `admin.php?page=sentinel-logs` | Filterable log table with pagination | `edit_published_posts` (editors see scoped categories only when role-based access is enabled) |
| Incidents (Premium) | `admin.php?page=sentinel-incidents` | Incident lifecycle, filters, resolve / trust actions | `manage_options` **and** `sentinel_is_premium()` |
| Event Registry | `admin.php?page=sentinel-events` | Enable/disable events, custom events, integrations, usage stats | `manage_options` |
| Settings | `admin.php?page=sentinel-settings` | All settings tabs (Database Status, Log Management, Performance, Privacy & Security, Notifications, Export & Integration) | `manage_options` |
| WP Dashboard widgets | `index.php` | "Sentinel Memory Insights" + "Sentinel+ Incidents" | per-user dashboard prefs |

For each page:

### Sentinel Dashboard
- **File.** `admin/views/admin-page.php` via
  `Sentinel_Admin::render_dashboard()`.
- **Shows.** Logs Today, Active Users Today, Top Event Today, DB size,
  retention policy, Oldest Log Date (Premium), 3 monitor cards
  (Rate Limiting / Memory Insights / Performance Status), four charts,
  Recent Activity table.
- **AJAX.** `sentinel_memory_insights` for the Memory Insights modal /
  drilldown.
- **Assets.** `sentinel-admin.css`, `chart-js` (CDN),
  `sentinel-admin.js`, `sentinel-dashboard.js`.
- **Live label.** "Sentinel Dashboard".

### Activity Logs
- **File.** `admin/views/logs.php`.
- **Filters.** Event Type, User, Priority, IP Address, Date From / To
  (verified live).
- **Pagination.** 50 per page.
- **Actions.** View row details modal; CSV/JSON/XML export from the
  Settings page rather than inline. Optional audit log if
  `audit_log_access` is enabled (logs `log_accessed` event on view).
- **AJAX.** `sentinel_export_logs_json`.
- **Live label.** "Activity Logs".

### Incidents (Premium)
- **File.** `admin/views/incidents-page.php`.
- **Capability.** Hard-gated on `manage_options` **and**
  `sentinel_is_premium()`. Non-premium users see a `wp_die()` with
  _"This feature requires Sentinel+ Premium."_
- **Shows.** 30-day stats (Open, Total, High Confidence, Unique IPs)
  + a filterable table (Status / Type / Confidence / Search).
- **Actions.** `resolve`, `trust_ip` (both nonce-protected by
  `wp_verify_nonce($_GET['_wpnonce'], $action . '_' . $incident_id)`).
- **AJAX.** `sentinel_ajax_nonce` for the per-incident details modal.

### Event Registry
- **File.** `admin/views/events-page.php`.
- **Sections.** 3rd Party Plugin Integrations (Yoast/WC/CF7/WPForms/Gravity
  with one-click setup templates), Custom Events Management (Premium),
  System Events Registry (full 74-event filterable table with usage
  count + last triggered).
- **Actions.** Toggle event on/off (writes
  `sentinel_disabled_events`), add/remove custom event (Premium),
  setup an integration template, test a custom event.
- **AJAX.** `sentinel_toggle_system_event`,
  `sentinel_add_custom_event`, `sentinel_remove_custom_event`,
  `sentinel_get_custom_events`, `sentinel_toggle_custom_event`,
  `sentinel_setup_template`, `sentinel_test_custom_event`.

### Settings
- **File.** `admin/views/settings-page.php`.
- **Tabs.** Database Status, Log Management, Performance, Privacy &
  Security, Notifications, Export & Integration. Verified live.
- **Submit.** Single `Save Settings` button at the bottom of the form;
  active tab is persisted in `sentinel_current_tab` hidden field.
- **AJAX.** `sentinel_save_api_key` (regenerate / clear API key),
  export helpers, integration setup helpers.
- **Live UI highlights.** Database tables status indicators, License &
  Plan Status panel (visible Premium key
  `SEN-M40Q-QXSF-7NEX-3JSJ` on the local install — **do not commit**),
  Digest Report Status block at the bottom, segmented toggles for
  Security Response Mode and Export Source.

### Dashboard widgets

| Widget | Class hook | Shown to | Source |
|--------|-----------|----------|--------|
| Sentinel Memory Insights | `Sentinel_Admin::register_dashboard_widgets` | any user with WP dashboard access | `Sentinel_Memory_Monitor::get_memory_stats` |
| Sentinel+ Incidents | same | admins with Premium | `Sentinel_Incidents::get_incident_stats` |

---

## AJAX / REST HANDLERS

### AJAX (admin-only unless noted)

| Action | Capability check | Nonce | File |
|--------|------------------|-------|------|
| `sentinel_export_logs` | `manage_options` | `sentinel_admin_nonce` | `admin/class-sentinel-admin.php` |
| `sentinel_clear_old_logs` | `manage_options` | `sentinel_admin_nonce` | `admin/class-sentinel-admin.php` |
| `sentinel_generate_test_data` | `manage_options` | `sentinel_admin_nonce` | `admin/class-sentinel-admin.php` |
| `sentinel_export_logs_json` | `manage_options` | `sentinel_export_logs_json` | `admin/class-sentinel-admin.php` |
| `sentinel_save_api_key` | `manage_options` | `sentinel_dashboard_nonce` | `sentinel.php` |
| `sentinel_add_custom_event` | `manage_options` + Premium | `sentinel_ajax_nonce` | `sentinel.php` |
| `sentinel_remove_custom_event` | `manage_options` + Premium | `sentinel_ajax_nonce` | `sentinel.php` |
| `sentinel_get_custom_events` | `manage_options` | `sentinel_ajax_nonce` | `sentinel.php` |
| `sentinel_toggle_custom_event` | `manage_options` + Premium | `sentinel_ajax_nonce` | `sentinel.php` |
| `sentinel_toggle_system_event` | `manage_options` | `sentinel_ajax_nonce` | `sentinel.php` |
| `sentinel_setup_template` | `manage_options` | `sentinel_ajax_nonce` | `sentinel.php` |
| `sentinel_test_custom_event` | `manage_options` + Premium | `sentinel_ajax_nonce` | `sentinel.php` |
| `sentinel_memory_insights` | `manage_options` | `sentinel_admin_nonce` | `includes/class-sentinel-memory-monitor.php` |
| `sentinel_test_file_check` | `manage_options` | `sentinel_admin_nonce` | `includes/class-sentinel-file-monitor.php` |
| `sentinel_get_file_diff` (Premium) | `manage_options` + Premium | `sentinel_admin_nonce` | `includes/class-sentinel-file-monitor.php` |
| `sentinel_submit_deletion_request` (logged-in **and** `nopriv`) | none — public form | `sentinel_deletion_request` nonce field on the form | `includes/class-sentinel-privacy.php` |
| `sentinel_process_deletion_request` | `manage_options` | `sentinel_ajax_nonce` | `includes/class-sentinel-privacy.php` |

`wp_ajax_nopriv_sentinel_submit_deletion_request` is the **only**
public AJAX surface; everything else is authenticated.

### REST (`sentinel-plugin/v1`)

| Route | Methods | Description | Auth |
|-------|---------|-------------|------|
| `/logs` | GET | Paginated event logs with filters | API key header **or** logged-in `manage_options` |
| `/stats` | GET | Aggregated counts for dashboard / external monitor | same as above |
| `/events/types` | GET | List of registered event keys + metadata | same as above |

`Sentinel_Rest_API::check_permissions()` accepts either the
`X-Sentinel-API-Key` HTTP header **or** an `api_key` query param.
Cookie auth via WordPress also works if the caller has the right
capability. The API can be globally disabled via
`sentinel_log_management.api_access`.

---

## JAVASCRIPT MODULES

| File / namespace | Purpose | Loaded on |
|------------------|---------|-----------|
| `admin/js/sentinel-admin.js` | Shared admin helpers, AJAX wrappers, dialog / toast scaffolding. Localised via `sentinel_admin_vars` (`ajaxurl`, `nonce`). | Any Sentinel admin page. |
| `admin/js/sentinel-dashboard.js` | Chart.js wiring (timeline / event distribution / error distribution / hourly heatmap), monitor-card navigation, API-key management AJAX. Localised via `sentinel_dashboard_vars`. | Sentinel admin pages (depends on `chart-js`). |
| `admin/js/sentinel-settings.js` | Tab switching, conditional rows (e.g. batch-size visibility when batch-logging is enabled), segmented toggles, custom-event AJAX form, copy/regenerate/clear API key, export buttons. | Sentinel admin pages. |
| `admin/js/sentinel-wp-dashboard.js` | Lightweight logic for the WP Dashboard widgets (Memory Insights / Incidents) — refresh / quick-action links. | WP Dashboard (`index.php`) only. |
| `assets/js/sentinel-privacy.js` | Front-end Right-to-be-Forgotten form submission via `sentinel_submit_deletion_request` AJAX action. | Wherever `[sentinel_data_deletion_request]` is rendered. |

There is no bundler / module system — these are plain jQuery-flavoured
scripts. The `sentinel-admin` script is the dependency for
`sentinel-dashboard` and `sentinel-settings` (loaded in that order via
`wp_enqueue_script`).

---

## CSS / ASSETS

- **Admin CSS.** `admin/css/sentinel-admin.css` (single file, loaded on
  every Sentinel admin page and on the WP Dashboard so the widgets are
  styled). Uses a `sentinel-*` class namespace.
- **Frontend CSS.** `assets/css/sentinel-privacy.css` — only used by
  the deletion-request shortcode.
- **JS.** Four admin files + one frontend file (above). No source
  maps, no transpilation, no minification step.
- **Chart library.** Chart.js 3.9.1 pulled from
  `https://cdn.jsdelivr.net/npm/chart.js` (not bundled).
- **Icons.** WordPress dashicons (`dashicons-shield`,
  `dashicons-chart-bar`, `dashicons-performance`, etc.).
- **Build process.** **None.** There is no `package.json`, no
  webpack/vite/parcel/prepros pipeline in the plugin folder. Edit
  the CSS/JS files directly.
- **Minified files.** None shipped — there is no separate `.min.js`
  / `.min.css`. The unminified files are the source and the artefact.

---

## SECURITY MODEL

- **Capabilities.** Admin pages enforce
  `current_user_can('edit_published_posts')` (Dashboard + Activity
  Logs) or `current_user_can('manage_options')` (Settings, Event
  Registry, Incidents). Incidents additionally requires Premium.
  Every AJAX handler re-checks the capability (verified across
  `Sentinel_Admin`, `Sentinel_Memory_Monitor`, `Sentinel_File_Monitor`,
  `Sentinel_Privacy`).
- **Nonces.** `sentinel_admin_nonce`, `sentinel_dashboard_nonce`,
  `sentinel_settings`, `sentinel_ajax_nonce`,
  `sentinel_export_logs_json`, `sentinel_deletion_request`,
  `sentinel_toggle_event` (Event Registry form), plus
  per-action incident nonces of the form `$action.'_'.$incident_id`.
- **Sanitisation.** `sanitize_text_field()`, `intval()`, `floatval()`,
  `wp_kses_post()` (email templates), `sanitize_email()` for the
  notification address, `inet_pton()`/`inet_ntop()` for IP storage in
  incidents.
- **Escaping.** `esc_html`, `esc_attr`, `esc_url`, `esc_textarea` used
  throughout admin views. JSON for charts is encoded with
  `wp_json_encode`.
- **SQL.** All custom queries use `$wpdb->prepare()` for parameterised
  values. Dashboard date queries and stats queries are all `prepare()`
  -wrapped (verified in `admin-page.php` and `logs.php`).
- **File upload safety.** Sentinel does not handle uploads itself; it
  observes WordPress's upload pipeline via filters.
- **Sensitive data masking.** `Sentinel_Cleanup::anonymize_old_logs()`
  hashes IPs and replaces user identifiers with `anonymized`
  placeholders after the configured window.
- **IP / UA capture and privacy.**
  - IP can be disabled site-wide (`log_ip_addresses` toggle).
  - User-agent is truncated to 500 chars.
  - The Right-to-be-Forgotten shortcode lets visitors request data
    deletion without an admin account.
- **Licence validation HTTP.** `sentinel_is_premium()` makes a
  `wp_remote_post()` to `https://getsentinel.dev/wp-json/sentinel/v1/validate-license`
  with the licence key. Failure returns gracefully (no premium).
  A bypass `SENTINEL_SITE_LICENSE_KEY` define exists for the
  `getsentinel.dev` site itself.
- **Known weak spots / things to be honest about.**
  - The licence key and `sentinel_api_key` are both stored as plain
    options. WP-CLI / DB admins can read them. Acceptable for an
    activity-log plugin, but worth knowing.
  - The Premium licence is **visible in the Settings → Database Status
    HTML** as the value of a regular text input (not masked). The API
    key is masked client-side but the full key is rendered into a
    `data-key` attribute on the Copy button — so any admin with DOM
    inspection can read it. This matches WP-admin norms but is worth
    flagging.
  - Some admin views still query `$wpdb` with the table name directly
    interpolated (e.g. `admin-page.php` uses `$table` from
    `$wpdb->prefix . 'sentinel_logs'`). This is safe because the name
    is built from `$wpdb->prefix`, but it pre-dates a more strict
    `prepare()` style.
  - `audit_log_access` reads `$_GET` and `$_SERVER['REMOTE_ADDR']` and
    pushes them as the `query` data into a `log_accessed` event. This
    can include sensitive query parameters — minor watch item.

---

## PUBLIC DOCUMENTATION ALIGNMENT

Comparison based on the public docs at <https://getsentinel.dev/docs/>
(captured 2026-05-11 in `tools/sentinel-explore/docs-public.txt`),
codebase v1.1.3, and the local admin UI.

| Docs claim / section | Codebase status | Live admin status | Notes |
|----------------------|-----------------|--------------------|-------|
| **Getting Started → First Steps / Installation / Quick Setup** | Confirmed | Confirmed | Step-by-step matches: Dashboard, Event Registry, Log Management, Privacy & Security tab. |
| **Sentinel+ Activation** | Confirmed | Confirmed | Licence flow uses `sentinel_is_premium()` against `getsentinel.dev`; license input is at Settings → Database Status. |
| **Authentication Tracking (Sentinel+)** with brute-force / enumeration / XML-RPC counters | Confirmed | Confirmed | `Sentinel_Auth_Counters` + `Sentinel_Incidents`; thresholds + windows match the Privacy & Security tab. |
| **Email Notifications & Digest Reports** | Confirmed | Confirmed | Real-time + daily + weekly digests; toggle keys match. |
| **IP Allowlist Management** including CIDR | Confirmed | Confirmed | `ip_whitelist` textarea on Privacy & Security, CIDR parsed by `Sentinel_Auth_Counters`. |
| **General Settings → Basic / Advanced configuration / Performance considerations** | Confirmed | Confirmed | Settings tabs match. |
| **Event Registry** | Confirmed | Confirmed | Live UI shows the same 3 sections described in docs (3rd Party Integrations / System Events / Custom Events). |
| **Performance Options → Batch Logging / Rate Limiting / Smart Memory Monitoring** | Confirmed | Confirmed | Toggle keys match. |
| **Memory Leak Detection (Sentinel+)** | Confirmed | Confirmed | `Sentinel_Memory_Monitor::detect_memory_leaks()` and the Premium gate match the docs. |
| **Privacy Settings → IP anonymization** | Confirmed | Confirmed | `anonymize_data` + `anonymize_schedule`. |
| **Security Controls → Response Mode / Detection Thresholds / Admin Trust** | Confirmed | Confirmed | Segmented toggle `observe / throttle / block`, thresholds, `trust_admin_ips`, `admin_trust_duration`. |
| **Data Deletion Request** shortcode | Confirmed | Confirmed | `[sentinel_data_deletion_request]` + AJAX flow exist (`Sentinel_Privacy`). |
| **File Monitoring (free + Premium extensions)** | Confirmed | Confirmed | `Sentinel_File_Monitor` and the File Monitoring card in Privacy & Security match. |
| **Enhanced Diff Viewer (Sentinel+)** | Partially confirmed | **Needs manual verification** | AJAX `sentinel_get_file_diff` exists and is Premium-gated, but there is no obvious visible button in v1.1.3 admin to invoke it. The UI entry point should be verified. |
| **REST API → /logs, /stats, /events/types** | Confirmed | Confirmed | Namespace `sentinel-plugin/v1`. |
| **REST API → JavaScript example** | Confirmed | n/a | Code path matches doc snippets. |
| **Common Issues** & **FAQs** | Confirmed | n/a | Aligns with codebase behaviour (events disabled, rate limits, fallback). |
| **Custom Events (Premium)** | Confirmed | Confirmed | AJAX-driven add/remove + display in Event Registry. |
| **Alerts & Notifications → Recommended Starter Configuration** | Confirmed | Confirmed | Matches the default `sentinel_email_*` toggles. |
| **Export & Import → Supported Formats** | Confirmed | Confirmed | CSV / JSON / XML available; date range + source toggles match. |
| **User Management → Role-Based Permissions** | Confirmed | Confirmed | `role_based_access` toggle in Privacy & Security restricts editors. |
| **Yoast SEO Integration** | Confirmed | Confirmed | 4 events visible in the Event Registry (`yoast_meta_updated`, `yoast_score_changed`, `yoast_schema_updated`, `yoast_bulk_action`). |
| **WooCommerce / CF7 / WPForms / Gravity Forms Integration** | Confirmed in code (`Sentinel_Integration_Registry`) | Visible as setup templates in the Event Registry | Their events only appear in the registry after the relevant plugin is active. |

### Features documented publicly but not clearly found in code

- **"Method 1: WordPress Repository (Coming Soon)"** in
  Installation — the plugin is currently distributed via manual
  download from `getsentinel.dev`, not the WP.org plugin directory.
  Code does not assume WP.org auto-update; this is correctly labelled
  in the docs.
- **"Backup Strategy"** Pro Tip recommending regular exports — the
  Export & Integration tab supports this, but there is no built-in
  scheduled-export mechanism. The advice is aspirational.

### Features found in code but missing from public docs

- **`audit_log_access` setting** (Privacy & Security → Access Control
  & Security). Logs `log_accessed` events whenever an admin opens a
  Sentinel screen. Not described on the docs site.
- **Email rate limiter knobs** `email_per_event_cooldown` and
  `email_hourly_cap` (introduced in v1.1.2). The behaviour is
  described in `docs/Version 1.1.2 Changelog` (shipped inside the
  plugin) but **not** on the public docs site.
- **`sentinel_failed_logs` / fallback transient** retry behaviour. The
  "Retry Failed Logs" admin button exists and the cron
  `sentinel_retry_database` runs hourly — neither is described in the
  public docs.
- **AJAX manual file check** (`sentinel_test_file_check`) and **manual
  cleanup / optimise / anonymise / retry buttons** at Settings → Log
  Management. The docs mention automated cleanup but not the manual
  triggers.
- **Mutual exclusion of Pathway A/B + per-event-key cooldown design.**
  The cooldown is described, but the mutual-exclusion guarantee from
  v1.1.1 is only in the internal changelog.
- **Rate-limit behaviour modes** `graceful_degradation` /
  `hard_blocking` / `priority_only` — described in the admin UI but
  only summarised in public docs.

### Features visible in wp-admin but missing from public docs

- The **License & Plan Status** card on the Settings → Database
  Status tab with the API key controls (Copy / Regenerate / **Clear
  Key (Debug)**).
- The **`Clear Key (Debug)` button** is visibly duplicated in the
  rendered HTML (two consecutive identical `<button id="clear-api-key">`
  elements). Minor UI defect — see Known Issues.
- The **Digest Report Status** info block at the bottom of the
  Settings page.
- The **"Performance Status — ALWAYS ON"** card on the Dashboard.
- **Admin trust** behaviour where a successful admin login marks the
  IP as trusted for `admin_trust_duration`. Mentioned in docs but the
  default 6-hour window is not.

### Naming mismatches between docs, code, and live admin UI

| Concept | Docs / UI label | Code identifier | Notes |
|---------|----------------|----------------|-------|
| Error category | "Error Events" (singular) | `errors` (plural) | Settings keys `sentinel_email_cat_error` map to category `errors`. |
| IP allowlist | "IP Allowlist" / "IP Allowlisting" | `ip_whitelist` field name | Legacy term in code, modern term in UI/docs. |
| "Sentinel+" | Marketing name | `sentinel_is_premium()` | Internal code consistently uses `premium`. |
| Activity Logs page | "Activity Logs" | menu slug `sentinel-logs`, file `logs.php` | OK; just be aware. |
| Action engine modes | "Observe Only / Throttle Threats / Block Threats" in UI | `observe` / `throttle` / `block` in `sentinel_security_settings.action_mode` | UI verbose, code terse. |
| Memory thresholds | "Early warning / Warning / Critical" | event keys `memory_early_warning`, `memory_warning`, `memory_critical` | Aligned. |

### Documentation improvements recommended

(For a later docs pass, not now.)

1. Add a **v1.1.2 email rate limiter** section to the public docs,
   mirroring `docs/Version 1.1.2 Changelog`.
2. Document the **manual maintenance buttons** on Settings → Log
   Management (Run Cleanup Now, Optimize Database Now, Anonymize Logs,
   Retry Failed Logs).
3. Document the **`audit_log_access`** setting and its consequences
   (it produces a `log_accessed` event for every admin view of
   Sentinel pages — surprising if you have multiple admins).
4. Add an explicit **WordPress.org distribution status** note ("The
   plugin is currently distributed only via getsentinel.dev. WP.org
   listing is planned but not live.") so the "Coming Soon" copy
   doesn't get stale.
5. Document the **JS asset map** for developers extending the
   admin UI — `sentinel-admin` is a dependency for
   `sentinel-dashboard` and `sentinel-settings`.

---

## LIVE ADMIN EXPERIENCE NOTES

Captured by signing into the local Sentinel admin at
`https://getsentinel.local/wp-admin` as `admin/admin` (same plugin
codebase, v1.1.3) and walking each Sentinel screen with the
discovery script. See `tools/sentinel-explore/admin-*.png` for
full-page screenshots.

- **First impression.** The dashboard is dense but legible. The brand
  bar at the top with a centred "Sentinel" logo plus a tall page
  title ("Sentinel Dashboard") makes it clear which plugin you are
  in. The three monitor cards across the top are a strong "this is
  what I'm doing right now" summary.
- **Main navigation.** Top-level "Sentinel" menu with a `shield`
  dashicon. Submenu order: **Dashboard → Activity Logs → Incidents
  (Premium only) → Event Registry → Settings**. Verified live on
  every screen — the order is stable across pages.
- **Match with docs.** The page names and Settings tab labels match
  the public docs almost exactly (Dashboard, Activity Logs, Event
  Registry, Settings → Privacy & Security tab → Access Control &
  Security). Good alignment.
- **Settings page UX.** Tabs work but the form is one giant `<form>`
  containing all tabs (hidden via CSS). That's why the **Save
  Settings** button at the bottom saves *every* tab at once — fine
  once you know it, slightly surprising at first.
- **Empty states.** The Activity Logs page handles "no rows" with a
  filtered view; if filters return nothing the table simply
  disappears. Could be improved with a friendly empty state.
- **Dangerous actions.** The maintenance buttons under Log Management
  use neutral grey "secondary" styling. They are **not** confirmed
  with a modal. The "Clear Key (Debug)" button is styled in red
  borders — appropriate. **Run Cleanup Now / Anonymize Logs / Retry
  Failed Logs** should probably have a confirmation modal — they are
  immediate-effect on the database. Watch item.
- **Confusing / mismatched copy.**
  - The Notifications tab has two cards that overlap thematically:
    "Notifications & Alerts" (with a single notification_email
    field) and "Compliance & Monitoring" (with the granular
    real-time / daily / weekly toggles). A single, well-titled card
    would help.
  - The Categories card includes a tile labelled **"Error"** while
    other Sentinel screens use **"Errors"** for the same bucket.
  - "Sentinel+" branding appears as a Premium plan badge in
    Settings → Database Status (`PREMIUM` in red caps), in the
    sidebar menu only when Premium is active (the **Incidents**
    submenu), and in event-registry copy. Consistent enough but
    worth a final naming pass.
- **Helper text.** The Performance and Privacy & Security tabs have
  good description text on every setting, including caveats like
  "Resource intensive" next to real-time file monitoring. The
  Notifications tab description text is sparser; relying on the
  pill labels alone is fine if you already know the plugin.
- **Dashboard widgets on WP Dashboard.** Visible: "Sentinel Memory
  Insights" and "Sentinel+ Incidents" alongside core WordPress and
  Yoast widgets. Useful at-a-glance health summary.

---

## RELEASE / VERSIONING NOTES

- **Current version.** `1.1.3`, declared in:
  - the plugin header (`Plugin Name: Sentinel ...` → `Version: 1.1.3`)
    in `sentinel.php`,
  - `SENTINEL_VERSION` constant defined immediately below the header,
  - `readme.txt` (`Stable tag: 1.1.3`),
  - `docs/Version 1.1.3 Changelog` (this release),
  - `docs/Version 1.1.2 Changelog` (v1.1.2 email rate limiter deep-dive).
- **Database migrations.** Schema changes are handled by
  `Sentinel_Migration::create_tables()` using `dbDelta`, gated by the
  `sentinel_db_version` option. `Sentinel_Migration::check_version()`
  runs on `plugins_loaded` and re-runs `dbDelta` when the stored
  version differs from `SENTINEL_VERSION`.
- **Update flow.** The plugin is currently distributed as a manual zip
  from `getsentinel.dev`. There is no WordPress.org listing yet, and
  no auto-update integration is wired up in code. The licence-key
  validation system is the closest thing to an update channel — it
  could be extended to advertise newer versions in future.
- **Folder name.** WordPress expects the plugin folder to be named
  `sentinel`. The shipped directory in this workspace is
  `wp-content/plugins/sentinel/` (verified).
- **Changelog references.**
  - `readme.txt` — public-facing condensed changelog.
  - `docs/Version 1.1.3 Changelog` — internal notes for the Quick Setup /
    licence UX release.
  - `docs/Version 1.1.2 Changelog` — internal deep-dive (email rate
    limiter).
  - `docs/CODEBASE_ANALYSIS_AND_ROADMAP.md` — version 1.1.0 snapshot
    + roadmap; useful for historical context but not authoritative
    for current behaviour.

---

## KNOWN ISSUES / RISKS / WATCH ITEMS

Each is evidence-based — anchored to code, the admin UI, or docs.

1. **Duplicate `Clear Key (Debug)` button in Settings → Database
   Status.** Two `<button id="clear-api-key">` elements render
   back-to-back (see `tools/sentinel-explore/admin-settings.txt`
   line ~190). Both bind to the same handler so behaviour is
   harmless, but the duplicate `id` is invalid HTML and the visual is
   confusing.
   - Why it matters: HTML validity + small UX confusion.
   - Suggested next step: remove the duplicate from
     `admin/views/settings-page.php`.

2. **Single notification recipient.** `notification_email` is a single
   `<input type="email">`. The public docs FAQ explicitly mentions
   _"Can I send email alerts to multiple addresses?"_ but the UI does
   not support a list.
   - Why it matters: agencies and teams often want shared inboxes.
   - Suggested next step: accept a comma-separated list and
     `array_map('sanitize_email', ...)` it; or surface a clear "single
     address only" hint.

3. **`Error` vs `Errors` naming inconsistency.** Settings checkbox
   `sentinel_email_cat_error` (singular) writes to a setting that
   filters the **`errors`** (plural) event category. Code paths
   reading both string forms have to be careful.
   - Where it appears: `admin/views/settings-page.php` (key
     `sentinel_email_cat_error`), `event-definitions.php` (category
     `errors`).
   - Why it matters: easy to introduce bugs when matching strings.
   - Suggested next step: pick one ("errors") and migrate; keep a
     read-time alias for backwards compatibility.

4. **No "Process Queue Now" admin button.** When batch logging is
   enabled, the queue can only be drained by `sentinel_process_log_queue`
   cron. If cron is disabled or stalled, logs pile up silently.
   - Why it matters: silent data loss on misbehaving hosts.
   - Suggested next step: add a manual "Process Queue Now" action
     under Settings → Log Management, mirroring "Run Cleanup Now".

5. **Maintenance buttons lack a confirmation step.** "Run Cleanup
   Now", "Anonymize Logs" and "Retry Failed Logs" perform
   destructive-ish actions on the database without a confirmation
   modal.
   - Why it matters: easy to misclick.
   - Suggested next step: add `confirm()` or a small JS confirmation
     modal; pair with a toast/notice on completion.

6. **`sentinel_log_queue` is stored as a single option.** Capped at
   1,000 entries by the logger, but on busy sites with cron lag the
   row can grow large and contend with autoload. The plugin
   correctly does not autoload it, but it is worth monitoring.
   - Where it appears: `Sentinel_Logger::add_to_queue` /
     `process_queue`.
   - Why it matters: option size growth + autoload contention.
   - Suggested next step: consider a dedicated queue table if a
     customer site experiences spikes; document the 1,000 cap in
     public docs.

7. **`sentinel_memory_analysis_history` and other option-stored
   histories.** Same shape: bounded by code, but unbounded if the
   bound is ever loosened.
   - Why it matters: option bloat.
   - Suggested next step: validate the size cap on each release and
     consider moving to transients with TTLs.

8. **WordPress.org "Coming Soon" copy** in the docs Installation
   section. If WP.org listing is delayed, this copy needs a refresh.
   - Why it matters: outdated marketing copy.
   - Suggested next step: rephrase to "Currently distributed via
     getsentinel.dev download — WP.org listing planned" and link the
     repo when ready.

9. **`audit_log_access` is "off by default" but very chatty when
   on.** Every admin view of any Sentinel page writes a
   `log_accessed` event including the full `$_GET` as data. On busy
   admin sessions this can dominate the log volume.
   - Where it appears: `admin/views/logs.php` lines 22-32 (and
     similar in other views if you turn the setting on).
   - Why it matters: log noise + potential leakage of query
     parameters.
   - Suggested next step: filter or whitelist the captured query
     keys; clarify the helper text.

10. **`SENTINEL_SITE_LICENSE_KEY` define** — bypass for
    `getsentinel.dev` itself. Documented in `sentinel.php` but only
    visible to code readers.
    - Why it matters: developers running a forked dev copy at the
      production hostname could inadvertently trip premium-only
      paths.
    - Suggested next step: keep the define gated to
      `parse_url(home_url())['host'] === 'getsentinel.dev'` and
      mention it in onboarding docs (or remove if no longer needed).

11. **Cron timing for digest emails depends on local time.** The
    `tomorrow_9am` / `next_monday_9am` calculation uses
    `wp_timezone()` / WP date functions, so misconfigured site
    timezones can shift the digest window.
    - Why it matters: support tickets like "my digest arrived at 2am".
    - Suggested next step: surface the next-fire timestamp on the
      Settings → Notifications page (it currently only shows the
      enabled/disabled state).

12. **No CSRF token on the GET-based `?action=cleanup_now` /
    `?action=optimize_now` / `?action=anonymize_logs` /
    `?action=retry_failed_logs` links** at the bottom of Settings →
    Log Management.
    - Where it appears: `admin/views/settings-page.php` (the inline
      `<a href=".../wp-admin/admin.php?page=sentinel-settings&action=cleanup_now">`).
    - Why it matters: cap-gated to `manage_options`, but
      cap + GET-with-side-effects without nonce is a long-standing
      WordPress smell.
    - Suggested next step: append `wp_nonce_url(...)` to each link
      and verify it on the receiving side.

---

## OPEN ITEMS / FUTURE IDEAS

These are bigger or more speculative than the watch items above.
Tagged **(suggestion)** — none are confirmed bugs.

- **(suggestion) Multi-site / network support.** Currently the plugin
  is wired for single-site. A network-aware mode (per-blog tables or
  a shared network table) would help agencies.
- **(suggestion) Webhooks / outbound integrations.** Sentinel already
  has a REST API for inbound queries; outbound webhooks
  (Slack / Discord / generic POST) on certain event categories would
  pair well with the existing email pipeline.
- **(suggestion) Structured export for SIEM.** A streaming JSON Lines
  export endpoint scoped to a time window would make ingestion into
  external SIEMs straightforward.
- **(suggestion) UI for the v1.1.2 rate-limiter knobs.** Surface
  `email_per_event_cooldown` and `email_hourly_cap` on Settings →
  Notifications instead of forcing them through
  `sentinel_security_settings`.
- **(suggestion) "Dry run" mode** for the Action Engine's `block`
  option, surfaced in the Security Response Mode segmented toggle.
- **(suggestion) Dashboard widget condensing.** Combine "Sentinel
  Memory Insights" and "Sentinel+ Incidents" into a single widget
  with tabs to reduce WP Dashboard clutter.
- **(suggestion) Public docs anchor for v1.1.2 email throttling.**
  The internal changelog is excellent — promote a condensed version
  to `getsentinel.dev/docs/`.
- **(suggestion) Per-URL hotspot detection** (already in
  `docs/CODEBASE_ANALYSIS_AND_ROADMAP.md` as a future idea — still
  not implemented).

---

## FINAL CHECKLIST

A short, practical checklist for the next developer or AI agent
touching Sentinel:

**Before editing the logging system:**
- [ ] Re-read `Sentinel_Logger::log()` end to end — recursion guard,
      memory guard, rate-limiter, queue path.
- [ ] Confirm whether the change should respect the `critical` bypass.
- [ ] Run a quick read-only test by reproducing the event you care
      about and checking the Activity Logs page.

**Before editing the database schema:**
- [ ] Bump `SENTINEL_VERSION`.
- [ ] Update `Sentinel_Migration::create_tables()` (uses `dbDelta` —
      stick to its formatting rules).
- [ ] Make `check_version()` idempotent on re-runs.
- [ ] Update `uninstall.php` if you introduce a new table or option.

**Before editing the admin UI:**
- [ ] Decide which capability the page or AJAX endpoint requires
      (`edit_published_posts` for content-side, `manage_options` for
      everything else).
- [ ] Add a nonce — pick from the existing ones unless you really need
      a new one.
- [ ] Use the `sentinel-*` CSS namespace.
- [ ] Watch the JS dependency order (`sentinel-admin` →
      `sentinel-dashboard` / `sentinel-settings`).

**Before editing notification emails:**
- [ ] Make sure the event passes the rate limiter
      (`sentinel_email_rate_limiter()`).
- [ ] Confirm Pathway A vs Pathway B selection is still mutually
      exclusive.
- [ ] Update the relevant section of `Sentinel_Email_Templates`.
- [ ] Verify daily/weekly digests are not double-counting if you
      add a new event to a digest section.

**Before editing public docs:**
- [ ] Decide whether the change belongs in `getsentinel.dev/docs/`
      (user-facing) or in `docs/` inside the plugin (internal).
- [ ] If naming changes, update both — and add a note to this
      `SENTINEL-REFERENCE.md`.

**Before shipping a release:**
- [ ] Update `SENTINEL_VERSION`, plugin header version, and
      `readme.txt` stable tag in lockstep.
- [ ] Add a `docs/Version X.Y.Z Changelog` entry for non-trivial
      releases.
- [ ] Update the changelog block in `readme.txt`.
- [ ] Run `node scripts/sentinel-explore.mjs` against the local
      install and skim the resulting dumps to catch regressions in
      admin pages.
- [ ] If a setting key changed, write a one-shot migration in
      `Sentinel_Migration::check_version()`.
- [ ] Tag the release in git and rebuild the distribution zip from
      a clean checkout (no `node_modules`, no `.env`, no
      `tools/sentinel-explore/`).
