Spaces:
Sleeping
Sleeping
| defined( 'ABSPATH' ) || exit; | |
| /** | |
| * Read-only admin visibility layer. | |
| * | |
| * Top-level menu "SA-Orchestration" with three subpages: | |
| * - Demand Trace (form + full trace including v0.2 closure fields) | |
| * - Recent Arbitrations (last 20 arbitrations) | |
| * - Acceptance Events (last 20 acceptance events) | |
| * | |
| * No editing. No new tables. Reuses SA_Orch_Rest::get_demand_trace() and | |
| * SA_Orch_Acceptance helpers — no logic duplication. | |
| */ | |
| class SA_Orch_Admin { | |
| const PARENT_SLUG = 'sa-orchestration'; | |
| const ARB_SLUG = 'sa-orchestration-arbitrations'; | |
| const EVENT_SLUG = 'sa-orchestration-events'; | |
| const LLM_SLUG = 'sa-orchestration-llm'; | |
| const CAP = 'manage_options'; | |
| public static function register_menu() { | |
| add_menu_page( | |
| 'SA-Orchestration', | |
| 'SA-Orchestration', | |
| self::CAP, | |
| self::PARENT_SLUG, | |
| [ __CLASS__, 'render_trace' ], | |
| 'dashicons-networking', | |
| 80 | |
| ); | |
| // First submenu — reusing the parent slug renames the auto-generated | |
| // submenu from "SA-Orchestration" to "Demand Trace". | |
| add_submenu_page( | |
| self::PARENT_SLUG, | |
| 'Demand Trace', | |
| 'Demand Trace', | |
| self::CAP, | |
| self::PARENT_SLUG, | |
| [ __CLASS__, 'render_trace' ] | |
| ); | |
| add_submenu_page( | |
| self::PARENT_SLUG, | |
| 'Recent Arbitrations', | |
| 'Recent Arbitrations', | |
| self::CAP, | |
| self::ARB_SLUG, | |
| [ __CLASS__, 'render_arbitrations' ] | |
| ); | |
| add_submenu_page( | |
| self::PARENT_SLUG, | |
| 'Acceptance Events', | |
| 'Acceptance Events', | |
| self::CAP, | |
| self::EVENT_SLUG, | |
| [ __CLASS__, 'render_events' ] | |
| ); | |
| add_submenu_page( | |
| self::PARENT_SLUG, | |
| 'Language Model Providers', | |
| 'Language Model Providers', | |
| self::CAP, | |
| self::LLM_SLUG, | |
| [ __CLASS__, 'render_llm_settings' ] | |
| ); | |
| } | |
| /* ---------- Demand Trace ---------- */ | |
| public static function render_trace() { | |
| if ( ! current_user_can( self::CAP ) ) wp_die( 'Insufficient permissions.' ); | |
| $surface = isset( $_GET['demand_surface'] ) ? sanitize_text_field( wp_unslash( $_GET['demand_surface'] ) ) : ''; | |
| $ref = isset( $_GET['demand_ref'] ) ? sanitize_text_field( wp_unslash( $_GET['demand_ref'] ) ) : ''; | |
| echo '<div class="wrap">'; | |
| echo '<h1>Demand Trace</h1>'; | |
| echo '<p>Walk a Demand through arbitration → plan_links → acceptance chain → closure verdicts.</p>'; | |
| // Picker form (replaces v0.6.x free-form text inputs). | |
| self::render_picker_form( $surface, $ref ); | |
| if ( $surface !== '' && $ref !== '' ) { | |
| // Reuse the REST handler. Bypasses URI routing (avoids URL-encoding fragility around `#`). | |
| $req = new WP_REST_Request( 'GET' ); | |
| $req->set_url_params( [ 'surface' => $surface, 'ref' => $ref ] ); | |
| $resp = SA_Orch_Rest::get_demand_trace( $req ); | |
| $data = $resp->get_data(); | |
| self::render_trace_results( $data ); | |
| } | |
| echo '</div>'; | |
| } | |
| /* ---------- Demand Trace picker (v0.6.2) ---------- */ | |
| /** | |
| * Demand picker: | |
| * - surface dropdown (closed enumeration of 4) | |
| * - quick-select strip (SA-ORCH-TEST fixtures pinned + last-traced) | |
| * - searchable browser of candidates for the selected surface | |
| * - manual ref entry as fallback (validated against known per-surface patterns) | |
| * | |
| * No new schema; no new endpoints. All candidate data is rendered inline | |
| * as JSON for client-side filtering. | |
| */ | |
| private static function render_picker_form( $surface, $ref ) { | |
| $candidates = [ | |
| 'fluent-support' => self::candidates_fluent_support(), | |
| 'fluent-boards' => self::candidates_fluent_boards(), | |
| 'fluentcrm' => self::candidates_fluentcrm(), | |
| 'derived' => self::candidates_derived(), | |
| ]; | |
| $quick = self::quick_select_demands( 10 ); | |
| $surface_labels = [ | |
| 'fluent-support' => 'fluent-support — Fluent Support tickets', | |
| 'fluent-boards' => 'fluent-boards — Fluent Boards tasks', | |
| 'fluentcrm' => 'fluentcrm — FluentCRM subscribers', | |
| 'derived' => 'derived — rowless / inferred demands', | |
| ]; | |
| // Per-surface validation regex (mirrors SA_Orch_Bridge_Fluent::validate / resolve). | |
| $patterns = [ | |
| 'fluent-support' => '^ticket#\\d+$', | |
| 'fluent-boards' => '^task#\\d+$', | |
| 'fluentcrm' => '^subscriber#\\d+$', | |
| 'derived' => '^derived#[A-Za-z0-9_\\-]+$', | |
| ]; | |
| echo '<form method="get" action="' . esc_url( admin_url( 'admin.php' ) ) . '" id="sa-orch-trace-form">'; | |
| echo '<input type="hidden" name="page" value="' . esc_attr( self::PARENT_SLUG ) . '" />'; | |
| // ---------- Quick-select ---------- | |
| if ( ! empty( $quick ) ) { | |
| echo '<div class="sa-orch-quick" style="margin:1em 0; padding:10px 14px; background:#f6f7f7; border:1px solid #dcdcde; max-width:900px">'; | |
| echo '<h3 style="margin:0 0 6px; font-size:13px; text-transform:uppercase; letter-spacing:0.05em; color:#50575e">Quick-select</h3>'; | |
| echo '<p class="description" style="margin:0 0 8px">SA-ORCH-TEST fixtures pinned, then the most recently traced Demands.</p>'; | |
| echo '<ul style="margin:0; padding:0; list-style:none">'; | |
| foreach ( $quick as $d ) { | |
| $url = admin_url( 'admin.php?page=' . self::PARENT_SLUG | |
| . '&demand_surface=' . urlencode( $d['surface'] ) | |
| . '&demand_ref=' . urlencode( $d['ref'] ) ); | |
| $tag = $d['is_test'] | |
| ? '<span style="background:#fff2c2;border:1px solid #c9a300;border-radius:3px;padding:0 6px;font-size:11px;margin-right:6px;color:#7a5b00">TEST</span> ' | |
| : ''; | |
| echo '<li style="padding:3px 0">' | |
| . $tag | |
| . '<a href="' . esc_url( $url ) . '">' | |
| . '<code>' . esc_html( $d['surface'] . ' / ' . $d['ref'] ) . '</code>' | |
| . ( $d['label'] !== '' ? ' — ' . esc_html( $d['label'] ) : '' ) | |
| . '</a></li>'; | |
| } | |
| echo '</ul></div>'; | |
| } | |
| // ---------- Surface + ref ---------- | |
| echo '<table class="form-table" role="presentation" style="max-width:900px"><tbody>'; | |
| echo '<tr><th scope="row"><label for="demand_surface_select">Demand surface</label></th><td>'; | |
| echo '<select id="demand_surface_select" name="demand_surface" class="regular-text">'; | |
| echo '<option value="">— pick a surface —</option>'; | |
| foreach ( $surface_labels as $sid => $slabel ) { | |
| echo '<option value="' . esc_attr( $sid ) . '"' . selected( $surface, $sid, false ) . '>' | |
| . esc_html( $slabel ) . '</option>'; | |
| } | |
| echo '</select>'; | |
| echo '<p class="description">Closed vocabulary; extending requires touching <code>SA_Orch_Bridge_Fluent</code>.</p>'; | |
| echo '</td></tr>'; | |
| echo '<tr><th scope="row"><label for="demand_ref_input">Demand ref</label></th><td>'; | |
| echo '<input type="text" id="demand_ref_input" name="demand_ref" value="' . esc_attr( $ref ) . '"'; | |
| echo ' class="regular-text" placeholder="pick from the list below, or type" autocomplete="off" />'; | |
| echo '<span id="sa-orch-ref-status" style="margin-left:10px; font-size:12px"></span>'; | |
| echo '<p class="description">Format depends on surface: <code>ticket#NNN</code> · <code>task#NNN</code> · <code>subscriber#NNN</code> · <code>derived#slug</code>. Type or pick.</p>'; | |
| echo '</td></tr>'; | |
| echo '</tbody></table>'; | |
| echo '<p class="submit" style="margin:6px 0 14px"><button type="submit" class="button button-primary">Trace</button></p>'; | |
| // ---------- Browser ---------- | |
| echo '<div class="sa-orch-browse" style="margin:0 0 1.5em; padding:10px 14px; background:#fff; border:1px solid #dcdcde; max-width:900px">'; | |
| echo '<h3 style="margin:0 0 8px; font-size:13px; text-transform:uppercase; letter-spacing:0.05em; color:#50575e">Browse candidates</h3>'; | |
| echo '<input type="search" id="sa-orch-filter" placeholder="filter by ID or label…" style="width:100%; max-width:560px" autocomplete="off" />'; | |
| echo '<div id="sa-orch-candidate-help" class="description" style="margin:6px 0">Pick a surface to load candidates.</div>'; | |
| echo '<ul id="sa-orch-candidate-list" style="margin:0; padding:0; list-style:none; max-height:360px; overflow:auto; border:1px solid #f0f0f1; border-radius:3px"></ul>'; | |
| echo '<p class="description" style="margin:8px 0 0"><em>Click any row to fill the Demand ref field. Ref is also free-form for advanced use; format is validated when you type.</em></p>'; | |
| echo '</div>'; | |
| // ---------- Inline JSON + JS ---------- | |
| $payload = [ | |
| 'candidates' => $candidates, | |
| 'patterns' => $patterns, | |
| ]; | |
| echo '<script>window.SA_ORCH_PICKER = ' . wp_json_encode( $payload ) . ';</script>'; | |
| echo '<script>' . self::picker_inline_js() . '</script>'; | |
| echo '</form>'; | |
| } | |
| /** | |
| * Vanilla-JS controller for the picker. No jQuery dependency. | |
| * Wires: surface change → repopulate browser; filter input → live filter; | |
| * candidate click → fill ref input; ref input → pattern hint. | |
| */ | |
| private static function picker_inline_js() { | |
| return <<<'JS' | |
| (function () { | |
| var data = window.SA_ORCH_PICKER || { candidates: {}, patterns: {} }; | |
| var surfaceSel = document.getElementById('demand_surface_select'); | |
| var refInput = document.getElementById('demand_ref_input'); | |
| var filterInp = document.getElementById('sa-orch-filter'); | |
| var listEl = document.getElementById('sa-orch-candidate-list'); | |
| var helpEl = document.getElementById('sa-orch-candidate-help'); | |
| var statusEl = document.getElementById('sa-orch-ref-status'); | |
| if (!surfaceSel || !refInput || !listEl || !helpEl || !filterInp) return; | |
| function escapeHtml(s) { | |
| return String(s).replace(/[&<>"']/g, function (c) { | |
| return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; | |
| }); | |
| } | |
| function renderCandidates() { | |
| var surface = surfaceSel.value; | |
| var query = (filterInp.value || '').trim().toLowerCase(); | |
| listEl.innerHTML = ''; | |
| if (!surface) { | |
| helpEl.textContent = 'Pick a surface to load candidates.'; | |
| filterInp.disabled = true; | |
| return; | |
| } | |
| filterInp.disabled = false; | |
| var pool = (data.candidates || {})[surface] || []; | |
| if (!pool.length) { | |
| helpEl.textContent = 'No candidates known for this surface.'; | |
| return; | |
| } | |
| var matches = pool; | |
| if (query) { | |
| matches = pool.filter(function (c) { | |
| return c.ref.toLowerCase().indexOf(query) !== -1 | |
| || (c.label || '').toLowerCase().indexOf(query) !== -1; | |
| }); | |
| } | |
| helpEl.textContent = matches.length + ' / ' + pool.length + ' candidate' | |
| + (pool.length === 1 ? '' : 's') | |
| + (query ? ' matching "' + query + '"' : ''); | |
| var frag = document.createDocumentFragment(); | |
| matches.slice(0, 200).forEach(function (c) { | |
| var li = document.createElement('li'); | |
| li.style.padding = '5px 8px'; | |
| li.style.cursor = 'pointer'; | |
| li.style.borderBottom = '1px solid #f0f0f1'; | |
| li.style.lineHeight = '1.4'; | |
| var tag = c.is_test | |
| ? '<span style="background:#fff2c2;border:1px solid #c9a300;border-radius:3px;padding:0 5px;font-size:11px;margin-right:6px;color:#7a5b00">TEST</span>' | |
| : ''; | |
| li.innerHTML = tag + '<code>' + escapeHtml(c.ref) + '</code>' | |
| + (c.label ? ' — ' + escapeHtml(c.label) : ''); | |
| li.addEventListener('mouseover', function () { li.style.background = '#f6f7f7'; }); | |
| li.addEventListener('mouseout', function () { if (!li.classList.contains('selected')) li.style.background = ''; }); | |
| li.addEventListener('click', function () { | |
| refInput.value = c.ref; | |
| Array.prototype.forEach.call( | |
| listEl.querySelectorAll('li.selected'), | |
| function (e) { e.classList.remove('selected'); e.style.background = ''; } | |
| ); | |
| li.classList.add('selected'); | |
| li.style.background = '#fff7c2'; | |
| validateRef(); | |
| refInput.focus(); | |
| }); | |
| frag.appendChild(li); | |
| }); | |
| listEl.appendChild(frag); | |
| } | |
| function validateRef() { | |
| var surface = surfaceSel.value; | |
| var ref = refInput.value.trim(); | |
| if (!surface || !ref) { | |
| statusEl.textContent = ''; | |
| return; | |
| } | |
| var pattern = (data.patterns || {})[surface]; | |
| if (!pattern) { | |
| statusEl.textContent = ''; | |
| return; | |
| } | |
| if ((new RegExp(pattern)).test(ref)) { | |
| statusEl.textContent = '✓ valid format'; | |
| statusEl.style.color = '#0a7d24'; | |
| } else { | |
| statusEl.textContent = '✗ does not match expected pattern'; | |
| statusEl.style.color = '#a02b1f'; | |
| } | |
| } | |
| surfaceSel.addEventListener('change', function () { | |
| filterInp.value = ''; | |
| renderCandidates(); | |
| validateRef(); | |
| }); | |
| filterInp.addEventListener('input', renderCandidates); | |
| refInput.addEventListener('input', validateRef); | |
| // Initial state (e.g. arrived via permalink with both fields preset). | |
| renderCandidates(); | |
| validateRef(); | |
| })(); | |
| JS; | |
| } | |
| /* ---------- Candidate fetchers (per surface) ---------- */ | |
| /** Fluent Support tickets, SA-ORCH-TEST pinned first. Cap 50. */ | |
| private static function candidates_fluent_support() { | |
| global $wpdb; | |
| $rows = $wpdb->get_results( | |
| "SELECT id, title FROM {$wpdb->prefix}fs_tickets | |
| ORDER BY (title LIKE '[SA-ORCH-TEST]%') DESC, id DESC | |
| LIMIT 50" | |
| ); | |
| $out = []; | |
| foreach ( (array) $rows as $r ) { | |
| $title = (string) $r->title; | |
| $out[] = [ | |
| 'ref' => 'ticket#' . (int) $r->id, | |
| 'label' => self::truncate( $title, 80 ), | |
| 'is_test' => ( strpos( $title, '[SA-ORCH-TEST]' ) === 0 ), | |
| ]; | |
| } | |
| return $out; | |
| } | |
| /** Fluent Boards tasks, most-recent first. Cap 50. */ | |
| private static function candidates_fluent_boards() { | |
| global $wpdb; | |
| // Defensive: Fluent Boards may not be installed. | |
| $exists = $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $wpdb->prefix . 'fbs_tasks' ) ); | |
| if ( ! $exists ) return []; | |
| $rows = $wpdb->get_results( | |
| "SELECT id, title FROM {$wpdb->prefix}fbs_tasks | |
| ORDER BY id DESC LIMIT 50" | |
| ); | |
| $out = []; | |
| foreach ( (array) $rows as $r ) { | |
| $title = (string) ( $r->title ?? '' ); | |
| $out[] = [ | |
| 'ref' => 'task#' . (int) $r->id, | |
| 'label' => self::truncate( $title, 80 ), | |
| 'is_test' => ( strpos( $title, '[SA-ORCH-TEST]' ) === 0 ), | |
| ]; | |
| } | |
| return $out; | |
| } | |
| /** FluentCRM subscribers, most-recent first. Cap 50. Label = full_name|email. */ | |
| private static function candidates_fluentcrm() { | |
| global $wpdb; | |
| $exists = $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $wpdb->prefix . 'fc_subscribers' ) ); | |
| if ( ! $exists ) return []; | |
| $rows = $wpdb->get_results( | |
| "SELECT id, full_name, email FROM {$wpdb->prefix}fc_subscribers | |
| ORDER BY id DESC LIMIT 50" | |
| ); | |
| $out = []; | |
| foreach ( (array) $rows as $r ) { | |
| $name = trim( (string) ( $r->full_name ?? '' ) ); | |
| $email = (string) ( $r->email ?? '' ); | |
| $label = $name !== '' ? ( $name . ( $email ? " <{$email}>" : '' ) ) : $email; | |
| $out[] = [ | |
| 'ref' => 'subscriber#' . (int) $r->id, | |
| 'label' => self::truncate( $label, 80 ), | |
| 'is_test' => false, | |
| ]; | |
| } | |
| return $out; | |
| } | |
| /** | |
| * 'derived' surface has no underlying surface table — list every distinct | |
| * derived demand_ref ever recorded in arbitrations or acceptance events. | |
| */ | |
| private static function candidates_derived() { | |
| global $wpdb; | |
| $arb = $wpdb->prefix . SA_ORCH_DB_PREFIX . 'arbitration'; | |
| $evt = $wpdb->prefix . SA_ORCH_DB_PREFIX . 'acceptance_event'; | |
| $rows = $wpdb->get_results( | |
| "SELECT demand_ref, MAX(last_at) AS last_at FROM ( | |
| SELECT demand_ref, MAX(created_at) AS last_at FROM {$arb} WHERE demand_surface='derived' GROUP BY demand_ref | |
| UNION ALL | |
| SELECT demand_ref, MAX(observed_at) AS last_at FROM {$evt} WHERE demand_surface='derived' GROUP BY demand_ref | |
| ) sub | |
| GROUP BY demand_ref | |
| ORDER BY last_at DESC LIMIT 50" | |
| ); | |
| $out = []; | |
| foreach ( (array) $rows as $r ) { | |
| $out[] = [ | |
| 'ref' => (string) $r->demand_ref, | |
| 'label' => '(derived demand, no surface row)', | |
| 'is_test' => false, | |
| ]; | |
| } | |
| return $out; | |
| } | |
| /** | |
| * Quick-select list: SA-ORCH-TEST tickets pinned, then most-recently-traced | |
| * demands across all surfaces (by max(arbitration.created_at, event.observed_at)). | |
| */ | |
| private static function quick_select_demands( $limit = 10 ) { | |
| global $wpdb; | |
| $arb = $wpdb->prefix . SA_ORCH_DB_PREFIX . 'arbitration'; | |
| $evt = $wpdb->prefix . SA_ORCH_DB_PREFIX . 'acceptance_event'; | |
| $rows = $wpdb->get_results( | |
| "SELECT demand_surface, demand_ref, MAX(last_at) AS last_at FROM ( | |
| SELECT demand_surface, demand_ref, MAX(created_at) AS last_at FROM {$arb} GROUP BY demand_surface, demand_ref | |
| UNION ALL | |
| SELECT demand_surface, demand_ref, MAX(observed_at) AS last_at FROM {$evt} GROUP BY demand_surface, demand_ref | |
| ) sub | |
| GROUP BY demand_surface, demand_ref | |
| ORDER BY last_at DESC LIMIT 30" | |
| ); | |
| if ( empty( $rows ) ) return []; | |
| // Resolve labels + is_test per row. | |
| $out = []; | |
| foreach ( $rows as $r ) { | |
| $surface = (string) $r->demand_surface; | |
| $ref = (string) $r->demand_ref; | |
| list( $label, $is_test ) = self::resolve_label( $surface, $ref ); | |
| $out[] = [ | |
| 'surface' => $surface, | |
| 'ref' => $ref, | |
| 'label' => $label, | |
| 'is_test' => $is_test, | |
| 'last_at' => (string) $r->last_at, | |
| ]; | |
| } | |
| // Pin SA-ORCH-TEST first; preserve last_at ordering within each group. | |
| usort( $out, function ( $a, $b ) { | |
| if ( $a['is_test'] !== $b['is_test'] ) return $a['is_test'] ? -1 : 1; | |
| return strcmp( $b['last_at'], $a['last_at'] ); | |
| } ); | |
| return array_slice( $out, 0, (int) $limit ); | |
| } | |
| /** Resolve a (surface, ref) to a human label + is_test flag. Best-effort, tolerant of missing tables. */ | |
| private static function resolve_label( $surface, $ref ) { | |
| global $wpdb; | |
| if ( $surface === 'fluent-support' && preg_match( '/^ticket#(\d+)$/', $ref, $m ) ) { | |
| $title = (string) $wpdb->get_var( $wpdb->prepare( | |
| "SELECT title FROM {$wpdb->prefix}fs_tickets WHERE id=%d", (int) $m[1] | |
| ) ); | |
| return [ self::truncate( $title, 80 ), strpos( $title, '[SA-ORCH-TEST]' ) === 0 ]; | |
| } | |
| if ( $surface === 'fluent-boards' && preg_match( '/^task#(\d+)$/', $ref, $m ) ) { | |
| if ( $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $wpdb->prefix . 'fbs_tasks' ) ) ) { | |
| $title = (string) $wpdb->get_var( $wpdb->prepare( | |
| "SELECT title FROM {$wpdb->prefix}fbs_tasks WHERE id=%d", (int) $m[1] | |
| ) ); | |
| return [ self::truncate( $title, 80 ), strpos( $title, '[SA-ORCH-TEST]' ) === 0 ]; | |
| } | |
| } | |
| if ( $surface === 'fluentcrm' && preg_match( '/^subscriber#(\d+)$/', $ref, $m ) ) { | |
| if ( $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $wpdb->prefix . 'fc_subscribers' ) ) ) { | |
| $row = $wpdb->get_row( $wpdb->prepare( | |
| "SELECT full_name, email FROM {$wpdb->prefix}fc_subscribers WHERE id=%d", (int) $m[1] | |
| ) ); | |
| if ( $row ) { | |
| $name = trim( (string) $row->full_name ); | |
| $email = (string) $row->email; | |
| $label = $name !== '' ? ( $name . ( $email ? " <{$email}>" : '' ) ) : $email; | |
| return [ self::truncate( $label, 80 ), false ]; | |
| } | |
| } | |
| } | |
| if ( $surface === 'derived' ) { | |
| return [ '(derived demand, no surface row)', false ]; | |
| } | |
| return [ '', false ]; | |
| } | |
| private static function truncate( $s, $n ) { | |
| $s = (string) $s; | |
| if ( function_exists( 'mb_strlen' ) && function_exists( 'mb_substr' ) ) { | |
| return mb_strlen( $s ) > $n ? ( mb_substr( $s, 0, $n - 1 ) . '…' ) : $s; | |
| } | |
| return strlen( $s ) > $n ? ( substr( $s, 0, $n - 1 ) . '…' ) : $s; | |
| } | |
| private static function render_trace_results( array $data ) { | |
| $arbs = $data['arbitrations'] ?? []; | |
| $links = $data['plan_links'] ?? []; | |
| $events = $data['acceptance'] ?? []; | |
| echo '<h2>Closure</h2>'; | |
| echo '<table class="widefat striped" style="max-width:600px"><tbody>'; | |
| echo '<tr><th scope="row" style="width:40%">closure_state_basic</th><td><code>' . esc_html( (string) ( $data['closure_state_basic'] ?? '' ) ) . '</code></td></tr>'; | |
| echo '<tr><th scope="row">closure_state_role_aware</th><td><code>' . esc_html( (string) ( $data['closure_state_role_aware'] ?? '' ) ) . '</code></td></tr>'; | |
| $auth = $data['closure_authority_valid'] ?? null; | |
| $auth_disp = $auth === null ? 'null' : ( $auth ? 'true' : 'false' ); | |
| echo '<tr><th scope="row">closure_authority_valid</th><td><code>' . esc_html( $auth_disp ) . '</code></td></tr>'; | |
| echo '</tbody></table>'; | |
| // === Deterministic Trace Audit (layer 2 — rules-based) === | |
| self::render_reasoning_review( $data ); | |
| echo '<h2>Arbitrations <span class="count">(' . count( $arbs ) . ')</span></h2>'; | |
| if ( empty( $arbs ) ) { | |
| echo '<p><em>No arbitrations recorded for this demand.</em></p>'; | |
| } else { | |
| echo '<table class="widefat striped"><thead><tr>'; | |
| echo '<th>ID</th><th>Hash</th><th>Projection Type</th><th>Truth Class</th><th>Actor</th><th>Correlation</th><th>Created (UTC)</th><th>Asterion</th>'; | |
| echo '</tr></thead><tbody>'; | |
| foreach ( $arbs as $a ) { | |
| echo '<tr>'; | |
| echo '<td>' . (int) $a->id . '</td>'; | |
| echo '<td><code>' . esc_html( substr( (string) $a->hash, 0, 12 ) ) . '…</code></td>'; | |
| echo '<td>' . esc_html( (string) $a->projection_type ) . '</td>'; | |
| echo '<td>' . esc_html( (string) $a->truth_class ) . '</td>'; | |
| echo '<td>' . esc_html( (string) ( $a->actor_user_id ?? '—' ) ) . '</td>'; | |
| echo '<td><code>' . esc_html( substr( (string) $a->correlation_id, 0, 8 ) ) . '…</code></td>'; | |
| echo '<td>' . esc_html( (string) $a->created_at ) . '</td>'; | |
| echo '<td><code>' . esc_html( (string) $a->asterion_path ) . '</code></td>'; | |
| echo '</tr>'; | |
| echo '<tr><td></td><td colspan="7"><strong>Rationale:</strong> ' . esc_html( (string) $a->rationale ) . '</td></tr>'; | |
| } | |
| echo '</tbody></table>'; | |
| } | |
| echo '<h2>Plan Links <span class="count">(' . count( $links ) . ')</span></h2>'; | |
| if ( empty( $links ) ) { | |
| echo '<p><em>No plan links — this Demand has no Plan refs (consistent with <code>scope-direct-no-plan</code> or <code>no-plan</code> arbitration).</em></p>'; | |
| } else { | |
| echo '<table class="widefat striped"><thead><tr>'; | |
| echo '<th>ID</th><th>Arbitration</th><th>Plan Surface</th><th>Plan Ref</th><th>Link Kind</th><th>Created (UTC)</th>'; | |
| echo '</tr></thead><tbody>'; | |
| foreach ( $links as $l ) { | |
| echo '<tr>'; | |
| echo '<td>' . (int) $l->id . '</td>'; | |
| echo '<td>' . (int) $l->arbitration_id . '</td>'; | |
| echo '<td>' . esc_html( (string) $l->plan_surface ) . '</td>'; | |
| echo '<td><code>' . esc_html( (string) $l->plan_ref ) . '</code></td>'; | |
| echo '<td>' . esc_html( (string) $l->link_kind ) . '</td>'; | |
| echo '<td>' . esc_html( (string) $l->created_at ) . '</td>'; | |
| echo '</tr>'; | |
| } | |
| echo '</tbody></table>'; | |
| } | |
| echo '<h2>Acceptance Events <span class="count">(' . count( $events ) . ')</span></h2>'; | |
| if ( empty( $events ) ) { | |
| echo '<p><em>No acceptance events.</em></p>'; | |
| } else { | |
| echo '<table class="widefat striped"><thead><tr>'; | |
| echo '<th>ID</th><th>Event Type</th><th>Actor Role</th><th>Actor</th><th>Truth Class</th><th>Prior</th><th>Observed (UTC)</th><th>Asterion</th>'; | |
| echo '</tr></thead><tbody>'; | |
| foreach ( $events as $e ) { | |
| echo '<tr>'; | |
| echo '<td>' . (int) $e->id . '</td>'; | |
| echo '<td>' . esc_html( (string) $e->event_type ) . '</td>'; | |
| echo '<td>' . esc_html( (string) $e->actor_role ) . '</td>'; | |
| echo '<td>' . esc_html( (string) ( $e->actor_user_id ?? '—' ) ) . '</td>'; | |
| echo '<td>' . esc_html( (string) $e->truth_class ) . '</td>'; | |
| echo '<td>' . esc_html( (string) ( $e->prior_event_id ?? '—' ) ) . '</td>'; | |
| echo '<td>' . esc_html( (string) $e->observed_at ) . '</td>'; | |
| echo '<td><code>' . esc_html( (string) $e->asterion_path ) . '</code></td>'; | |
| echo '</tr>'; | |
| if ( ! empty( $e->details ) ) { | |
| echo '<tr><td></td><td colspan="7"><strong>Details:</strong> ' . esc_html( (string) $e->details ) . '</td></tr>'; | |
| } | |
| } | |
| echo '</tbody></table>'; | |
| } | |
| } | |
| /** | |
| * Deterministic Trace Audit block. | |
| * | |
| * Local rules-based audit over the trace data. No API calls, no LLM, no | |
| * persistence. Recomputed on every page load. | |
| * | |
| * This is the FLOOR of the review stack: | |
| * 1. Trace data | |
| * 2. Deterministic Trace Audit ← this section | |
| * 3. Strategy LLM Review (future; will sit above this audit) | |
| * 4. Human arbitration (final meaning; always wins) | |
| */ | |
| private static function render_reasoning_review( array $data ) { | |
| $r = SA_Orch_Reasoning::evaluate( $data ); | |
| echo '<h2>Deterministic Trace Audit <span class="count">(rules-based, mode: ' . esc_html( (string) ( $r['mode'] ?? 'deterministic' ) ) . ')</span></h2>'; | |
| echo '<p><em>Local rules-based audit — repeatable, no API, no LLM. Computed on each page load. Floor of a four-layer stack: '; | |
| echo '<strong>(1) Trace data</strong> → <strong>(2) this Deterministic Trace Audit</strong> → <strong>(3) Strategy LLM Review</strong> (future) → <strong>(4) Human arbitration</strong> (final).</em></p>'; | |
| $color_map = [ 'high' => '#0a7d24', 'medium' => '#a76b00', 'low' => '#a02b1f', 'n/a' => '#666' ]; | |
| $color = $color_map[ $r['closure_confidence'] ] ?? '#666'; | |
| echo '<table class="widefat striped" style="max-width:900px"><tbody>'; | |
| echo '<tr><th scope="row" style="width:25%">Summary</th><td>' . esc_html( $r['summary'] ) . '</td></tr>'; | |
| echo '<tr><th scope="row">Closure confidence</th><td>'; | |
| echo '<strong style="color:' . esc_attr( $color ) . '">' . esc_html( strtoupper( (string) $r['closure_confidence'] ) ) . '</strong>'; | |
| echo ' — ' . esc_html( $r['closure_confidence_explanation'] ); | |
| echo '</td></tr>'; | |
| // Needs Human Arbitration — prominent row | |
| echo '<tr><th scope="row">Needs Human Arbitration</th><td>'; | |
| if ( ! empty( $r['needs_human_arbitration'] ) ) { | |
| echo '<strong style="color:#a02b1f">⚠ YES — ' . esc_html( (string) $r['human_arbitration_message'] ) . '</strong>'; | |
| echo '<p style="margin:6px 0 0">Triggered by:</p>'; | |
| echo '<ul style="margin:0; padding-left:1.2em">'; | |
| foreach ( (array) $r['human_arbitration_triggers'] as $t ) { | |
| echo '<li>' . esc_html( $t ) . '</li>'; | |
| } | |
| echo '</ul>'; | |
| } else { | |
| echo '<em>No — none of the deterministic escalation triggers fired.</em>'; | |
| } | |
| echo '</td></tr>'; | |
| $render_list = function ( $label, $items ) { | |
| echo '<tr><th scope="row">' . esc_html( $label ) . '</th><td>'; | |
| if ( empty( $items ) ) { | |
| echo '<em>(none)</em>'; | |
| } else { | |
| echo '<ul style="margin:0; padding-left:1.2em">'; | |
| foreach ( $items as $i ) { | |
| echo '<li>' . esc_html( $i ) . '</li>'; | |
| } | |
| echo '</ul>'; | |
| } | |
| echo '</td></tr>'; | |
| }; | |
| $render_list( 'Inconsistencies', $r['inconsistencies'] ); | |
| $render_list( 'Potential scope drift', $r['scope_drift'] ); | |
| $render_list( 'Notes', $r['notes'] ); | |
| echo '</tbody></table>'; | |
| echo '<p style="margin-top:8px"><em>Layer 3 — Strategy LLM Review — sits below this section. The audit remains as a sanity-check / fallback layer underneath any LLM verdict.</em></p>'; | |
| // --- Strategy Review (v0.5) --- | |
| self::render_strategy_review( $data, $r ); | |
| } | |
| /** | |
| * Strategy LLM Review block (layer 3). | |
| * | |
| * Consumes the trace + the deterministic audit; delegates to the active | |
| * provider via SA_Orch_Strategy::review(). Strategy Review is advisory: | |
| * it cannot override closure_state or needs_human_arbitration. | |
| */ | |
| private static function render_strategy_review( array $data, array $deterministic_audit ) { | |
| $strategy = SA_Orch_Strategy::review( $data, $deterministic_audit ); | |
| $provider_id = (string) ( $strategy['provider_id'] ?? 'simulated' ); | |
| $provider_name = (string) ( $strategy['provider_name'] ?? 'Simulated (no API call)' ); | |
| $configured_id = (string) ( $strategy['configured_provider_id'] ?? $provider_id ); | |
| $configured_name = (string) ( $strategy['configured_provider_name'] ?? $provider_name ); | |
| $fallback_used = ! empty( $strategy['fallback_used'] ); | |
| $provider_err = $strategy['provider_error'] ?? null; | |
| $heading_suffix = $fallback_used | |
| ? '(layer 3 — used: ' . esc_html( $provider_id ) . ', configured: ' . esc_html( $configured_id ) . ' — <strong style="color:#a76b00">FALLBACK</strong>)' | |
| : '(layer 3 — provider: ' . esc_html( $provider_id ) . ')'; | |
| echo '<h2>Strategy Review (simulated LLM) <span class="count">' . $heading_suffix . '</span></h2>'; | |
| echo '<p><em>Layer 3 of the review stack. Provider in use: <code>' . esc_html( $provider_name ) . '</code>. '; | |
| if ( $fallback_used ) { | |
| echo 'Configured provider (<code>' . esc_html( $configured_name ) . '</code>) failed and was replaced by the simulated fallback. '; | |
| } | |
| echo 'Compares its interpretation against the deterministic audit. '; | |
| echo '<strong>Strategy Review cannot override <code>closure_state</code> or the <code>needs_human_arbitration</code> flag</strong> — those remain governed by the deterministic audit and ultimately by human arbitration.</em></p>'; | |
| if ( $provider_err ) { | |
| echo '<div class="notice notice-warning"><p><strong>Provider error (configured: <code>' . esc_html( $configured_id ) . '</code>):</strong> ' . esc_html( $provider_err ) . ' — fell back to <code>simulated</code>.</p></div>'; | |
| } | |
| // Color hints | |
| $color_map_conf = [ 'high' => '#0a7d24', 'medium' => '#a76b00', 'low' => '#a02b1f' ]; | |
| $color_map_agree = [ 'true' => '#0a7d24', 'partial' => '#a76b00', 'false' => '#a02b1f' ]; | |
| $strat_conf = (string) ( $strategy['confidence'] ?? 'low' ); | |
| $strat_agree = (string) ( $strategy['agreement_with_audit'] ?? 'partial' ); | |
| $strat_color = $color_map_conf[ $strat_conf ] ?? '#666'; | |
| $agree_color = $color_map_agree[ $strat_agree ] ?? '#666'; | |
| echo '<table class="widefat striped" style="max-width:900px"><tbody>'; | |
| echo '<tr><th scope="row" style="width:25%">Summary</th><td>' . esc_html( (string) ( $strategy['summary'] ?? '' ) ) . '</td></tr>'; | |
| echo '<tr><th scope="row">Agreement with Deterministic Audit</th><td>'; | |
| echo '<strong style="color:' . esc_attr( $agree_color ) . '">' . esc_html( strtoupper( $strat_agree ) ) . '</strong>'; | |
| echo '</td></tr>'; | |
| echo '<tr><th scope="row">Additional risks (not caught by audit)</th><td>'; | |
| $risks = (array) ( $strategy['additional_risks'] ?? [] ); | |
| if ( empty( $risks ) ) { | |
| echo '<em>(none)</em>'; | |
| } else { | |
| echo '<ul style="margin:0; padding-left:1.2em">'; | |
| foreach ( $risks as $r ) { | |
| echo '<li>' . esc_html( (string) $r ) . '</li>'; | |
| } | |
| echo '</ul>'; | |
| } | |
| echo '</td></tr>'; | |
| echo '<tr><th scope="row">Suggested next action</th><td>' . esc_html( (string) ( $strategy['suggested_next_action'] ?? '' ) ) . '</td></tr>'; | |
| echo '<tr><th scope="row">Confidence</th><td>'; | |
| echo '<strong style="color:' . esc_attr( $strat_color ) . '">' . esc_html( strtoupper( $strat_conf ) ) . '</strong>'; | |
| echo '</td></tr>'; | |
| echo '</tbody></table>'; | |
| // Visual side-by-side comparison | |
| self::render_audit_strategy_comparison( $deterministic_audit, $strategy ); | |
| } | |
| /** | |
| * Side-by-side comparison of Deterministic Audit (layer 2) and Strategy Review (layer 3). | |
| */ | |
| private static function render_audit_strategy_comparison( array $audit, array $strategy ) { | |
| $color_map_conf = [ 'high' => '#0a7d24', 'medium' => '#a76b00', 'low' => '#a02b1f', 'n/a' => '#666' ]; | |
| $audit_conf = (string) ( $audit['closure_confidence'] ?? 'n/a' ); | |
| $strat_conf = (string) ( $strategy['confidence'] ?? 'low' ); | |
| $audit_inc = count( (array) ( $audit['inconsistencies'] ?? [] ) ); | |
| $audit_drift = count( (array) ( $audit['scope_drift'] ?? [] ) ); | |
| $strat_extra = count( (array) ( $strategy['additional_risks'] ?? [] ) ); | |
| $needs_hum = ! empty( $audit['needs_human_arbitration'] ); | |
| echo '<h3 style="margin-top:1.2em">Deterministic Audit ↔ Strategy Review (side-by-side)</h3>'; | |
| echo '<table class="widefat striped" style="max-width:900px"><thead><tr>'; | |
| echo '<th style="width:25%"> </th>'; | |
| echo '<th>Deterministic Audit (layer 2)</th>'; | |
| echo '<th>Strategy Review (layer 3)</th>'; | |
| echo '</tr></thead><tbody>'; | |
| $audit_conf_color = $color_map_conf[ $audit_conf ] ?? '#666'; | |
| $strat_conf_color = $color_map_conf[ $strat_conf ] ?? '#666'; | |
| echo '<tr><th scope="row">Confidence</th>'; | |
| echo '<td><strong style="color:' . esc_attr( $audit_conf_color ) . '">' . esc_html( strtoupper( $audit_conf ) ) . '</strong></td>'; | |
| echo '<td><strong style="color:' . esc_attr( $strat_conf_color ) . '">' . esc_html( strtoupper( $strat_conf ) ) . '</strong></td>'; | |
| echo '</tr>'; | |
| echo '<tr><th scope="row">Findings counted</th>'; | |
| echo '<td>' . (int) $audit_inc . ' inconsistenc' . ( $audit_inc === 1 ? 'y' : 'ies' ) . ', ' . (int) $audit_drift . ' scope-drift indicator' . ( $audit_drift === 1 ? '' : 's' ) . '</td>'; | |
| echo '<td>' . (int) $strat_extra . ' additional risk' . ( $strat_extra === 1 ? '' : 's' ) . '</td>'; | |
| echo '</tr>'; | |
| echo '<tr><th scope="row">Closure authority</th>'; | |
| echo '<td>' . ( $needs_hum ? '<strong>flagged needs_human_arbitration = YES</strong>' : 'no escalation flag' ) . '</td>'; | |
| echo '<td><em>cannot override the audit</em></td>'; | |
| echo '</tr>'; | |
| echo '<tr><th scope="row">Mode / character</th>'; | |
| echo '<td>rules-based, repeatable</td>'; | |
| echo '<td>interpretive, narrative (' . esc_html( (string) ( $strategy['provider_id'] ?? 'simulated' ) ) . ')</td>'; | |
| echo '</tr>'; | |
| echo '</tbody></table>'; | |
| echo '<p style="margin-top:8px"><em>Reminder: <code>closure_state_basic</code>, <code>closure_state_role_aware</code>, <code>closure_authority_valid</code>, and <code>needs_human_arbitration</code> are governed by the deterministic audit (layer 2) and ultimately by human arbitration (layer 4). Strategy Review is advisory.</em></p>'; | |
| } | |
| /* ---------- Recent Arbitrations ---------- */ | |
| public static function render_arbitrations() { | |
| if ( ! current_user_can( self::CAP ) ) wp_die( 'Insufficient permissions.' ); | |
| global $wpdb; | |
| $p = $wpdb->prefix . SA_ORCH_DB_PREFIX; | |
| $rows = $wpdb->get_results( | |
| "SELECT id, demand_surface, demand_ref, projection_type, truth_class, actor_user_id, created_at | |
| FROM {$p}arbitration | |
| ORDER BY created_at DESC, id DESC LIMIT 20" | |
| ); | |
| echo '<div class="wrap">'; | |
| echo '<h1>Recent Arbitrations</h1>'; | |
| echo '<p>Last 20 arbitration records, newest first. Click <strong>Trace</strong> to walk the full chain.</p>'; | |
| if ( empty( $rows ) ) { | |
| echo '<p><em>No arbitrations recorded yet.</em></p>'; | |
| } else { | |
| echo '<table class="widefat striped"><thead><tr>'; | |
| echo '<th>ID</th><th>Demand</th><th>Projection Type</th><th>Truth Class</th><th>Actor</th><th>Created (UTC)</th><th>Action</th>'; | |
| echo '</tr></thead><tbody>'; | |
| foreach ( $rows as $r ) { | |
| $trace_url = admin_url( 'admin.php?page=' . self::PARENT_SLUG | |
| . '&demand_surface=' . urlencode( $r->demand_surface ) | |
| . '&demand_ref=' . urlencode( $r->demand_ref ) ); | |
| echo '<tr>'; | |
| echo '<td>' . (int) $r->id . '</td>'; | |
| echo '<td><code>' . esc_html( $r->demand_surface . ' / ' . $r->demand_ref ) . '</code></td>'; | |
| echo '<td>' . esc_html( (string) $r->projection_type ) . '</td>'; | |
| echo '<td>' . esc_html( (string) $r->truth_class ) . '</td>'; | |
| echo '<td>' . esc_html( (string) ( $r->actor_user_id ?? '—' ) ) . '</td>'; | |
| echo '<td>' . esc_html( (string) $r->created_at ) . '</td>'; | |
| echo '<td><a href="' . esc_url( $trace_url ) . '">Trace</a></td>'; | |
| echo '</tr>'; | |
| } | |
| echo '</tbody></table>'; | |
| } | |
| echo '</div>'; | |
| } | |
| /* ---------- Language Model Providers (settings page) ---------- */ | |
| public static function render_llm_settings() { | |
| if ( ! current_user_can( self::CAP ) ) wp_die( 'Insufficient permissions.' ); | |
| $message = ''; | |
| if ( $_SERVER['REQUEST_METHOD'] === 'POST' ) { | |
| check_admin_referer( 'sa_orch_llm_settings' ); | |
| SA_Orch_LLM_Settings::save( wp_unslash( $_POST ) ); | |
| $message = 'Settings saved.'; | |
| } | |
| $settings = SA_Orch_LLM_Settings::get(); | |
| $choices = SA_Orch_LLM_Providers::get_provider_choices(); | |
| $token_label = SA_Orch_LLM_Settings::token_status_label(); | |
| echo '<div class="wrap">'; | |
| echo '<h1>Language Model Providers</h1>'; | |
| if ( $message ) { | |
| echo '<div class="notice notice-success is-dismissible"><p>' . esc_html( $message ) . '</p></div>'; | |
| } | |
| echo '<p>Configure the provider used by the Strategy Review (layer 3 of the review stack: trace data → deterministic audit → <strong>strategy review</strong> → human arbitration).</p>'; | |
| echo '<p><strong>Local-dev posture:</strong> secrets are stored in <code>wp_options</code>. Tokens are never displayed back — saving with a blank token field preserves the existing value. This storage layer is the attachment seam for a future SA-Core key vault; replacing it later will not change the consumer surface.</p>'; | |
| echo '<form method="post">'; | |
| wp_nonce_field( 'sa_orch_llm_settings' ); | |
| echo '<table class="form-table" role="presentation"><tbody>'; | |
| echo '<tr><th scope="row">Enabled</th><td>'; | |
| echo '<label><input type="checkbox" name="enabled" value="1"' . checked( ! empty( $settings['enabled'] ), true, false ) . ' /> Use the selected provider for Strategy Review.</label>'; | |
| echo '<p class="description">When unchecked, Strategy Review falls back to the simulated provider regardless of the dropdown selection.</p>'; | |
| echo '</td></tr>'; | |
| echo '<tr><th scope="row"><label for="sa_orch_provider">Provider</label></th><td>'; | |
| echo '<select id="sa_orch_provider" name="provider">'; | |
| foreach ( $choices as $id => $label ) { | |
| echo '<option value="' . esc_attr( $id ) . '"' . selected( $settings['provider'], $id, false ) . '>' . esc_html( $label ) . '</option>'; | |
| } | |
| echo '</select>'; | |
| echo '<p class="description">Only <code>simulated</code> is functional in v0.5. Other providers are listed as inert placeholders to surface the seam.</p>'; | |
| echo '</td></tr>'; | |
| echo '<tr><th scope="row"><label for="sa_orch_model">Model name</label></th><td>'; | |
| echo '<input type="text" id="sa_orch_model" name="model" value="' . esc_attr( $settings['model'] ) . '" class="regular-text" placeholder="e.g. gpt-4o, claude-sonnet-4-5, llama-3.1-70b" />'; | |
| echo '<p class="description">Free-form. Future providers may validate against their own catalog.</p>'; | |
| echo '</td></tr>'; | |
| echo '<tr><th scope="row"><label for="sa_orch_api_base_url">API base URL</label></th><td>'; | |
| echo '<input type="text" id="sa_orch_api_base_url" name="api_base_url" value="' . esc_attr( $settings['api_base_url'] ) . '" class="regular-text" placeholder="e.g. https://api.openai.com/v1" />'; | |
| echo '<p class="description">Used by HTTP-backed providers. Ignored by the simulated provider.</p>'; | |
| echo '</td></tr>'; | |
| echo '<tr><th scope="row"><label for="sa_orch_api_token">API token / key</label></th><td>'; | |
| echo '<input type="password" id="sa_orch_api_token" name="api_token" value="" class="regular-text" autocomplete="new-password" placeholder="enter token to set or update; leave blank to preserve" />'; | |
| echo '<p class="description">' . esc_html( $token_label ) . ' Tokens are stored in <code>wp_options</code> for local dev only and are never displayed back through the UI. Leave the field blank to keep the existing value.</p>'; | |
| echo '</td></tr>'; | |
| echo '</tbody></table>'; | |
| echo '<p class="submit"><button type="submit" class="button button-primary">Save Settings</button></p>'; | |
| echo '</form>'; | |
| echo '<hr />'; | |
| echo '<h2>Status</h2>'; | |
| echo '<table class="widefat striped" style="max-width:600px"><tbody>'; | |
| echo '<tr><th scope="row" style="width:40%">Active provider (resolved)</th><td><code>' . esc_html( SA_Orch_LLM_Providers::get_active_provider()->id() ) . '</code></td></tr>'; | |
| echo '<tr><th scope="row">Settings.enabled</th><td><code>' . ( ! empty( $settings['enabled'] ) ? 'true' : 'false' ) . '</code></td></tr>'; | |
| echo '<tr><th scope="row">Settings.provider</th><td><code>' . esc_html( (string) $settings['provider'] ) . '</code></td></tr>'; | |
| echo '<tr><th scope="row">Settings.model</th><td><code>' . esc_html( (string) $settings['model'] ?: '—' ) . '</code></td></tr>'; | |
| echo '<tr><th scope="row">Settings.api_base_url</th><td><code>' . esc_html( (string) $settings['api_base_url'] ?: '—' ) . '</code></td></tr>'; | |
| echo '<tr><th scope="row">Settings.api_token</th><td>' . ( SA_Orch_LLM_Settings::has_token() ? '<code>***** (set)</code>' : '<code>(not set)</code>' ) . '</td></tr>'; | |
| echo '</tbody></table>'; | |
| echo '<p style="margin-top:8px"><em>v0.5 ships with the <code>simulated</code> provider only — no external API call is made by this plugin under any settings configuration. Adding real providers (OpenAI, Anthropic, local model endpoint, custom HTTP) is a matter of implementing <code>SA_Orch_LLM_Provider_Interface</code> and adding a case to <code>SA_Orch_LLM_Providers::instantiate()</code>.</em></p>'; | |
| echo '</div>'; | |
| } | |
| /* ---------- Acceptance Events ---------- */ | |
| public static function render_events() { | |
| if ( ! current_user_can( self::CAP ) ) wp_die( 'Insufficient permissions.' ); | |
| global $wpdb; | |
| $p = $wpdb->prefix . SA_ORCH_DB_PREFIX; | |
| $rows = $wpdb->get_results( | |
| "SELECT id, event_type, actor_role, demand_surface, demand_ref, observed_at, truth_class, actor_user_id | |
| FROM {$p}acceptance_event | |
| ORDER BY observed_at DESC, id DESC LIMIT 20" | |
| ); | |
| echo '<div class="wrap">'; | |
| echo '<h1>Acceptance Events</h1>'; | |
| echo '<p>Last 20 acceptance events, newest first. Append-only — these rows are never updated or deleted.</p>'; | |
| if ( empty( $rows ) ) { | |
| echo '<p><em>No acceptance events recorded yet.</em></p>'; | |
| } else { | |
| echo '<table class="widefat striped"><thead><tr>'; | |
| echo '<th>ID</th><th>Event Type</th><th>Actor Role</th><th>Actor</th><th>Demand</th><th>Truth Class</th><th>Observed (UTC)</th><th>Action</th>'; | |
| echo '</tr></thead><tbody>'; | |
| foreach ( $rows as $r ) { | |
| $trace_url = admin_url( 'admin.php?page=' . self::PARENT_SLUG | |
| . '&demand_surface=' . urlencode( $r->demand_surface ) | |
| . '&demand_ref=' . urlencode( $r->demand_ref ) ); | |
| echo '<tr>'; | |
| echo '<td>' . (int) $r->id . '</td>'; | |
| echo '<td>' . esc_html( (string) $r->event_type ) . '</td>'; | |
| echo '<td>' . esc_html( (string) $r->actor_role ) . '</td>'; | |
| echo '<td>' . esc_html( (string) ( $r->actor_user_id ?? '—' ) ) . '</td>'; | |
| echo '<td><code>' . esc_html( $r->demand_surface . ' / ' . $r->demand_ref ) . '</code></td>'; | |
| echo '<td>' . esc_html( (string) $r->truth_class ) . '</td>'; | |
| echo '<td>' . esc_html( (string) $r->observed_at ) . '</td>'; | |
| echo '<td><a href="' . esc_url( $trace_url ) . '">Trace</a></td>'; | |
| echo '</tr>'; | |
| } | |
| echo '</tbody></table>'; | |
| } | |
| echo '</div>'; | |
| } | |
| } | |