PR Vision v0.2.0: admin/settings interface + correctness fixes (#3)

Model manager (UI-swappable models) + versioned/idempotent prv_models migration; metric comparability (config-version stamping); run-lock; projected cost + truncation state; per-model run-health; cadence reschedule; API-key 3-state; design/a11y fixes (dark v2).

QA: APPROVE-WITH-P2-NOTES — qa-reviews/pr-vision/2026-06-14-ee4371d9163f992f60e09c7e4ac6cf8cd1d3c5eb.md

Agent-Session: pr-vision admin v0.2.0 merge
This commit is contained in:
peptiderepo
2026-06-14 14:48:25 +07:00
committed by GitHub
parent 401077fe45
commit 2fcef7a323
28 changed files with 2923 additions and 388 deletions

View File

@@ -1,14 +1,15 @@
# Architecture — PR Vision
**Version:** 0.1.1 | **Last updated:** 2026-06-13
**Version:** 0.2.0 | **Last updated:** 2026-06-14
---
## 1. Overview
`pr-vision` is an internal WordPress plugin that runs weekly server-side LLM probes to measure whether peptiderepo.com appears in AI citations across core peptide queries. It stores a time-series in a custom table and renders an admin dashboard.
`pr-vision` is an internal WordPress plugin that runs configurable server-side LLM probes to measure whether peptiderepo.com appears in AI citations across core peptide queries. It stores a time-series in a custom table and renders an admin dashboard (trendline + standings) and a full Settings page (model manager + probe config).
**v1 scope:** AI-visibility only. The collector/panel seam is present so future data categories (keyword rankings, technical-SEO) can be added as new classes.
**v1 scope:** AI-visibility only. The collector/panel seam is present so future data categories can be added as new classes.
**v2 additions:** Model manager CRUD (no deploy needed), config-version tracking, run-lock, projected cost, API-key three-state, dark "Assay" Settings UI.
---
@@ -17,23 +18,32 @@
```
pr-vision/
├── pr-vision.php # Plugin boot: constants, autoloader, hooks
├── uninstall.php # Full data teardown: DROP TABLE + DELETE prv_* options
├── composer.json # Dev deps (PHPCS only)
├── phpcs.xml.dist # PHPCS config (WordPress standard)
├── uninstall.php # Full data teardown: DROP TABLE + DELETE prv_* options + transients
├── composer.json # Dev deps (PHPCS only)
├── phpcs.xml.dist # PHPCS config (WordPress standard)
├── includes/
│ ├── core/
│ │ ├── class-prv-autoloader.php # SPL autoloader: PRV_* → includes/**
│ │ ├── class-prv-plugin.php # Orchestrator: boots sub-systems
│ │ ├── class-prv-autoloader.php # SPL autoloader: PRV_* -> includes/**
│ │ ├── class-prv-plugin.php # Orchestrator: boots sub-systems + upgrader
│ │ ├── class-prv-upgrader.php # [v0.2] Runs all migrations on every plugins_loaded
│ │ ├── class-prv-activator.php # Activation: table + defaults + cron
│ │ ├── class-prv-deactivator.php # Deactivation: clear cron
│ │ ├── class-prv-table-manager.php # dbDelta create/drop + get_table_name()
│ │ ├── class-prv-config.php # Typed getters + seed_defaults()
│ │ ├── class-prv-cron.php # Weekly WP-Cron schedule/clear
│ │ ├── class-prv-table-manager.php # dbDelta create/drop + config_version column (v0.2)
│ │ ├── class-prv-config.php # Typed getters + seed_defaults() + get_projected_cost()
│ │ ├── class-prv-model-registry.php # [v0.2] prv_models CRUD + v1->v2 migration + health
│ │ ├── class-prv-config-version.php # [v0.2] Config-version stamps + bump on change
│ │ ├── class-prv-run-lock.php # [v0.2] Transient-based run-lock (cron + Run now)
│ │ ├── class-prv-cron.php # Probe cron: schedule/reschedule/clear; skip if locked
│ │ ├── class-prv-cost-ledger.php # MTD cost + hard monthly cap enforcement
│ │ ├── class-prv-probe-result.php # Immutable value object from a probe call
│ │ ├── class-prv-probe-runner.php # Orchestrates peptide×intent×model run
│ │ ├── class-prv-probe-runner.php # Orchestrates run: lock + peptide*intent*model loop
│ │ ├── class-prv-collector-registry.php # Singleton registry for collectors + panels
│ │ └── class-prv-admin-page.php # Admin page: menu, Run now, rendering
│ │ ├── class-prv-admin-page.php # Dashboard page: menu, Run now, rendering
│ │ ├── class-prv-settings-page.php # [v0.2] Settings page: menu, Run-now, AJAX dispatch; delegates POST to PRV_Settings_Controller
│ │ ├── class-prv-settings-controller.php # [v0.2] POST handler impl: save config, model CRUD (split from settings-page)
│ │ ├── class-prv-settings-renderer.php # [v0.2] Settings HTML renderer (dark Assay theme)
│ │ ├── class-prv-model-manager-table.php # [v0.2] Model manager table + Add form renderer
│ │ └── class-prv-model-test-ajax.php # [v0.2] Rate-limited AJAX handler for Test button
│ ├── providers/
│ │ ├── interface-prv-probe-provider.php # probe(query): PRV_Probe_Result contract
│ │ ├── class-prv-gateway-client.php # Cloudflare AI Gateway HTTP + retry
@@ -54,8 +64,13 @@ pr-vision/
│ ├── test-cost-ledger.php # MTD cost + budget cap
│ ├── test-provider-parsing.php # Mock-HTTP provider response parsing
│ ├── test-cron.php # Schedule/clear + activation/deactivation
│ ├── test-probe-runner.php # Run mechanics + budget abort
│ └── test-uninstall.php # Table drop + option purge
│ ├── test-probe-runner.php # Run mechanics + budget abort + lock + health
│ ├── test-uninstall.php # Table drop + option purge (incl. v0.2 keys)
│ ├── test-model-registry.php # [v0.2] Migration, CRUD, run-health
│ ├── test-run-lock.php # [v0.2] Acquire/release/sentinel
│ ├── test-config-version.php # [v0.2] Hash, bump, version stamp
│ ├── test-projected-cost.php # [v0.2] probe_count, over_cap, truncation flag
│ └── test-cron-reschedule.php # [v0.2] Reschedule, idempotent, tick guard
└── .github/workflows/ci.yml # PHP lint matrix + PHPCS + 300-line check
```
@@ -64,42 +79,45 @@ pr-vision/
## 3. Data flow
```
WP-Cron (weekly) ──────────────────────────────────────┐
Admin "Run now" (POST + nonce) ─────────────────────────┤
↓
PRV_Probe_Runner::run()
│
┌───────────────────────────────────┤
│ for each peptide × intent × model│
│ │
│ PRV_Cost_Ledger::can_afford() │
│ └─ ABORT gracefully if at cap │
│ │
│ PRV_Probe_Provider::probe(query) │
│ └─ PRV_Gateway_Client │
│ └─ Cloudflare AI Gateway │
│ └─ OpenRouter │
│ └─ LLM │
│ │
│ PRV_Citation_Detector │
│ └─ extract domains │
│ └─ detect peptiderepo.com │
│ │
│ $wpdb→insert(prv_ai_visibility) │
│ PRV_Cost_Ledger::update_row_cost()│
└───────────────────────────────────┘
│
Admin page load ────────────────────────────────────────┤
↓
PRV_Admin_Page::render_page()
│
PRV_Collector_Registry
│
PRV_Ai_Visibility_Collector::collect()
│ (DB reads)
PRV_Ai_Visibility_Panel::render()
│ (HTML output)
Browser (Chart.js)
WP-Cron (scheduled cadence) ─────────────────────────────────────┐
Admin "Run now" (Settings POST + nonce) ──────────────────────────┤
↓
PRV_Run_Lock::acquire() ← refuse if busy
↓
PRV_Probe_Runner::run()
│
┌───────────────────────────────────────┤
│ for each peptide × intent × model │
│ │
│ PRV_Cost_Ledger::can_afford() │
│ └─ ABORT gracefully if at cap │
│ │
│ PRV_Probe_Provider::probe(query) │
│ └─ PRV_Gateway_Client │
│ └─ Cloudflare AI Gateway │
│ └─ OpenRouter / LLM │
│ │
│ PRV_Citation_Detector │
│ │
│ $wpdb->insert(prv_ai_visibility) │
│ └─ config_version column stamped │
│ PRV_Cost_Ledger::update_row_cost() │
└───────────────────────────────────────┘
│
PRV_Model_Registry::update_health()
PRV_Run_Lock::release()
│
Admin page load ──────────────────────────────────────────────────┤
↓
PRV_Admin_Page::render_page()
│
PRV_Collector_Registry
│
PRV_Ai_Visibility_Collector::collect()
PRV_Ai_Visibility_Panel::render()
│
Browser (Chart.js)
(config-change annotations via config_versions option)
```
---
@@ -122,58 +140,63 @@ Admin page load ─────────────────────
| source_domains | LONGTEXT | JSON array of domains |
| raw_excerpt | LONGTEXT | First 500 chars of LLM response |
| cost_usd | DECIMAL(12,8) | Actual call cost |
| config_version | INT NULL | [v0.2] Config version at run time; NULL for legacy rows |
Schema version tracked in `prv_schema_version` option.
Schema version: `prv_schema_version` = 2.
**Key options (v0.2.0)**
| Option | Type | Notes |
|--------|------|-------|
| prv_models | array | v2 rich-object model list; migrated from flat-string v1 |
| prv_models_schema_version | int | 2 when migration complete |
| prv_config_versions | array | All config-version records |
| prv_active_config_version | int | Currently active version number |
| prv_cadence | string | 'weekly'\|'daily'\|'twicedaily' |
| prv_api_key_status | string | 'ok'\|'failed'\|'unknown' |
| prv_api_key_last_check | string | Datetime of last status update |
| prv_last_run_at | string | Datetime of last run |
| prv_last_run_counts | array | {probed, skipped_budget, skipped_error, truncated, run_id} |
| prv_last_run_truncated | int | 1 when last run hit the budget cap mid-way |
| prv_last_run_truncated_at | string | Datetime of last truncation event |
| _transient_prv_run_lock | int | Holds Unix timestamp while a run is in progress (TTL 3600s) |
---
## 5. External API integrations
### Cloudflare AI Gateway
All LLM calls route through:
```
https://gateway.ai.cloudflare.com/v1/{PRV_CF_ACCOUNT_ID}/{PRV_CF_GATEWAY_ID}/openrouter
```
Falls back to direct OpenRouter when the constants are absent.
Falls back to direct OpenRouter (`https://openrouter.ai/api/v1`) when the constants are absent or empty.
Pattern mirrors PRAutoBlogger's `class-open-router-config.php` and `class-open-router-request-builder.php` — same auth injection, same cURL belt-and-suspenders for Hostinger.
### Providers (v1)
### Providers (v0.1 + v0.2 UI-managed)
| Provider | Model | Citation source |
|----------|-------|-----------------|
| PRV_Perplexity_Provider | `perplexity/sonar` | `citations[]` array (primary, real-web retrieval) |
| PRV_OpenRouter_Provider | `openai/gpt-4o-search-preview` | annotations or inline URL regex |
| PRV_OpenRouter_Provider | `google/gemini-2.0-flash-001` | annotations or inline URL regex |
| PRV_Perplexity_Provider | `perplexity/sonar` | `citations[]` array |
| PRV_OpenRouter_Provider | any OpenRouter slug | annotations or inline URL regex |
Models are now managed via the Settings UI rather than hardcoded. The `prv_models` option is the source of truth; `PRV_Config::get_models()` delegates to `PRV_Model_Registry::get_enabled_slugs()`.
---
## 6. Collector / Panel seam
```
PRV_Data_Collector (interface) PRV_Dashboard_Panel (interface)
│ │
PRV_Ai_Visibility_Collector PRV_Ai_Visibility_Panel
│ │
└──────── PRV_Collector_Registry ──────────┘
(key: "ai_visibility")
```
To add a future SEO collector (keyword rankings, schema coverage…):
1. Implement `PRV_Data_Collector` + `PRV_Dashboard_Panel`.
2. Register both in `PRV_Plugin::init()`.
No dashboard shell changes required.
Same as v0.1.0 -- see CONTEXT.md for the extension guide.
---
## 7. Security
- Secrets read from `wp-config.php` constants at runtime — never hardcoded.
- All admin actions: `manage_options` capability check + nonce verification.
- Secrets read from `wp-config.php` constants at runtime -- never stored in DB.
- API key status shown in 3 states; key never displayed.
- All admin actions: `manage_options` check + nonce verification.
- All DB values escaped with `$wpdb->prepare()`.
- All HTML output escaped with `esc_html()`, `esc_url()`, `wp_json_encode()`.
- All HTML output escaped at render.
- AJAX `prv_test_model`: capability + nonce + 30s rate-limit transient.
---
@@ -181,8 +204,9 @@ No dashboard shell changes required.
| Decision | Rationale |
|----------|-----------|
| Use OpenRouter for all models | Single API key, single gateway URL, Perplexity sonar is accessible via OpenRouter's model routing. |
| Perplexity sonar as primary | Returns an explicit `citations[]` array — the most faithful citation signal available. |
| Cost ledger from visibility table | No separate ledger table needed; `SUM(cost_usd)` per calendar month is sufficient and keeps the schema minimal. |
| Chart.js via CDN | No build step, no bundler, admin-only load, pinned version. |
| Plain PHP tests (no PHPUnit) | Mirrors peptide-repo-core's pattern; CI runs in the lint-php job with no extra setup. |
| `prv_models_schema_version` for migration gate | Cheap option check; migration is idempotent and safe to call on every plugins_loaded without performance cost. |
| Config-version as hash + version-number | Hash detects changes; integer version is stable foreign key for run rows and chart annotations. Orphaned rows (NULL config_version or old version) are excluded from current-config scoring. |
| Run-lock as transient | WP transients work across all environments; TTL guards against stale locks from crashed runs; no separate table needed. |
| Projected cost from probe_count × fixed estimate | Conservative overestimate; avoids the need for per-model price config in v1; operator sees a safe upper bound. |
| Three-state API key (not two) | A defined-but-revoked key showed false-green in v1 (QA finding M4). The third state "defined, last run failed" closes that gap without ever displaying the key. |
| Sticky save-bar warning (not just card banner) | Card banner scrolls off; save-bar is always in viewport on a page with a tall model table (QA finding M2). |