v0.2.1 — dashboard visuals + deploy pipeline
Piece 1 — Visuals - Add KPI bento bar (score tile with run-health pill, cost tile, last-run tile). Run-health pill shows 'All models healthy' / 'N model(s) degraded' / 'No run yet' sourced from PRV_Model_Registry health fields via collector. - Add config-change trendline marker: inline configMarker Chart.js plugin draws a vertical dashed orange line + pin at every config_version boundary. Legend swatch + annotation note appear when breaks exist. - Chart sits in fixed .prv-chartbox (300px); typeof-Chart guard + .prv-noscript fallback — cannot grow layout, degrades gracefully offline. - PRV_Dashboard_Renderer: new class owns bento tile + trendline rendering. PRV_Ai_Visibility_Panel is now a thin orchestrator (237 lines). - PRV_Ai_Visibility_Collector: trendline query now includes MIN(config_version) per run; collect() adds last_run_counts and config_versions to payload. - PRV_Admin_Page: get_dashboard_css() returns Assay dark palette CSS. Piece 2 — Deploy pipeline - .github/workflows/deploy.yml: on push to main, runs CI, rsyncs plugin to Hostinger PROD (excl. .git .github tests phpunit composer vendor phpcs *.md), purges LiteSpeed, health-checks (200/403). Standalone — not smoke-gated. DoD - Version 0.2.1 in header + constant; CHANGELOG, ARCHITECTURE, CONVENTIONS, CONTEXT all updated same commit. - tests/unit/test-dashboard-panel.php: 27 assertions. - Pre-push checks: no PHP built-in name shadows in bootstrap.php; all includes/*.php files <= 300 lines (max: 298).
This commit is contained in:
@@ -11,9 +11,11 @@ declare(strict_types=1);
|
||||
* AI-visibility data collector — v1 implementation of PRV_Data_Collector.
|
||||
*
|
||||
* Reads the prv_ai_visibility table and returns structured data for:
|
||||
* - Trendline: per-run visibility score (% cited, position-weighted).
|
||||
* - Trendline: per-run visibility score (% cited, position-weighted) with config_version stamp.
|
||||
* - Standings: per-peptide latest cited status, our position, competing domains.
|
||||
* - Run metadata: last run time, MTD cost vs cap.
|
||||
* - Per-model run-health from PRV_Model_Registry (last_run_counts).
|
||||
* - All config-version records for trendline break annotations (config_versions).
|
||||
*
|
||||
* Score formula (documented in CONTEXT.md):
|
||||
* base_score = cited_probes / total_probes (range 0–1)
|
||||
@@ -21,10 +23,13 @@ declare(strict_types=1);
|
||||
* visibility_score = round((base_score + position_bonus) / 2, 4)
|
||||
*
|
||||
* Who triggers: PRV_Admin_Page calls the registered collector via the registry.
|
||||
* Dependencies: $wpdb, PRV_Table_Manager, PRV_Cost_Ledger, PRV_Config.
|
||||
* Dependencies: $wpdb, PRV_Table_Manager, PRV_Cost_Ledger, PRV_Config,
|
||||
* PRV_Model_Registry, PRV_Config_Version.
|
||||
*
|
||||
* @see interface-prv-data-collector.php — Interface this implements.
|
||||
* @see class-prv-ai-visibility-panel.php — Renders the data returned here.
|
||||
* @see class-prv-model-registry.php — Source of per-model health data.
|
||||
* @see class-prv-config-version.php — Source of config-version records.
|
||||
* @see CONTEXT.md — Score formula + glossary.
|
||||
* @see ARCHITECTURE.md — §Storage, §Score.
|
||||
* @package PrVision
|
||||
@@ -51,11 +56,13 @@ class PRV_Ai_Visibility_Collector implements PRV_Data_Collector {
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return array{
|
||||
* trendline: array<int, array{run_id: string, captured_at: string, score: float}>,
|
||||
* trendline: array<int, array{run_id: string, captured_at: string, score: float, config_version: int|null}>,
|
||||
* standings: array<string, array{label: string, cited: bool, our_position: int|null, top_domains: string[], model_count: int}>,
|
||||
* last_run_at: string|null,
|
||||
* mtd_cost_usd: float,
|
||||
* monthly_cap_usd: float,
|
||||
* last_run_counts: array<string, array{health_status: string, health_probed: int, health_errors: int}>,
|
||||
* config_versions: array<int, array<string, mixed>>,
|
||||
* }
|
||||
*/
|
||||
public function collect(): array {
|
||||
@@ -63,13 +70,14 @@ class PRV_Ai_Visibility_Collector implements PRV_Data_Collector {
|
||||
|
||||
$table = PRV_Table_Manager::get_table_name();
|
||||
|
||||
// ── Trendline: one score per run_id ────────────────────────────
|
||||
// ── Trendline: one score per run_id, with config_version ───────
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$run_rows = $wpdb->get_results(
|
||||
"SELECT run_id, MIN(captured_at) AS captured_at,
|
||||
SUM(cited) AS cited_count,
|
||||
COUNT(*) AS total_count,
|
||||
SUM(CASE WHEN our_position IS NOT NULL THEN 1.0/our_position ELSE 0 END) AS position_sum
|
||||
SUM(CASE WHEN our_position IS NOT NULL THEN 1.0/our_position ELSE 0 END) AS position_sum,
|
||||
MIN(config_version) AS config_version
|
||||
FROM {$table}
|
||||
GROUP BY run_id
|
||||
ORDER BY MIN(captured_at) ASC
|
||||
@@ -81,13 +89,14 @@ class PRV_Ai_Visibility_Collector implements PRV_Data_Collector {
|
||||
$trendline = array();
|
||||
foreach ( (array) $run_rows as $row ) {
|
||||
$trendline[] = array(
|
||||
'run_id' => (string) $row['run_id'],
|
||||
'captured_at' => (string) $row['captured_at'],
|
||||
'score' => $this->compute_score(
|
||||
'run_id' => (string) $row['run_id'],
|
||||
'captured_at' => (string) $row['captured_at'],
|
||||
'score' => $this->compute_score(
|
||||
(int) $row['cited_count'],
|
||||
(int) $row['total_count'],
|
||||
(float) $row['position_sum']
|
||||
),
|
||||
'config_version' => isset( $row['config_version'] ) ? (int) $row['config_version'] : null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,7 +117,7 @@ class PRV_Ai_Visibility_Collector implements PRV_Data_Collector {
|
||||
|
||||
$standings = array();
|
||||
foreach ( (array) $standing_rows as $row ) {
|
||||
$top_domains = $this->extract_top_domains( (string) ( $row['domains_json_list'] ?? '' ) ); // phpcs:ignore Generic.Formatting.MultipleStatementAlignment.NotSameWarning
|
||||
$top_domains = $this->extract_top_domains( (string) ( $row['domains_json_list'] ?? '' ) );
|
||||
$standings[ (string) $row['peptide_slug'] ] = array(
|
||||
'label' => (string) $row['peptide_label'],
|
||||
'cited' => (bool) (int) $row['cited'],
|
||||
@@ -123,12 +132,20 @@ class PRV_Ai_Visibility_Collector implements PRV_Data_Collector {
|
||||
$last_run = $wpdb->get_var( "SELECT MAX(captured_at) FROM {$table}" );
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
|
||||
// ── Per-model health from PRV_Model_Registry ────────────────────
|
||||
$last_run_counts = $this->collect_model_health();
|
||||
|
||||
// ── Config-version records for trendline break annotations ──────
|
||||
$config_versions = PRV_Config_Version::get_all_versions();
|
||||
|
||||
return array(
|
||||
'trendline' => $trendline,
|
||||
'standings' => $standings,
|
||||
'last_run_at' => $last_run ? (string) $last_run : null,
|
||||
'mtd_cost_usd' => $this->ledger->get_month_to_date_usd(),
|
||||
'monthly_cap_usd' => PRV_Config::get_monthly_budget_usd(),
|
||||
'last_run_counts' => $last_run_counts,
|
||||
'config_versions' => $config_versions,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -159,6 +176,32 @@ class PRV_Ai_Visibility_Collector implements PRV_Data_Collector {
|
||||
return round( ( $base_score + $position_bonus ) / 2.0, 4 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect per-model health data from PRV_Model_Registry for the dashboard pill.
|
||||
*
|
||||
* Returns a keyed map of model slug -> health fields. Models with
|
||||
* health_status='disabled' are included so the pill reflects all registered
|
||||
* models, not just the active ones.
|
||||
*
|
||||
* @return array<string, array{health_status: string, health_probed: int, health_errors: int}>
|
||||
*/
|
||||
private function collect_model_health(): array {
|
||||
$models = PRV_Model_Registry::get_all();
|
||||
$out = array();
|
||||
foreach ( $models as $m ) {
|
||||
$slug = (string) ( $m['slug'] ?? '' );
|
||||
if ( '' === $slug ) {
|
||||
continue;
|
||||
}
|
||||
$out[ $slug ] = array(
|
||||
'health_status' => (string) ( $m['health_status'] ?? 'unknown' ),
|
||||
'health_probed' => (int) ( $m['health_probed'] ?? 0 ),
|
||||
'health_errors' => (int) ( $m['health_errors'] ?? 0 ),
|
||||
);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract and deduplicate the top domain names from a GROUP_CONCAT result.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user