diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 6dd87bd..3fb2744 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -121,13 +121,13 @@ prautoblogger/ │ │ └── OFL.txt # SIL Open Font License │ ├── css/ │ │ ├── admin.css # Admin page styles (wp-admin conventions) -│ │ ├── board.css # Kanban board styles -- warm editorial palette (v0.19.0) +│ │ ├── board.css # Mission Brief board styles -- verdict chips, run-list, inspector (v0.19.0, v0.27.0) │ │ ├── dossier.css # Article dossier page styles: verdict pills, cost receipt, trace toggle (v0.19.2) │ │ ├── dossier-edit.css # M3 edit/re-run layer: chips, edit panel, image grid, spend warning (v0.20.0) │ │ └── posts-widget.css # Frontend posts widget styles (uses theme CSS vars) │ └── js/ │ ├── admin.js # Admin page interactivity (vanilla JS / Alpine.js) -│ ├── board.js # Board poller: AJAX poll + DOM updates + backoff (v0.19.0) +│ ├── board.js # Mission Brief board: AJAX poll, run-list render, inspector rail (v0.19.0, v0.27.0) │ ├── dossier.js # Per-stage raw-trace toggle: aria-expanded / aria-controls (v0.19.2) │ ├── dossier-edit.js # M3 edit/re-run actions + stage-status polling (queued -> pickup -> result) (v0.20.0) │ └── posts-widget.js # React component for frontend post cards (wp.element) @@ -146,8 +146,9 @@ prautoblogger/ │ ├── class-autoloader.php # PSR-4-style autoloader for plugin classes │ │ │ ├── admin/ -│ │ ├── class-board-page.php # Kanban board -- primary landing screen (v0.19.0) -│ │ ├── class-board-data-provider.php # Board orchestrator: 4-column snapshot + dossier deep-links (v0.19.0, v0.19.2) +│ │ ├── class-board-page.php # Mission Brief board -- primary landing screen (v0.19.0, v0.27.0) +│ │ ├── class-board-data-provider.php # Board orchestrator: run-list snapshot + stage dots (v0.19.0, v0.27.0) +│ │ ├── class-board-stage-dots.php # Dot-rail enrichment: batched run_stages query per board section (M5, v0.27.0) │ │ ├── class-board-gen-log-query.php # Board gen_log queries: Generating + Failed column raw DB reads (v0.19.0) │ │ ├── class-dossier-page.php # Article dossier admin page: options.php-parent hidden-page, priority 12, enqueue, nonce (v0.19.3) │ │ ├── class-dossier-data-assembler.php # 5-query view-model builder: runs+stages+gen_log+decisions+meta (v0.19.2) @@ -173,7 +174,8 @@ prautoblogger/ │ │ ├── class-model-registry-refresh.php # AJAX: refresh OpenRouter model registry (manual trigger) │ │ ├── class-pipeline-preview-handler.php # M3: assembled-instructions preview from last-run gen_log (v0.25.0) │ │ ├── class-pipeline-history-handler.php # M3: prompt version list + LCS diff (history + diff actions) (v0.25.0) -│ │ └── class-gen-run-io-handler.php # M4: per-run stage I/O drill-down (prautoblogger_gen_run_io) (v0.26.0) +│ │ ├── class-gen-run-io-handler.php # M4: per-run stage I/O drill-down (prautoblogger_gen_run_io) (v0.26.0) +│ │ └── class-board-inspector-handler.php # M5: per-run stage inspector AJAX (prautoblogger_board_inspector) (v0.27.0) │ │ │ ├── core/ │ │ ├── class-scheduler.php # WP-Cron / Action Scheduler job management @@ -273,7 +275,7 @@ prautoblogger/ │ ├── templates/ │ └── admin/ -│ ├── board-page.php # Kanban board template (4-column editorial layout) (v0.19.0) +│ ├── board-page.php # Mission Brief board template (run-list + inspector layout) (v0.19.0, v0.27.0) │ ├── dossier-page.php # Article dossier: two-column layout, sidebar, stage sections (v0.19.2) │ ├── dossier-stage-section.php # Stage section partial: output, raw trace, M3 chips + rerun footer │ ├── dossier-edit-panel.php # M3 per-stage edit panel: message textareas, fork info, spend strip (v0.20.0) @@ -677,7 +679,7 @@ All prefixed with `prautoblogger_`: | `prautoblogger_image_compose_variants` | Comma list of composed variants for Image A (default 'og,square'; whitelisted at point of use) | | `prautoblogger_image_featured_mark_enabled` | Toggle: subtle corner mark on featured images (default '1') | | `prautoblogger_image_compose_capability` | Cached capability probe `{fingerprint, capability}`; fingerprint = PHP version + imagick/gd presence, so it auto-invalidates on host changes | -| `prautoblogger_board_poll_interval` | Kanban board poll interval in seconds (default 5, min 3). Localized into board.js. | +| `prautoblogger_board_poll_interval` | Mission Brief board poll interval in seconds (default 5, min 3). Localized into board.js. | | `prautoblogger_board_published_window_days` | Days back to show in the Published column (default 7). | ### Post Meta @@ -1010,16 +1012,15 @@ Tests use Brain\Monkey for WordPress function mocking (no database required). Th **Hidden admin page convention (v0.19.3):** See CONVENTIONS.md §Hidden Admin Pages for the full rule. Summary: hidden admin pages (link-accessed only, not visible in nav) MUST use `options.php` as the `add_submenu_page()` parent. NEVER unset/remove registered pages from `$submenu` after registration -- doing so causes `get_admin_page_parent()` to fail at request time, which makes WP recompute the hookname in the `admin_page_*` orphan namespace, find no registered handler, and call `wp_die(403)`. This is the dossier 403 incident (v0.19.2→v0.19.3). Same hookname-mismatch class as the board 404 (v0.19.0→v0.19.1); the only difference is that the board bug was a priority-ordering issue at registration time while the dossier bug was a post-registration $submenu mutation. -The primary admin screen is now a kanban board (Direction C "Editorial Record") with four -columns: Generating | In Review | Published | Failed. A separate `class-board-page.php` -registers the `Board` submenu and a `$submenu` reorder makes it the first (primary) entry. -`class-board-data-provider.php` orchestrates the four columns and handles WP_Query columns -(In Review, Published). Raw `prab_generation_log` queries (Generating, Failed) are in the -extracted `class-board-gen-log-query.php` (split for 300-line compliance) -- no new schema -required for M1. The data provider uses the `prautoblogger_generation_status` transient -(written by the Executor for manual/daily runs) as the primary active-run signal, with a -secondary query on recent `prab_generation_log` run_ids that have no linked post as a -fallback. M2 will rename Runs & Audit -> Articles and wire board cards to the Article Dossier. +The primary admin screen is now a Mission Brief board (M5, v0.27.0) -- a vertical run-list ++ right-rail inspector layout (CEO-selected Direction C "Mission Brief"). A separate +`class-board-page.php` registers the `Board` submenu and a `$submenu` reorder makes it +the first (primary) entry. `class-board-data-provider.php` orchestrates the four status +sections (Generating | In review | Published | Failed) and enriches each card with a +lightweight `run_stages_summary` for the dot-rail. Raw `prab_generation_log` queries +(Generating, Failed) remain in `class-board-gen-log-query.php`. The right-rail inspector +is powered by `ajax/class-board-inspector-handler.php`, which reuses +`PRAutoBlogger_Gen_History_Query::get_run_io()` from M4 (no new DB queries/schema). Poll interval and published window are settings-backed, localized into board.js, never hardcoded. diff --git a/CHANGELOG.md b/CHANGELOG.md index fa6457a..2de88b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,54 @@ All notable changes to PRAutoBlogger will be documented in this file. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project uses [Semantic Versioning](https://semver.org/). +## [0.27.0] - 2026-06-23 + +### Fixed (QA P1/P2 sweep — post-3cd2843) +- **P1-C (regression):** Restored `get_option( 'prautoblogger_board_column_limit', PRAUTOBLOGGER_DEFAULT_BOARD_COLUMN_LIMIT )` + in both `get_in_review_cards()` and `get_published_cards()` — the M5 commit had hard-coded + `posts_per_page => 20`, silently bypassing the board column-limit admin setting. +- **P1-D (test):** Removed invalid `Functions\when( 'PRAutoBlogger_Run_Stage_State::is_available' )` stub + from `BoardInspectorHandlerTest.php` line 99. `Brain\Monkey\Functions\when()` only intercepts + global functions; the static-method stub was a silent no-op violating the repo rule against + `Functions\when()` on `Class::method`. +- **P1-A (docs):** Added `class-board-stage-dots.php` (admin/) and `class-board-inspector-handler.php` + (ajax/) to the ARCHITECTURE.md file tree. +- **P1-B (docs):** Added Mission Brief domain vocabulary (Mission Brief, inspector rail, dot-rail, + run_stages_summary) to CONTEXT.md. +- **P2-A:** Deleted dead `assets/js/board-generate.js` (no longer enqueued; button gone in M5). +- **P2-B:** Added `return;` after `wp_send_json_error()` in + `class-board-inspector-handler.php` handle() for explicit early-exit consistency. + +### Added +- **Pipeline Board M5 -- Mission Brief (board redesign, CEO-selected Direction C):** + + - **Vertical run list:** replaces the four-column kanban with a status-grouped vertical + list (Generating | In review | Published | Failed). Each row shows a verdict-style + status chip, article title, current stage, cost (IBM Plex Mono), and elapsed time. + Stalled/failed rows have a red left-border accent and are never softened to grey. + + - **Persistent right-rail inspector:** selecting any run row fetches full per-stage + I/O via new `PRAutoBlogger_Board_Inspector_Handler` (AJAX) and populates the rail + without navigation. Inspector shows: stage breakdown with status dots, model, + per-stage cost, expandable prompt/response text (textContent -- XSS-safe), total + cost receipt, and an "Open dossier" CTA. + + - **Reuses M4 data layer:** `PRAutoBlogger_Gen_History_Query::get_run_io()` and + `get_run_meta()` power the inspector -- no new DB queries or schema changes. + + - **Stage-display-map driven:** stage labels resolve via `Stage_Display_Map::label()`; + Phase 2b stages (curate/seo) appear automatically. + + - **New Article action** preserved (top-right of board heading, links to Ideas Browser). + + - All existing board capabilities preserved: per-run dossier deep-link, status counts, + live AJAX polling with backoff, published-window filter, poll-interval setting, + human-modified badge, error banner on poll failure, empty state per section. + + - **Security:** inspector AJAX uses `prautoblogger_board` nonce (same page as poller), + `manage_options` cap; all output `esc_html`'d before JSON; JS renders prompt/response + via `textContent`, never `innerHTML`. API key never exposed (architecture contract). + ## [0.26.0] - 2026-06-23 ### Added diff --git a/CONTEXT.md b/CONTEXT.md index 2a666cd..4967e10 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -146,3 +146,14 @@ Full table names are defined in `class-activator.php`. | **output** | The LLM's raw response text from `run_stages.meta_json.output`. Null when the stage is log-only (image_a, image_b, llm_research, image_prompt_rewrite — no run_stages row). | | **output_pruned** | True when `run_stages.meta_json` exists but has no `output` key — indicating the output was pruned by the `prautoblogger_request_json_retention_days` setting. The UI shows an explicit message instead of a blank field. | | **log-only stage** | A generation_log stage with no corresponding run_stages row. Image stages (image_a, image_b, image_prompt_rewrite) and llm_research fall here. Their output is null (not pruned) in the drill-down. | + +--- + +## Pipeline Board M5 — Mission Brief (v0.27.0) + +| Term | Definition | +|------|-----------| +| **Mission Brief** | The new board layout (CEO-selected Direction C, v0.27.0). Replaces the four-column kanban with a status-grouped vertical run list (Generating / In Review / Published / Failed). Each section is independently expandable; the board name reflects the editorial-command framing of the design. Class: `PRAutoBlogger_Board_Page`. | +| **inspector rail** | The persistent right-hand panel that opens when a run row is clicked. Fetches per-stage I/O via AJAX (`prautoblogger_board_inspector`) and renders the full stage breakdown — status dots, model, cost, expandable prompt/response text, total cost receipt, and an "Open dossier" CTA — without navigating away from the board. Class: `PRAutoBlogger_Board_Inspector_Handler`. | +| **dot-rail** | The lightweight row-level stage-progress indicator in the Mission Brief run list. Each stage in the run is represented by a small dot coloured by its status (e.g. complete = teal, failed = red). Populated by `PRAutoBlogger_Board_Stage_Dots::enrich()`, which adds a `run_stages_summary` key to each card. | +| **run_stages_summary** | A card-level enrichment key added by `PRAutoBlogger_Board_Stage_Dots::enrich()`. Array of `{stage, status}` objects for each stage in the run, sourced from the `wp_prautoblogger_run_stages` table in a single batched query per board section. Consumed by `board.js` to render the dot-rail. | diff --git a/INFORMATION-ARCHITECTURE.md b/INFORMATION-ARCHITECTURE.md index 3114626..3288c73 100644 --- a/INFORMATION-ARCHITECTURE.md +++ b/INFORMATION-ARCHITECTURE.md @@ -10,7 +10,7 @@ Maintained per DoD v1.1.0 (same-PR update rule). | Slug | Class | Template | Purpose | |------|-------|----------|---------| | `prautoblogger-settings` | `PRAutoBlogger_Admin_Page` | `admin-page.php` | Main settings: API Keys, Schedule & Budget, Publishing, Analytics, Display, Images. AI Models / Content / Sources retired to Pipeline Settings in M2 (v0.24.0). | -| `prautoblogger-board` | `PRAutoBlogger_Board_Page` | `board-page.php` | Kanban board: Generating / Failed / In Review / Published columns. New Article button (v0.21.0). | +| `prautoblogger-board` | `PRAutoBlogger_Board_Page` | `board-page.php` | Mission Brief board (M5): vertical run-list + right-rail inspector. Status sections: Generating / In review / Published / Failed. New Article button. Stage dot-rail. Inspector: per-stage I/O, cost receipt, dossier link (v0.21.0, v0.27.0). | | `prautoblogger-pipeline` | `PRAutoBlogger_Pipeline_Settings_Page` | `pipeline-settings-page.php` | Per-step pipeline config: Global Content Context (niche), step option fields, model picker, system instructions, agent prompts, params (M1 v0.23.0; M2 v0.24.0 adds editable step options, retires AI Models/Content/Sources Settings tabs). M3 v0.25.0 adds Template/Preview toggle (assembled-instructions preview from last-run gen_log), version history accordion, and inline diff panel. Three new AJAX endpoints: `prautoblogger_pipeline_preview`, `prautoblogger_pipeline_history`, `prautoblogger_pipeline_diff` (all `manage_options` + nonce gated). | | `prautoblogger-gen-history` | `PRAutoBlogger_Gen_History_Page` | `gen-history-page.php` | M4: Paginated list of all generation runs — title, date, status, models, cost, duration; Stage I/O toggle for inline per-step input/output drill-down (AJAX). Hidden options.php-parent page. `manage_options` + `prautoblogger_gen_run_io` nonce. | | `prautoblogger-dossier` | `PRAutoBlogger_Dossier_Page` | `dossier-page.php` | Per-article generation log + stage edit/re-run (M3). Preferred per-step I/O surface for runs with a linked post. | @@ -65,7 +65,7 @@ DB version history: 1.0→1.1 (schema normalisation), 1.1→1.2 (M2 dossier), 1. | Option | Type | Default | Purpose | |--------|------|---------|---------| -| `prautoblogger_board_column_limit` | int | 20 | Max cards per board column | +| `prautoblogger_board_column_limit` | int | 20 | Max runs per board section | | `prautoblogger_ideas_per_page` | int | 30 | Ideas browser page size | | `prautoblogger_article_queue` | array | — | Checkpoint queue (run_id + ideas + results) | | `prautoblogger_checkpoint_run_id` | string | — | Current run UUID for Tick 2..N finalize path | diff --git a/assets/css/board.css b/assets/css/board.css index 52d1110..02e0751 100644 --- a/assets/css/board.css +++ b/assets/css/board.css @@ -1,249 +1,550 @@ /** - * PRAutoBlogger — Kanban Board Styles + * PRAutoBlogger — Mission Brief Board Styles (M5, v0.27.0) * - * Warm editorial style (Proposal C / Direction C). - * Inside wp-admin chrome; uses WP admin vars where available. - * No build step — plain CSS, no preprocessor. + * Replaces the four-column kanban with a vertical run-list + right-rail + * inspector layout (Option C / "Mission Brief"). * - * Palette: - * Teal #1B8A92 (brand primary — Generating pill, spinner) - * Cream #FAF6EE (card background) - * Ink #0D0D0D (primary text) - * Orange #FF8A3D (In Review pill) - * Lime #7FD600 (Published pill — success) - * Red #D94F4F (Failed pill) - * Mid-grey #8A8A8A (meta text, border) + * Design language (locked by CMO consult 2026-06-20): + * Surface: #FFFFFF (bg-page / bg-surface) + * Inset: #F5F5F5 (bg-inset — sections, inspector bg) + * Primary: #1B8A92 (teal-500 — Generating chip, focus ring, CTA) + * Hover: #147178 (teal-600) + * Lime: #7FD600 (lime-500 — Published chip) + * Orange: #FF8A3D (orange-500 — In Review chip) + * Error: #C0392B (cautionary — Failed chip, failed row border) + * Text: #0D0D0D / #3D3D3D / #808080 + * Border: #E8E8E8 * - * @see templates/admin/board-page.php — Rendered HTML structure. - * @see assets/js/board.js — DOM updates that match these selectors. + * Fonts: Poppins (display), Inter (body/UI), IBM Plex Mono (all numerics). + * + * No dark mode. No glassmorphism. No glow effects. + * Namespace: .prab-board-wrap and .prab-* selectors only. + * + * @see templates/admin/board-page.php -- Rendered HTML structure. + * @see assets/js/board.js -- DOM updates that match these selectors. */ +/* ── Google Fonts load (same subset as theme — no second copy if already loaded) */ +@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@600;700&family=Inter:wght@400;500;600&family=IBM+Plex+Mono:wght@400;600&display=swap'); + /* ── Page wrapper ──────────────────────────────────────────────────────── */ .prab-board-wrap { padding: 0; } +/* ── Top bar: heading + New Article CTA ───────────────────────────────── */ +.prab-board-topbar { + display: flex; + align-items: baseline; + justify-content: space-between; + padding: 18px 20px 12px; + border-bottom: 1px solid #E8E8E8; +} + .prab-board-heading { - font-size: 1.4rem; - font-weight: 700; - color: #0d0d0d; - margin: 20px 0 18px 0; - padding: 0 20px; + font-family: 'Poppins', sans-serif; + font-size: 20px; + font-weight: 600; + color: #0D0D0D; + margin: 0; display: flex; align-items: baseline; gap: 10px; } .prab-board-heading-sub { - font-size: 1rem; + font-family: 'Inter', sans-serif; + font-size: 13px; font-weight: 400; - color: #8a8a8a; + color: #808080; } -/* ── Four-column board grid ────────────────────────────────────────────── */ -.prab-board { +.prab-board-new-article-btn { + display: inline-flex; + align-items: center; + padding: 7px 16px; + background: #1B8A92; + color: #FFFFFF; + border-radius: 8px; + font-family: 'Inter', sans-serif; + font-size: 13px; + font-weight: 600; + text-decoration: none; + transition: background 0.15s ease; +} + +.prab-board-new-article-btn:hover, +.prab-board-new-article-btn:focus { + background: #147178; + color: #FFFFFF; + outline: 2px solid #1B8A92; + outline-offset: 2px; +} + +/* ── Two-pane Mission Brief layout ────────────────────────────────────── */ +.prab-board--mission-brief { display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 16px; - padding: 0 20px 24px; + grid-template-columns: 60% 40%; align-items: start; + min-height: 400px; } -@media (max-width: 1200px) { - .prab-board { - grid-template-columns: repeat(2, 1fr); - } -} - -@media (max-width: 720px) { - .prab-board { +@media (max-width: 900px) { + .prab-board--mission-brief { grid-template-columns: 1fr; } } -/* ── Column ────────────────────────────────────────────────────────────── */ -.prab-board-column { - background: #f8f7f5; - border: 1px solid #e2ddd6; - border-radius: 8px; - min-height: 180px; +/* ── Run list (left pane) ─────────────────────────────────────────────── */ +.prab-run-list { + border-right: 1px solid #E8E8E8; + min-height: 400px; + padding-bottom: 24px; } -.prab-col-header { +/* ── Section ──────────────────────────────────────────────────────────── */ +.prab-section { + border-bottom: 1px solid #E8E8E8; +} + +.prab-section:last-child { + border-bottom: none; +} + +.prab-section-header { display: flex; align-items: center; justify-content: space-between; - padding: 12px 14px 10px; - border-bottom: 1px solid #e2ddd6; + padding: 10px 16px 8px; + background: #F5F5F5; + border-bottom: 1px solid #E8E8E8; } -.prab-col-title { - font-size: 0.82rem; +.prab-section-label { + font-family: 'Inter', sans-serif; + font-size: 11px; font-weight: 600; text-transform: uppercase; - letter-spacing: 0.06em; - color: #4a4540; + letter-spacing: 0.08em; + color: #3D3D3D; } -.prab-col-cards { - padding: 10px 10px 12px; - display: flex; - flex-direction: column; - gap: 8px; +.prab-section-rows { + padding: 4px 0; } -.prab-col-empty { - font-size: 0.82rem; - color: #b0a898; - text-align: center; - padding: 20px 0; +.prab-section-empty { + font-family: 'Inter', sans-serif; + font-size: 13px; + color: #808080; + padding: 14px 16px; margin: 0; } -/* ── Status pills ──────────────────────────────────────────────────────── */ -.prab-pill { +/* ── Verdict-style status chips ───────────────────────────────────────── */ +.prab-chip { display: inline-flex; align-items: center; justify-content: center; - padding: 1px 8px; - border-radius: 12px; - font-size: 0.75rem; + padding: 2px 10px; + border-radius: 999px; + font-family: 'Inter', sans-serif; + font-size: 11px; font-weight: 600; - line-height: 1.5; - color: #fff; - min-width: 24px; + line-height: 1.6; + white-space: nowrap; } -.prab-pill--generating { - background: #1b8a92; +.prab-chip--generating { + background: #1B8A92; + color: #FFFFFF; } -.prab-pill--review { - background: #ff8a3d; +.prab-chip--in_review { + background: #FF8A3D; + color: #0D0D0D; } -.prab-pill--published { - background: #5a9e28; +.prab-chip--published { + background: #7FD600; + color: #0D0D0D; } -.prab-pill--failed { - background: #d94f4f; +.prab-chip--failed { + background: #C0392B; + color: #FFFFFF; } -/* ── Card ──────────────────────────────────────────────────────────────── */ -.prab-board-card { - background: #faf6ee; - border: 1px solid #e0d8cc; - border-radius: 6px; - padding: 11px 12px 10px; - position: relative; - transition: box-shadow 0.15s ease, border-color 0.15s ease; +.prab-chip--human { + background: #1B8A92; + color: #FFFFFF; + font-size: 9px; + padding: 1px 6px; + margin-left: 4px; } -.prab-board-card:hover { - box-shadow: 0 2px 8px rgba(27, 138, 146, 0.12); - border-color: #b8cfd2; +/* ── Run row ──────────────────────────────────────────────────────────── */ +.prab-run-row { + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + gap: 12px; + padding: 10px 16px; + border-left: 3px solid transparent; + cursor: pointer; + transition: background 0.1s ease, border-color 0.1s ease; + background: #FFFFFF; } -.prab-board-card--generating { - border-left: 3px solid #1b8a92; +.prab-run-row:hover, +.prab-run-row:focus { + background: #F5F5F5; + outline: none; } -.prab-card-title { - font-size: 0.875rem; +.prab-run-row.is-selected { + background: #F0F9FA; + border-left-color: #1B8A92; +} + +.prab-run-row--failed { + border-left-color: #C0392B; +} + +.prab-run-row--failed.is-selected { + border-left-color: #C0392B; + background: #FFF5F5; +} + +/* Chip column */ +.prab-row-chip-wrap { + display: flex; + align-items: center; + gap: 4px; + flex-shrink: 0; +} + +/* Title + stage label column */ +.prab-row-main { + overflow: hidden; +} + +.prab-row-title { + font-family: 'Inter', sans-serif; + font-size: 14px; font-weight: 500; - color: #0d0d0d; - line-height: 1.4; - margin-bottom: 6px; - padding-right: 20px; /* room for spinner */ + color: #0D0D0D; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -.prab-card-meta { +.prab-run-row--failed .prab-row-title { + color: #C0392B; +} + +.prab-row-stage-label { + font-family: 'Inter', sans-serif; + font-size: 11px; + color: #808080; + margin-top: 2px; +} + +.prab-row-error { + font-family: 'Inter', sans-serif; + font-size: 11px; + color: #C0392B; + margin-top: 2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Cost + elapsed column */ +.prab-row-meta { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 2px; + flex-shrink: 0; +} + +.prab-row-cost, +.prab-row-elapsed { + font-family: 'IBM Plex Mono', monospace; + font-size: 12px; + color: #3D3D3D; + white-space: nowrap; +} + +.prab-row-elapsed { + color: #808080; +} + +/* ── Inspector rail (right pane) ─────────────────────────────────────── */ +.prab-inspector { + background: #F5F5F5; + min-height: 400px; + position: sticky; + top: 32px; /* WP admin bar height */ + max-height: calc(100vh - 60px); + overflow-y: auto; +} + +.prab-inspector-inner { + padding: 20px; +} + +.prab-inspector-placeholder { + font-family: 'Inter', sans-serif; + font-size: 13px; + color: #808080; + text-align: center; + padding: 40px 20px; + margin: 0; +} + +/* Inspector header */ +.prab-inspector-title { + font-family: 'Poppins', sans-serif; + font-size: 16px; + font-weight: 600; + color: #0D0D0D; + margin: 10px 0 4px; + word-break: break-word; +} + +.prab-inspector-chip-row { display: flex; align-items: center; gap: 8px; - margin-bottom: 5px; + margin-bottom: 16px; + flex-wrap: wrap; } -.prab-card-cost { - font-size: 0.75rem; - color: #5a8a50; - font-weight: 600; - font-variant-numeric: tabular-nums; +/* Inspector stage breakdown */ +.prab-inspector-stages { + margin: 0 0 16px; + padding: 0; + list-style: none; } -.prab-card-stage { - font-size: 0.74rem; - color: #1b8a92; - font-weight: 500; - margin-bottom: 5px; +.prab-inspector-stage { + display: grid; + grid-template-columns: 12px 1fr auto; + align-items: start; + gap: 8px; + padding: 8px 0; + border-bottom: 1px solid #E8E8E8; } -.prab-card-error { - font-size: 0.74rem; - color: #d94f4f; - margin-bottom: 5px; - font-style: italic; +.prab-inspector-stage:last-child { + border-bottom: none; } -.prab-card-actions { - margin-top: 8px; -} - -.prab-card-link { - display: inline-block; - font-size: 0.75rem; - color: #1b8a92; - text-decoration: none; - font-weight: 500; - border: 1px solid #b8d8db; - border-radius: 4px; - padding: 2px 8px; - transition: background 0.1s ease; -} - -.prab-card-link:hover { - background: #e8f4f5; - color: #136870; - border-color: #8ac4c9; -} - -/* ── Generating spinner ────────────────────────────────────────────────── */ -.prab-card-spinner { - position: absolute; - top: 11px; - right: 11px; - width: 14px; - height: 14px; - border: 2px solid #c8e6e8; - border-top-color: #1b8a92; +.prab-stage-dot { + width: 10px; + height: 10px; border-radius: 50%; - animation: prab-spin 0.9s linear infinite; + margin-top: 3px; + flex-shrink: 0; + background: #D0D0D0; } -@keyframes prab-spin { - to { transform: rotate(360deg); } +.prab-stage-dot--done { background: #1B8A92; } +.prab-stage-dot--active { background: #1B8A92; animation: prab-pulse 2s ease-in-out infinite; } +.prab-stage-dot--failed { background: #C0392B; } +.prab-stage-dot--pending { background: #D0D0D0; } + +@keyframes prab-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } } -/* ── Board footer ──────────────────────────────────────────────────────── */ -.prab-board-footer { - padding: 0 20px 16px; - font-size: 0.78rem; - color: #a0998f; +.prab-stage-info { + overflow: hidden; +} + +.prab-stage-name { + font-family: 'Inter', sans-serif; + font-size: 12px; + font-weight: 600; + color: #3D3D3D; +} + +.prab-stage-model { + font-family: 'IBM Plex Mono', monospace; + font-size: 11px; + color: #808080; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.prab-stage-error-msg { + font-family: 'Inter', sans-serif; + font-size: 11px; + color: #C0392B; + margin-top: 2px; +} + +.prab-stage-io-toggle { + font-family: 'Inter', sans-serif; + font-size: 10px; + color: #1B8A92; + background: none; + border: none; + cursor: pointer; + padding: 0; + text-decoration: underline; +} + +.prab-stage-io { + grid-column: 2 / 4; + margin-top: 6px; + background: #FFFFFF; + border: 1px solid #E8E8E8; + border-radius: 4px; + padding: 8px; +} + +.prab-stage-io-label { + font-family: 'Inter', sans-serif; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: #808080; + margin-bottom: 4px; +} + +.prab-stage-io-text { + font-family: 'IBM Plex Mono', monospace; + font-size: 11px; + color: #0D0D0D; + white-space: pre-wrap; + word-break: break-word; + max-height: 200px; + overflow-y: auto; + margin: 0; +} + +.prab-stage-cost { + font-family: 'IBM Plex Mono', monospace; + font-size: 12px; + color: #3D3D3D; + flex-shrink: 0; + align-self: start; + padding-top: 3px; +} + +/* Inspector receipt */ +.prab-inspector-receipt { + background: #FFFFFF; + border: 1px solid #E8E8E8; + border-radius: 8px; + padding: 12px 16px; + margin-bottom: 16px; +} + +.prab-receipt-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 4px 0; + font-family: 'Inter', sans-serif; + font-size: 13px; + color: #3D3D3D; +} + +.prab-receipt-label { + color: #808080; +} + +.prab-receipt-value { + font-family: 'IBM Plex Mono', monospace; + font-weight: 600; + color: #0D0D0D; +} + +/* Inspector CTA buttons */ +.prab-inspector-actions { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin-top: 4px; +} + +.prab-inspector-dossier-btn { + display: inline-flex; + align-items: center; + padding: 7px 16px; + background: #1B8A92; + color: #FFFFFF; + border-radius: 8px; + font-family: 'Inter', sans-serif; + font-size: 13px; + font-weight: 600; + text-decoration: none; + transition: background 0.15s ease; +} + +.prab-inspector-dossier-btn:hover, +.prab-inspector-dossier-btn:focus { + background: #147178; + color: #FFFFFF; } /* ── Poll-error banner ─────────────────────────────────────────────────── */ .prab-board-error { - background: #fff4f4; - border: 1px solid #f5c4c4; - color: #8b2020; + background: #FFF4F4; + border: 1px solid #F5C4C4; + color: #8B2020; border-radius: 6px; - padding: 8px 14px; - margin: 0 20px 12px; - font-size: 0.82rem; + padding: 8px 16px; + margin: 8px 20px; + font-family: 'Inter', sans-serif; + font-size: 13px; display: none; } .prab-board-error.is-visible { display: block; } + +/* ── Board footer ──────────────────────────────────────────────────────── */ +.prab-board-footer { + padding: 10px 20px 16px; + font-family: 'Inter', sans-serif; + font-size: 12px; + color: #808080; + border-top: 1px solid #E8E8E8; +} + +/* ── Inspector loading / error states ─────────────────────────────────── */ +.prab-inspector-loading, +.prab-inspector-error { + font-family: 'Inter', sans-serif; + font-size: 13px; + color: #808080; + text-align: center; + padding: 30px 16px; + margin: 0; +} + +.prab-inspector-error { + color: #C0392B; +} + +/* ── Narrow viewport: inspector becomes full-width below list ─────────── */ +@media (max-width: 900px) { + .prab-run-list { + border-right: none; + border-bottom: 1px solid #E8E8E8; + } + + .prab-inspector { + position: static; + max-height: none; + } +} diff --git a/assets/js/board-generate.js b/assets/js/board-generate.js deleted file mode 100644 index 2cd6833..0000000 --- a/assets/js/board-generate.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * PRAutoBlogger Board -- "New Article" generate button handler (v0.21.0, M4). - * - * Loaded on the board page alongside board.js. Depends on the prabBoard config - * object and jQuery, both already present when board.js loads. - * - * @see assets/js/board.js -- Board poller (loads first). - * @see includes/admin/class-board-page.php -- Enqueues + localizes prabBoard. - */ -( function ( $ ) { - 'use strict'; - - var cfg = window.prabBoard || {}; - - /** - * Handle click on the "New Article" button. Fires the existing generate_now - * AJAX action and triggers board backoff reset so the new card appears fast. - */ - function onBoardGenerateNow() { - var $btn = $( '#prab-board-generate-now' ); - if ( ! $btn.length || $btn.prop( 'disabled' ) ) { - return; - } - $btn.prop( 'disabled', true ).text( cfg.strings && cfg.strings.generatingBtn || 'Generating\u2026' ); - var $status = $( '#prab-board-gen-status' ); - $status.text( '' ).hide(); - - $.ajax( { - url: cfg.ajaxUrl, - method: 'POST', - data: { action: 'prautoblogger_generate_now', nonce: cfg.generateNonce }, - success: function ( response ) { - if ( response && response.success ) { - $status.text( cfg.strings && cfg.strings.genStarted || 'Generation started.' ).show(); - // Notify board poller to reset backoff (run is now active). - $( document ).trigger( 'prab:generation-started' ); - } else { - $status.text( cfg.strings && cfg.strings.genError || 'Generation failed.' ).show(); - $btn.prop( 'disabled', false ).text( cfg.strings && cfg.strings.newArticle || 'New Article' ); - } - }, - error: function () { - $status.text( cfg.strings && cfg.strings.genError || 'Generation failed.' ).show(); - $btn.prop( 'disabled', false ).text( cfg.strings && cfg.strings.newArticle || 'New Article' ); - } - } ); - } - - $( function () { - $( document ).on( 'click', '#prab-board-generate-now', onBoardGenerateNow ); - } ); - -}( jQuery ) ); diff --git a/assets/js/board.js b/assets/js/board.js index f103e93..05251f2 100644 --- a/assets/js/board.js +++ b/assets/js/board.js @@ -1,42 +1,41 @@ /** - * PRAutoBlogger — Kanban Board Poller + * PRAutoBlogger — Mission Brief Board (M5, v0.27.0) * - * Live-polls the board status AJAX endpoint and updates the four columns - * (Generating | In Review | Published | Failed) in-place without a full - * page reload. + * Replaces the four-column kanban with a vertical run-list + right-rail + * inspector layout. Responsibilities: * - * Poll behaviour: - * - Polls at `prabBoard.pollInterval` ms when any article is generating. - * - Doubles the interval (backoff) when `has_active_runs` is false, capping - * at 4× the base interval — prevents needless AJAX on an idle board. - * - Resets to base interval as soon as an active run is detected again. + * 1. AJAX polling (prautoblogger_board_status) — same backoff logic as M4. + * Updates the four status sections in-place without flicker. + * 2. Row-click inspector — fetches per-run stage I/O via + * prautoblogger_board_inspector and renders the right rail. + * 3. Stage I/O expand — toggles per-stage prompt/response text inline + * inside the inspector. * - * Click-throughs (M1): - * - Generating → Activity Log - * - In Review → Review Queue - * - Published → Post edit screen - * - Failed → Activity Log - * (M2 rewires all to Article Dossier) + * Security: All server data rendered via textContent (never innerHTML for + * prompt/response text). escHtml() guards any dynamic HTML concatenation. * - * Localised config: `prabBoard` object (ajaxUrl, nonce, pollInterval, action, - * strings) injected by PRAutoBlogger_Board_Page::on_enqueue_assets(). + * Localised config: `prabBoard` object (ajaxUrl, nonce, pollInterval, + * action, inspectorAction, strings) injected by Board_Page::on_enqueue_assets(). * - * @see includes/admin/class-board-page.php — AJAX handler + localise. - * @see assets/css/board.css — Card and column styles. - * @see templates/admin/board-page.php — Initial server-rendered HTML. + * @see includes/admin/class-board-page.php -- AJAX handler + localize. + * @see includes/ajax/class-board-inspector-handler.php -- Inspector AJAX. + * @see assets/css/board.css -- Styles. + * @see templates/admin/board-page.php -- Server-rendered HTML. */ ( function ( $ ) { 'use strict'; - var cfg = window.prabBoard || {}; - var baseInterval = cfg.pollInterval || 5000; - var maxBackoff = 4; - var backoffFactor = 1; - var pollTimer = null; - var $board = null; - var $errorBanner = null; + var cfg = window.prabBoard || {}; + var baseInterval = cfg.pollInterval || 5000; + var maxBackoff = 4; + var backoffFactor = 1; + var pollTimer = null; + var $board = null; + var $errorBanner = null; + var selectedRunId = null; + var inspFetching = false; - /** Escape HTML for safe DOM insertion. */ + /** Escape HTML for safe DOM insertion via innerHTML. */ function escHtml( str ) { return $( '
' + escHtml( cfg.strings.empty ) + '
' ); + $rows.html( '' + escHtml( cfg.strings.empty || '' ) + '
' ); return; } - // Build new card HTML. - var newHtml = ''; + var html = ''; for ( var i = 0; i < cards.length; i++ ) { - newHtml += buildCardHtml( cards[ i ], colKey ); + html += buildRowHtml( cards[ i ], secKey ); } - $cards.html( newHtml ); + $rows.html( html ); + + // Re-highlight selected row if it reappears after poll. + if ( selectedRunId ) { + $rows.find( '[data-run-id="' + selectedRunId + '"]' ).addClass( 'is-selected' ); + } + + // Re-bind row-click for freshly rendered rows. + bindRowClicks( $rows ); } /** - * Apply a server snapshot to all four columns. + * Apply snapshot to all four sections. * - * @param {Object} snapshot Board snapshot from AJAX. + * @param {Object} snapshot Board snapshot from AJAX. * @returns {void} */ function applySnapshot( snapshot ) { - renderColumn( 'generating', snapshot.generating || [] ); - renderColumn( 'in_review', snapshot.in_review || [] ); - renderColumn( 'published', snapshot.published || [] ); - renderColumn( 'failed', snapshot.failed || [] ); - // Re-enable board Generate button once no active runs remain. - if ( ! snapshot.has_active_runs ) { - $( '#prab-board-generate-now' ).prop( 'disabled', false ) - .text( ( cfg.strings && cfg.strings.newArticle ) || 'New Article' ); + renderSection( 'generating', snapshot.generating || [] ); + renderSection( 'in_review', snapshot.in_review || [] ); + renderSection( 'published', snapshot.published || [] ); + renderSection( 'failed', snapshot.failed || [] ); + } + + // ── Inspector rail ──────────────────────────────────────────────────── + + /** + * Render the inspector rail with a loaded run's data. + * + * @param {Object} data Response data from Board_Inspector_Handler. + * @returns {void} + */ + function renderInspector( data ) { + var run = data.run || {}; + var stages = data.stages || []; + var total = data.cost_total || 0; + + var $inner = $( '#prab-inspector-inner' ); + $inner.empty(); + + // Title + chip. + var titleText = run.post_title || run.run_id || ''; + var $title = $( '' ).text( titleText ); + $inner.append( $title ); + + var statusMod = run.status || 'generating'; + var statusMap = { + running: 'generating', + complete: 'published', + error: 'failed', + draft: 'in_review', + }; + var chipMod = statusMap[ statusMod ] || statusMod; + var chipLabel = sectionMeta( chipMod ).label; + var $chipRow = $( '' ); + $chipRow.append( $( '' ).text( chipLabel ) ); + $inner.append( $chipRow ); + + // Stage breakdown. + var $stageList = $( '' + escHtml( cfg.strings.inspectorLoad || 'Loading…' ) + '
' ); + + $.ajax( { + url: cfg.ajaxUrl, + method: 'POST', + data: { + action: cfg.inspectorAction, + nonce: cfg.nonce, + run_id: runId + }, + success: function ( response ) { + inspFetching = false; + if ( response && response.success && response.data ) { + renderInspector( response.data ); + } else { + $inner.html( '' + escHtml( cfg.strings.inspectorError || 'Could not load run details.' ) + '
' ); + } + }, + error: function () { + inspFetching = false; + $inner.html( '' + escHtml( cfg.strings.inspectorError || 'Could not load run details.' ) + '
' ); + } + } ); + } + + // ── Row click binding ───────────────────────────────────────────────── + + /** + * Bind click + keyboard-activate on run rows within a container. + * + * @param {jQuery} $container Container to search within. + * @returns {void} + */ + function bindRowClicks( $container ) { + $container.find( '.prab-run-row' ).on( 'click keydown', function ( e ) { + if ( e.type === 'keydown' && e.which !== 13 && e.which !== 32 ) { return; } + e.preventDefault(); + + var $row = $( this ); + var runId = $row.data( 'run-id' ); + if ( ! runId ) { return; } + + // Toggle off if already selected. + if ( selectedRunId === String( runId ) ) { + selectedRunId = null; + $row.removeClass( 'is-selected' ); + $( '#prab-inspector-inner' ).html( + '' + escHtml( cfg.strings.inspectorEmpty || 'Select an article to preview its pipeline.' ) + '
' + ); + return; + } + + // Deselect all rows, select this one. + $board.find( '.prab-run-row' ).removeClass( 'is-selected' ); + $row.addClass( 'is-selected' ); + selectedRunId = String( runId ); + loadInspector( selectedRunId ); + } ); + } + + // ── Polling ─────────────────────────────────────────────────────────── + + /** Compute next poll delay. */ function nextDelay( hasActiveRuns ) { if ( hasActiveRuns ) { backoffFactor = 1; @@ -186,22 +418,13 @@ return baseInterval * backoffFactor; } - /** - * Schedule the next poll after a given delay. - * - * @param {number} delay Milliseconds. - * @returns {void} - */ + /** Schedule the next poll. */ function schedulePoll( delay ) { clearTimeout( pollTimer ); pollTimer = setTimeout( doPoll, delay ); } - /** - * Execute one board poll: AJAX POST → apply snapshot → reschedule. - * - * @returns {void} - */ + /** Execute one poll cycle. */ function doPoll() { $.ajax( { url: cfg.ajaxUrl, @@ -211,9 +434,7 @@ nonce: cfg.nonce }, success: function ( response ) { - if ( $errorBanner ) { - $errorBanner.removeClass( 'is-visible' ); - } + if ( $errorBanner ) { $errorBanner.removeClass( 'is-visible' ); } if ( response && response.success && response.data ) { applySnapshot( response.data ); schedulePoll( nextDelay( !! response.data.has_active_runs ) ); @@ -222,15 +443,29 @@ } }, error: function () { - if ( $errorBanner ) { - $errorBanner.addClass( 'is-visible' ); - } + if ( $errorBanner ) { $errorBanner.addClass( 'is-visible' ); } schedulePoll( nextDelay( false ) ); } } ); } + // ── Init ────────────────────────────────────────────────────────────── + $( function () { + $board = $( '#prab-board' ); + if ( ! $board.length ) { return; } + + // Error banner. + $errorBanner = $( '' ); + $errorBanner.text( cfg.strings.pollError || '' ); + $board.before( $errorBanner ); + + // Bind clicks on server-rendered rows. + bindRowClicks( $board ); + + // Start poller. + var hasActive = ( $board.find( '[data-section="generating"] .prab-run-row' ).length > 0 ); + schedulePoll( nextDelay( hasActive ) ); } ); }( jQuery ) ); diff --git a/includes/admin/class-board-data-provider.php b/includes/admin/class-board-data-provider.php index 17347ed..1f3f719 100644 --- a/includes/admin/class-board-data-provider.php +++ b/includes/admin/class-board-data-provider.php @@ -4,25 +4,29 @@ declare(strict_types=1); /** * phpcs:ignore WordPress.Files.FileName.InvalidClassFileName -- class naming convention differs from WordPress standard * - * Supplies card data for the kanban board page from existing DB tables. + * Supplies run-list data for the Mission Brief board (M5). * - * Orchestrates the four board columns: + * Orchestrates the four status sections: * Generating | In Review | Published | Failed * - * Generation-log queries (Generating, Failed) are delegated to - * PRAutoBlogger_Board_Gen_Log_Query. WP_Query columns (In Review, Published) - * are handled here. + * M5 additions (v0.27.0): each card now includes `run_stages_summary` + * -- a lightweight array of { stage, status } entries derived from the + * run_stages table, used by the board's dot-rail (stage-progress indicator). + * The inspector's full I/O is fetched separately via Board_Inspector_Handler + * on row-click (not loaded up-front for every card). * - * M2: cards now include `dossier_url` so board.js can deep-link to the - * Article Dossier instead of the old per-column destinations. + * Generation-log queries (Generating, Failed) are delegated to + * PRAutoBlogger_Board_Gen_Log_Query. WP_Query sections (In Review, Published) + * are handled here. * * No schema changes -- all data comes from tables that already exist. * * Triggered by: PRAutoBlogger_Board_Page::on_ajax_board_status(). - * Dependencies: PRAutoBlogger_Board_Gen_Log_Query, PRAutoBlogger_Dossier_Page, - * WordPress WP_Query, get_option(). + * Dependencies: PRAutoBlogger_Board_Gen_Log_Query, PRAutoBlogger_Board_Stage_Dots, + * PRAutoBlogger_Dossier_Page, WordPress WP_Query, get_option(). * * @see admin/class-board-gen-log-query.php -- Raw gen_log queries (Generating/Failed). + * @see admin/class-board-stage-dots.php -- Stage dot-rail enrichment (M5). * @see admin/class-board-page.php -- Calls get_board_snapshot(). * @see admin/class-dossier-page.php -- Provides url_for_post() for deep-links. * @see ARCHITECTURE.md -- Database schema. @@ -56,6 +60,12 @@ class PRAutoBlogger_Board_Data_Provider { $published = $this->get_published_cards(); $failed = $this->gen_log_query->get_failed_cards(); + // Enrich cards with lightweight stage dots. + $generating = PRAutoBlogger_Board_Stage_Dots::enrich( $generating ); + $in_review = PRAutoBlogger_Board_Stage_Dots::enrich( $in_review ); + $published = PRAutoBlogger_Board_Stage_Dots::enrich( $published ); + $failed = PRAutoBlogger_Board_Stage_Dots::enrich( $failed ); + return array( 'generating' => $generating, 'in_review' => $in_review, @@ -85,7 +95,6 @@ class PRAutoBlogger_Board_Data_Provider { /** * Cards for generated draft posts awaiting human review. - * M2: cards include dossier_url for deep-linking. * * @return array