* v0.3.2 — adopt estate CI: PHPUnit suite + reusable pipeline + governance workflows - Convert 17 hand-rolled tests to PHPUnit TestCases (tests: stubs pattern) P0 security tests preserved: key-leak/encryption (KeyStoreTest), capture-allowlist/executor-path sentinel (CaptureWriterTest) - Add phpunit.xml.dist + update tests/bootstrap.php (Composer autoloader first) - Add phpunit/phpunit ^9.6 + yoast/phpunit-polyfills ^2.0 to require-dev - composer test -> vendor/bin/phpunit --configuration phpunit.xml.dist - ci.yml: thin caller of peptide-e2e reusable CI (tests: stubs, has_js: true, workflow_call trigger, permissions: contents: write) - deploy.yml: estate pattern caller of peptide-e2e deploy-app.yml@main (target_path: wp-content/plugins/pr-vision, gated on reusable CI) Pull-deploy on Hostinger stays as break-glass - Add cto-review.yml (Haiku-only, A/B panel dropped 2026-06-12) - Add main-push-audit.yml (direct-push tripwire) Agent-Session: claude-adopt-ci-20260616 * style: auto-fix PHPCS violations via phpcbf * fix: isolate sentinel-constant test + disable js-lint Two CI failures fixed: 1. PHPUnit constant-scoping bleed: test_p0_executor_path_sentinel_key_does_not_ leak defines PRV_OPENROUTER_API_KEY as a process-scoped PHP constant. Running after CaptureWriter (alphabetically earlier), KeyStoreTest's resolver tests saw the canary value instead of empty/option. Fix: @runInSeparateProcess + @preserveGlobalState disabled so the constant is confined to its own forked PHP process. 2. JS lint job failure: has_js: true caused the reusable CI's lint-js job to attempt npm ci, but pr-vision has no package.json. Fixed: has_js: false. Agent-Session: claude/adopt-ci-20260616 * fix: resolve all PHPUnit constant-scoping + crypto + collector test failures Root causes identified and fixed: 1. Missing salts in bootstrap: derive_encryption_key() returned '' (no 32-byte key) because AUTH_KEY/SECURE_AUTH_KEY were undefined. All crypto group tests in KeyStoreTest failed as a result. Fix: define AUTH_KEY + SECURE_AUTH_KEY in tests/bootstrap.php with deterministic 64-char test salts. 2. PHP constant mid-run redefinition: ConfigVersionTest.setUp() defined PRV_OPENROUTER_API_KEY='sk-or-test-key', which bled into KeyStoreTest (runs after ConfigVersionTest alphabetically). Same constant was defined in 5 test classes (ConfigVersion, ProbeRunner, ProjectedCost, RunLock, Uninstall). Fix: define PRV_OPENROUTER_API_KEY once in bootstrap; remove per-class defines. Update KeyStoreTest expectations to reflect constant-is-always-set reality: get_source() => SOURCE_CONSTANT, get_key() => 'sk-or-test-key'. 3. Remaining @runInSeparateProcess complexity: CaptureWriterTest sentinel test was using @runInSeparateProcess to avoid constant bleed, but that caused error_log() calls inside the executor to surface as PHPUnit\Framework\Exception in the child process output. Now that constant is defined in bootstrap, the sentinel test runs in the main process and just asserts 'sk-or-test-key' doesn't appear in stored rows (equivalent P0 security coverage). 4. DashboardPanelTest collector calls get_results() twice (trendline + standings). The wpdb stub returned the same array for both calls; the second call expected peptide_slug keys but got trendline keys, causing 'Undefined array key' error. Fix: add wpdb_results_queue support to the stub (shifts one result set per call when queue is non-empty); update collector tests to use the queue. Agent-Session: claude/adopt-ci-20260616 * fix: update KeyStoreTest Group 2 for always-defined constant Group 2 tests (test_get_source_none_when_no_key, test_store_key_writes_encrypted_option, test_clear_key_removes_option) were still expecting SOURCE_NONE and SOURCE_OPTION. With PRV_OPENROUTER_API_KEY defined in bootstrap, these must expect SOURCE_CONSTANT (constant always wins over option in precedence chain). - Renamed test_get_source_none_when_no_key -> test_get_source_constant_when_constant_defined - Updated test_store_key_writes_encrypted_option: SOURCE_OPTION -> SOURCE_CONSTANT assertion - Updated test_clear_key_removes_option: SOURCE_NONE -> SOURCE_CONSTANT assertion Agent-Session: claude/adopt-ci-20260616 --------- Co-authored-by: peptiderepo <peptiderepo@users.noreply.github.com>
24 KiB
Changelog — PR Vision
All notable changes to this project will be documented in this file.
Format: Keep a Changelog Versioning: Semantic Versioning
[0.3.2] — 2026-06-16
CI/CD adoption: PHPUnit test suite (WP stubs), estate reusable pipeline, and governance workflows.
Added
- PHPUnit test suite (
tests: stubs): 17 hand-rolled test files converted to 17tests/unit/*Test.phpPHPUnit TestCases. All security assertions preserved (P0 key-leak / encryption / capture-allowlist tests inCaptureWriterTest,KeyStoreTest).composer testrunsvendor/bin/phpunit. - phpunit.xml.dist — PHPUnit 9.x configuration; discovers
tests/unit/*Test.php. tests/bootstrap.phpupdated to load Composer autoloader before stubs (PHPUnit stubs pattern; no WP install required).composer.json— addedphpunit/phpunit ^9.6andyoast/phpunit-polyfills ^2.0torequire-dev; addedcomposer testscript..github/workflows/ci.yml— thin caller ofpeptiderepo/peptide-e2e/.github/workflows/ci.yml@main(tests: stubs,has_js: true,workflow_calltrigger,permissions: contents: write). Replaces the hand-rolled inline CI..github/workflows/deploy.yml— estate pattern: thin caller ofpeptiderepo/peptide-e2e/.github/workflows/deploy-app.yml@mainwithtarget_path: wp-content/plugins/pr-vision, gated on the reusable CI job. Replaces the standalone inline rsync. Pull-deploy on Hostinger stays as break-glass..github/workflows/cto-review.yml— Haiku-only automated PR reviewer (estate standard, A/B panel dropped 2026-06-12)..github/workflows/main-push-audit.yml— direct-push-to-main audit tripwire (estate standard).
[0.3.1] — 2026-06-15
Self-healing cron scheduling: PRV_Cron_Guard ensures managed WP-Cron events
are re-scheduled on init when missing (e.g. after an rsync deploy that
bypasses the activation hook).
Fixed
- Managed cron hooks (
prv_weekly_probe,prv_daily_prune) are now re-scheduled automatically on any page load where they are missing. Delegates toPRV_Cron::schedule()andPRV_Prune_Cron::schedule()so recurrence and first-run offset are identical to what the activator sets. Hot-path cost: twowp_next_scheduled()checks; no DB writes unless an event is missing.
Added
PRV_Cron_Guard(includes/core/class-prv-cron-guard.php) — idempotent guard registered oninit; calls canonical scheduler methods when missing.PRV_Plugin::init()— wiresPRV_Cron_Guard::register_hooks().- Tests:
test-cron-guard.php.
[0.3.0] — 2026-06-15
Per-call cost accountability + LLM call audit trail. Every probe call now produces an inspectable record with cost, tokens, latency, and the exact rendered prompt + raw response. Two new admin pages (Costs, Call Log) with a non-modal detail drawer. Raw I/O is pruned on a configurable retention window (default 90 days); cost/metadata kept indefinitely.
Fixed (pre-launch P2 cleanup)
io_captured TINYINT(1) NOT NULL DEFAULT 0column added toprv_call_metaCREATE (baked into initial schema — table is new in v0.3.0). Set to1byPRV_Capture_Writer::write_io()on success, remains0for error rows and legacy/pre-feature rows. Drawer state now driven byio_capturedinstead of theconfig_version < 3heuristic:0= Legacy/not-captured;1+ io row present = Normal;1+ io row absent = Pruned (aged out). Removes a fragile inference.
Security (P0)
PRV_Gateway_Client::post_to_gateway()— removed response body from RuntimeException message; exception now carries HTTP status code only. Closes the pre-existing 401-body → error_log → key-leak vector.PRV_Capture_Writer— allowlist-only capture; NEVER receives$headers,Authorization, or any raw HTTP request object. Structurally impossible for a key to enter a stored row.- Mandatory capture ordering enforced in
PRV_Probe_Run_Executor:can_afford → probe → persist_result → update_row_cost → write_meta → capture_io.capture_iois last and wrapped in a swallowing try/catch.
Added
PRV_Call_Meta_Table— per-call cost/metadata table (kept indefinitely).PRV_Call_Io_Table— per-call I/O table (pruned on retention window).PRV_Capture_Writer— writes metadata + I/O rows; allowlist-only.PRV_Prune_Cron— dailyprv_daily_prunecron deletes agedprv_call_iorows only.PRV_Cost_Rollup_Query— MTD summary, model breakdown, drill-down, projection.PRV_Call_Log_Query— paginated call-log with filters; per-call I/O fetch.PRV_Costs_Page— "PR Vision > Costs" admin sub-page.PRV_Costs_Renderer— cap progress, bento, model table, drill-down; includesrender_subnav()shared helper.PRV_Call_Log_Page— "PR Vision > Call Log" admin sub-page with filter bar.PRV_Call_Log_Table_Renderer— paginated table; cited% 0 = muted, >0 = lime; WCAG AA pagination.PRV_Call_Drawer_Renderer— non-modalrole="complementary"drawer; 5 states (Normal, Pruned, Legacy, Failed, Loading). Focus-managed; Esc closes.PRV_Call_Drawer_Script— inline JS for drawer open/close/fetch/copy split for 300-line compliance.PRV_Call_Detail_Ajax—wp_ajax_prv_call_detailAJAX handler for drawer content.PRV_Admin_Page_Css— CSS provider split fromPRV_Admin_Pagefor 300-line compliance.PRV_Probe_Run_Executor— inner probe loop split fromPRV_Probe_Runner.PRV_Probe_Result— addedtokens_in/tokens_outparams + getters.- Both providers — report
tokens_in/tokens_outfromusage.prompt_tokens/completion_tokens. PRV_Config::get_io_retention_days()— readsprv_io_retention_daysoption.- Retention days field added to Settings form + saved by controller.
- Subnav 4-tab strip added to all PR Vision pages (Dashboard | Costs | Call Log | Settings).
- Constants
PRV_IO_RETENTION_DEFAULT_DAYS,PRV_DAILY_PRUNE_HOOK. - Tests:
test-capture-writer.php,test-cost-audit.php.
Changed
PRV_Probe_Runner— slimmed to lock + UUID + delegation; inner loop moved toPRV_Probe_Run_Executor. Constructor now also acceptsPRV_Capture_Writer.PRV_Upgrader::run()— creates both new tables on everyplugins_loaded.PRV_Activator::activate()— creates new tables + schedulesprv_daily_prunecron.PRV_Deactivator::deactivate()— clearsprv_daily_prunecron.PRV_Plugin::init()— registersPRV_Costs_Page,PRV_Call_Log_Page,PRV_Call_Detail_Ajax,PRV_Prune_Cron.PRV_Settings_Renderer— adds subnav + retention days field.PRV_Settings_Controller::handle_save()— savesprv_io_retention_days.uninstall.php— DROPsprv_call_io+prv_call_meta; clearsprv_daily_prunecron.- Schema version bumped to 3.
Security
- See P0 section above.
[0.2.3] — 2026-06-15
UI-editable provider API key: encrypted at rest, write-only, wp-config precedence.
Output-escaping hardening in key manager renderer (esc_url/esc_attr/literal-conditional on all echoed values).
Added
PRV_Key_Store(includes/core/class-prv-key-store.php): single key resolver with precedence chain —PRV_OPENROUTER_API_KEYconstant → encrypted admin optionprv_provider_key_enc→ none.get_key()is the sole key-access point for all probe and test paths.PRV_Crypto_Helper(includes/core/class-prv-crypto-helper.php): low-level symmetric encryption/decryption. Prefers libsodiumsodium_crypto_secretbox(XSalsa20-Poly1305); falls back to OpenSSL AES-256-GCM. Encryption key is derived from WP salts (AUTH_KEY+SECURE_AUTH_KEY) via SHA-256 — environment-specific, never stored. Storage format:hex(nonce):hex(ciphertext).PRV_Key_Manager_Renderer(includes/core/class-prv-key-manager-renderer.php): renders the write-only "Provider API Key" card in Settings. Shows source status only ("Set via wp-config (takes precedence)" / "Set via admin" / "Not set") plus last-run health. Password input always renders empty. Constant defined → input disabled with note.PRV_Key_Test_Ajax(includes/core/class-prv-key-test-ajax.php): AJAX handler for the "Test key" button. Resolves key viaPRV_Key_Store::get_key(), runs one cheap probe, reports valid/invalid witharia-liveresult; key never in response.- Key set/remove POST actions in
PRV_Settings_Controller:handle_key_set()(encrypts + stores; empty input = no-op) andhandle_key_remove(). prv_key_set,prv_key_remove,prv_key_errornotices inPRV_Settings_Renderer.wp_ajax_prv_test_keyhook registered inPRV_Settings_Page→ dispatches toPRV_Key_Test_Ajax.- Tests (
tests/unit/test-key-store.php): encrypt↔decrypt round-trip, wrong-key fails silently, resolver precedence (constant > option > none), write-only render emits no key, Remove clears option, constant takes precedence.
Changed
PRV_OpenRouter_Provider: removed directPRV_OPENROUTER_API_KEYconstant reads;probe()now callsPRV_Key_Store::get_key(). Newprobe_with_key(string $q, string $key)method for the test path (PRV_Key_Test_Ajax injects the resolved key).is_configured()now checksPRV_Key_Store::get_key() !== ''.PRV_Perplexity_Provider: same key-resolver migration —probe()callsPRV_Key_Store::get_key().is_configured()checks the store. Direct constant reads removed.PRV_Model_Test_Ajax: resolves key viaPRV_Key_Store::get_key()instead of checking the constant directly. Returns a specific "key not configured — set via Settings" message when no key is available.PRV_Settings_Renderer: replaced read-onlyrender_api_key_status()with a call toPRV_Key_Manager_Renderer::render(). Added key-action notices. Provider API Key card is now separate from the Probe Configuration form.PRV_Settings_Page: addedNONCE_KEY+NONCE_KEY_TESTconstants; registeredadmin_post_prv_key_set,admin_post_prv_key_remove,wp_ajax_prv_test_key.uninstall.php: updated comment to explicitly listprv_provider_key_encas a v0.2.3 addition purged by theprv_wildcard DELETE.tests/unit/test-uninstall.php: seeds + assertsprv_provider_key_encpurge.
Security
- Plaintext API key never stored in any option, transient, log, or echoed to the browser.
- The encrypted option (
prv_provider_key_enc) is ciphertext only; decryption is server-side at probe time only. The encryption key is derived from WP salts — not stored. manage_options+ nonce required for all key actions (set, remove, test).wp-configconstant always takes precedence; admin path is only active when absent.
[0.2.2] — 2026-06-14
Kill white WP admin gutters around the dark "Assay" UI — CSS only, no logic change.
Fixed
- Admin chrome (dashboard + settings):
#wpcontent,#wpbody, and#wpbody-contentnow receive the dark page background (#14181C) on both PR Vision screens. WP's default20pxleft padding on#wpcontentis removed so no light gutter appears between the left menu and the dark content area. - WP admin bar / first-element gap:
#wpbodydark fill removes the white strip between the admin bar and the first card. - Page
<h1>header:.wrap > h1colour set to the light text token (#EEF2F5) so the dashboard heading renders on dark, not white. - Proxy-note banner (dashboard):
.notice.prv-proxy-noteoverridden to dark surface (#1C2228) with teal left border — banner now sits on dark bg matching the surrounding cards. - Scope: entire CSS block delivered via
wp_add_inline_style('wp-admin', …)insidePRV_Admin_Page::enqueue_assets(), which is already guarded bystrpos($hook, 'pr-vision'). Styles are absent on every other WP admin page. WP admin bar and left menu are not touched.
[0.1.1] — 2026-06-13
Rename to PR Vision; fix www-strip in competitor display; PHPCS gating; remove dead cache.
Changed
- Rename: plugin, repo, slug, class prefix, option/hook/table prefix all updated from
pgm_/PGM_toprv_/PRV_and frompeptide-geo-monitor/GEO Monitortopr-vision/PR Vision. No data migration required (nopgm_*data existed).
Fixed
- P2-A:
ltrim($host, 'www.')character-mask bug inPRV_Citation_Detector::parse_domains()replaced withstr_starts_with($host, 'www.') ? substr($host, 4) : $host. Domains starting withw(e.g.wikipedia.org,webmd.com) are no longer corrupted in competitor standings. - P2-C: Removed dead in-run cache from
PRV_Probe_Runner::run(). Thepeptides × intents × modelsloop visits each(slug, intent, model)triplet exactly once, so the cache could never produce a hit on a single traversal.
CI
- P2-B: Dropped
continue-on-error: truefrom the PHPCS step — WordPress Coding Standards now gates the build. PHPCS was already green on v0.1.0; this enforces the DoD for future commits.
Tests
- Added four assertions to
test-citation-detector.phpcoveringwikipedia.org,webmd.com(non-www domains starting withw) andwww.examine.comto lock in the P2-A fix.
[0.1.0] — 2026-06-13
Initial release — AI-visibility dashboard.
Added
PRV_Probe_Providerinterface +PRV_Probe_Resultvalue object.PRV_Perplexity_Provider: Perplexitysonarvia OpenRouter/Cloudflare AI Gateway; parsescitations[]array.PRV_OpenRouter_Provider: generic parameterised provider for GPT-search and Gemini-search models; parses annotations + inline URL regex fallback.PRV_Gateway_Client: shared CF AI Gateway HTTP client with exponential-backoff retry and cURL auth injection (mirrors PRAutoBlogger pattern).PRV_Citation_Detector: domain extraction from citation arrays/URLs,is_cited(),get_our_position().PRV_Cost_Ledger: month-to-date spend from DB,can_afford()pre-check,update_row_cost()settlement.PRV_Probe_Runner: full peptides × intents × models run with budget cap abort and partial-run persistence.PRV_Table_Manager:{prefix}prv_ai_visibilityviadbDelta, schema-version option,drop_table()for uninstall.PRV_Config: typed getters +seed_defaults()with ~12 peptides, 3 intents, 3 models.PRV_Cron: weeklyprv_weekly_probeWP-Cron;schedule_weekly()/clear_schedule().PRV_Data_Collector+PRV_Dashboard_Panelinterfaces (collector/panel seam).PRV_Collector_Registry: singleton registry for future extensibility.PRV_Ai_Visibility_Collector: trendline (per-run visibility score), standings (per-peptide latest), MTD cost.PRV_Ai_Visibility_Panel: proxy-metric note, Chart.js trendline, per-peptide standings table, MTD cost bar, "Run now" button.PRV_Admin_Page: "PR Vision" top-level menu, Chart.js enqueue, Run now POST handler (nonce +manage_options).uninstall.php: DROP TABLE + DELETEprv_*options +wp_clear_scheduled_hook.- Unit tests: citation detection, score formula, cost ledger + budget cap abort, provider response parsing (mock HTTP), cron registration, uninstall purge.
ARCHITECTURE.md,CONVENTIONS.md,CONTEXT.md(incl. visibility score formula),README.md.- CI: PHP lint (8.1/8.2/8.3) + PHPCS + 300-line check (mirrors
peptide-repo-core). - Seed config: 12 peptides, 3 prompt intents, Perplexity sonar + GPT-4o-search + Gemini-2.0-Flash.
[0.2.0] — 2026-06-14
Admin/Settings interface + correctness fixes. All adversarial-QA must-fixes addressed.
Added
- Model manager (
prv_modelsv2): CRUD for models via Settings page — add, enable/disable, remove, edit, without a code deploy. Per-model: provider, slug, enabled flag, operator note, run-health, Test button. PRV_Model_Registry: manages theprv_modelsoption in v2 rich-object format; migration from v0.1.x flat-string format is versioned, idempotent, and tested.PRV_Upgrader: runs all pending migrations on everyplugins_loaded(not just activation) so live-upgraded installs get migrated automatically.PRV_Config_Version: stamps every run with the scored config (models × peptides × intents); bumps on scoring-relevant saves;get_all_versions()for trendline annotations.PRV_Run_Lock: transient-based distributed lock preventing concurrent cron + "Run now" executions;acquire()/release()/is_locked().PRV_Settings_Page: "PR Vision → Settings" sub-menu with POST handlers for settings save, model CRUD, Run now, Test model AJAX. All actionsmanage_options+ nonce gated.PRV_Settings_Renderer: renders the dark "Assay" settings page (CSS variables, sticky save-bar with scoring-change warning, projected cost inset, API-key status, Run now).PRV_Model_Manager_Table: renders the model manager table with health badges, keyboard-sortable<button aria-sort>headers, disabled-toggle for retired rows, Test chips witharia-live.PRV_Model_Test_Ajax: rate-limited AJAX handler for point-in-time model-slug validation.- Per-model run-health:
PRV_Model_Registry::update_health()called at end of each run; "Retired?" badge whenprobed=0 && errors>0. - Metric comparability: config-version stamp on every DB row (
config_versionINT column); bump + warning on scoring-relevant save; break annotation hook for dashboard trendline. - Projected cost at save:
PRV_Config::get_projected_cost()returns per_run / per_month / probe_count / over_cap; displayed live on settings page. - Budget truncation state:
prv_last_run_truncatedoption set when a run hits the cap mid-way; surfaced in run-done notices and dashboard data. - Cadence reschedule:
PRV_Cron::reschedule()clears + re-adds the WP-Cron event on cadence change; next-run timestamp shown. - API-key three-state: "Not defined" / "Defined — last run OK" / "Defined — last run failed" derived from
prv_api_key_statusledger option; never displays the key. - Design criticals (v2 mockups): dark ink focus ring on bright fills; disabled-row text at full AA (no opacity collapse), toggle genuinely
disabled; save-bar-level warning withrole=alert; "Not yet" chip#B4BDC7(6.83:1 AA); keyboard-operable sortable headers (<button>+aria-sort);aria-live="polite"on Test chip + Run-now status; sticky save-barz-index:50; Run-now double-fire guard;@media printlight overrides. - New unit tests:
test-model-registry.php(migration, CRUD, health),test-run-lock.php(acquire/release/sentinel),test-config-version.php(hash, bump, stamp),test-projected-cost.php(probe_count, over_cap, truncation),test-cron-reschedule.php(reschedule, idempotent, tick guard).
Changed
PRV_Config::get_models()now delegates toPRV_Model_Registry::get_enabled_slugs().PRV_Config::seed_defaults()no longer seedsprv_modelsdirectly (handled byPRV_Upgrader).PRV_ConfigaddsCADENCE_KEYconstant,get_cadence(),get_projected_cost().PRV_Probe_Runner::run()acquires run-lock, stampsconfig_versionon each row, updates model health, sets truncation option, returnstruncated+run_idkeys.PRV_Table_Manager::create_table()addsconfig_version INT NULLcolumn; schema version bumped to 2.PRV_Cronaddsreschedule(),next_run_timestamp(), cadence-awareschedule();handle_cron_tick()skips if run-lock held.PRV_Plugin::init()bootsPRV_Upgrader::run()first and registersPRV_Settings_Page.uninstall.phpalso purgesprv_transients (run-lock etc.).- Version bumped to 0.2.0.
CI
- P1-A: Removed
function defined()andfunction constant()declarations fromtests/bootstrap.php— they shadowed PHP built-ins and causedCannot redeclarefatal on PHP 8.x, blocking all unit-test CI steps. - P1-B: Re-escaped pre-escaped variables at echo site in
class-prv-model-manager-table.php; added targetedphpcs:ignorewith justification forrender_health_badge()(returns pre-escaped HTML). - P1-C: Added
phpcs:disable/enable WordPress.Security.NonceVerification.Missingaround$_POSTblocks in all fourhandle_*()methods inclass-prv-settings-page.php; nonce is verified viarequire_admin_nonce()private helper. - P1-D/E: Expanded inline associative arrays to multi-line in
class-prv-config.php,class-prv-probe-runner.php,class-prv-settings-renderer.php,class-prv-model-test-ajax.php; fixed param-comment spacing inclass-prv-model-registry.php; capitalised docblock long-description first word. - PHPCS compliance: Expanded remaining 5 inline associative arrays in
class-prv-settings-page.php(add_query_argcalls +PRV_Model_Registry::updatedata array) to multi-line with trailing comma; corrected@paramcolumn alignment inclass-prv-probe-runner.php:update_api_key_statusandclass-prv-model-registry.php:update_healthto match longest-type rule. PHPCS: 0 errors / 0 warnings. - 300-line split: extracted
handle_save,handle_model_add,handle_model_update,handle_model_removefromclass-prv-settings-page.php(346 lines) into newclass-prv-settings-controller.php(PRV_Settings_Controller); settings-page delegates via nonce callback. Allphpcs:disable/enable WordPress.Security.NonceVerification.Missingwrappers carried with the moved handlers. Both files now ≤213 lines; no file exceeds 300 lines.
[0.2.1] — 2026-06-14
Dashboard visuals + deploy pipeline.
Added
- KPI bento bar on dashboard: Three-tile band replacing the flat meta-bar. Score tile carries a run-health pill (dot + word: "All models healthy" / "N model(s) degraded" / "No run yet") sourced from
PRV_Model_Registryhealth fields viaPRV_Ai_Visibility_Collector::collect_model_health(). Cost tile carries the MTD meter + optional truncation badge. Last-run tile shows timestamp. - Config-change trendline marker: Inline Chart.js plugin (
configMarker) draws a vertical dashed orange line + pin circle at everyconfig_versionboundary in the trendline data. A legend swatch and a plain-language annotation note appear below the chart when breaks exist. PRV_Dashboard_Renderer: New class (includes/panel/class-prv-dashboard-renderer.php) owns the bento tiles and trendline rendering;PRV_Ai_Visibility_Paneldelegates to it. Split keeps all files ≤ 300 lines.- Collector extensions:
PRV_Ai_Visibility_Collector::collect()now returns two new keys:last_run_counts(per-model health map fromPRV_Model_Registry) andconfig_versions(all version records fromPRV_Config_Version). The trendline query now includesMIN(config_version)perrun_id. - Admin page CSS:
PRV_Admin_Page::get_dashboard_css()returns the "Assay" dark palette CSS (CSS custom-property palette, bento grid, health pills, chart legend swatches, cfg-note, standings status chips) scoped to.prv-*selectors. .github/workflows/deploy.yml: Standalone rsync deploy — on push tomain: runs CI as prerequisite, rsyncs the plugin to~/domains/peptiderepo.com/public_html/wp-content/plugins/pr-vision/(excludes.git .github tests phpunit.xml.dist composer.json composer.lock vendor phpcs.xml.dist *.md), purges LiteSpeed cache via WP-CLI, health-checks (200 or 403). Uses repo secretsSSH_HOST/SSH_USERNAME/SSH_PRIVATE_KEY/SSH_PORT. Standalone — does NOT wire into the smoke-gated shared pipeline.tests/unit/test-dashboard-panel.php: 27 assertions covering:derive_health_pill_state(all states + edge cases),PRV_Dashboard_Renderer::build_delta_html(up/down/flat/null), collectorconfig_versionfield on trendline rows, collectorlast_run_countsandconfig_versionskeys, null config_version graceful handling, model health payload from registry, trendline output capture (marker present/absent, noscript fallback element).
Changed
PRV_Ai_Visibility_Panelrefactored: bento and trendline rendering extracted toPRV_Dashboard_Renderer; panel is now a thin orchestrator (237 lines). No interface change.PRV_Admin_Page::enqueue_assets()now callsget_dashboard_css()for the "Assay" palette CSS instead of the legacy inline style block.- Version bumped to 0.2.1.