| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381 |
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <pstm-uri>/</pstm-uri>
- <pstm-template>main</pstm-template>
- <script>
- // Live dashboard: the pulse subscription (see the entrypoint)
- // re-runs the sealed refresh action whenever task or saddle state
- // changes, so progress, blocks and review requests stream in.
- // The notification bell drives the Web Push subscribe flow (plan
- // 11 task E) - the shared engine lives in app.js
- // (window.initPushBell, goal 41 task 3: the dashboard's compact
- // rail strip and /profile's settings card run the same code).
- if (window.statum) {
- (function () {
- 'use strict';
- statum.addStateListener('pulse', function () {
- var dashboard = statum.read('dashboard');
- if (dashboard && dashboard.refresh) {
- statum.act(dashboard.refresh).catch(function () { });
- }
- });
- function dashboard() {
- return statum.read('dashboard') || {};
- }
- // The hero composer (goal 27): starts a NEW advisor chat.
- // statum.act rides the JSON transport, so free text (with
- // its plusses and quotes) survives intact; the action
- // answers with a navigation to the new chat page.
- window.startAdvisorChat = function () {
- var state = dashboard();
- if (!state.start_advisor) return;
- var box = document.getElementById('advisor-composer');
- if (!box) return;
- var text = box.value.replace(/^\s+|\s+$/g, '');
- if (!text) return;
- box.value = '';
- statum.act(state.start_advisor, { message: text }).catch(function () { });
- };
- // ---- work stream: "new since your last visit" + the row
- // cap ----
- // Goal 41 task 3; goal 41 task 10 moved the feed to the
- // cross-entity work stream (the slot's work_stream rows).
- // The framework carries no cookies (auth is slot-based)
- // and statum exposes no client slot WRITE, so a true
- // server-side last-visit is not available: the stamp
- // lives in localStorage, written when the dashboard tab
- // UNLOADS - a visit updates the baseline, a background
- // pulse does not (an open tab keeps its count growing
- // until you come back). First visit: no stamp, no badge
- // (everything would be "new"). The count covers the full
- // stream the slot carries (the server caps the stream
- // itself at 15 rows - the old activity table's uncap
- // control died with it; the expander stays for the
- // quiet frame).
- var VISIT_KEY = 'clanker.lastVisit';
- function readLastVisit() {
- try {
- var raw = window.localStorage.getItem(VISIT_KEY);
- if (!raw) return null;
- var stamp = parseInt(raw, 10);
- return isNaN(stamp) ? null : stamp;
- } catch (error) {
- return null;
- }
- }
- function syncActivityCount() {
- var state = statum.read('dashboard');
- var badge = document.querySelector('[data-activity-new]');
- if (!state || !badge) return;
- var rows = state.work_stream || [];
- var last = readLastVisit();
- var fresh = 0;
- if (last !== null) {
- for (var index = 0; index < rows.length; index++) {
- if (rows[index].at_epoch > last) fresh++;
- }
- }
- badge.textContent = fresh > 0
- ? fresh + ' new since your last visit' : '';
- badge.hidden = fresh === 0;
- }
- statum.afterEvaluate(syncActivityCount);
- window.addEventListener('beforeunload', function () {
- try {
- window.localStorage.setItem(VISIT_KEY,
- String(Math.floor(Date.now() / 1000)));
- } catch (error) { /* private mode: no baseline, no badge */ }
- });
- // The per-browser bell engine (fingerprint this browser's
- // subscription against the slot's endpoint list; Enable vs
- // Turn off follows the DISPLAYED verdict): shared with the
- // /profile settings card, defined in app.js.
- window.initPushBell({ stateType: 'dashboard' });
- })();
- }
- </script>
- </head>
- <body>
- <div class="stack">
- <p stm-if="dashboard == null">Loading…</p>
- <div stm-if="dashboard != null" class="stack">
- <!-- The advisor hero (goal 27; goal 41 task 3 keeps it the
- page's first control but gives it the samplers' shape:
- the heading and the description sit INSIDE the composer
- card, above the textarea, so the primary surface is
- self-describing). Starting a chat here creates it
- (titled from the first line) and lands on the chat
- page; the advisor reads fleet state, asks questions
- and suggests tasks/goals, but never runs commands. -->
- <section stm-if="dashboard != null" class="advisor-hero card">
- <!-- Its own block layout (.advisor-hero-form): NOT
- form.inline + .form-row, whose inline-flex and 34rem
- column cap squeeze a multi-line textarea into a
- cramped sliver. The composer spans the card's full
- width with a roomy textarea; the button sits below
- it, right-aligned. -->
- <form class="advisor-hero-form"
- onsubmit="window.startAdvisorChat(); return false;">
- <label for="advisor-composer">Ask the advisor</label>
- <p class="hint advisor-hero-lede">Reads your projects, tasks, goals, rules and
- discoveries — asks you questions and suggests work. It never runs
- commands.</p>
- <textarea id="advisor-composer" rows="3"
- stm-attribute.disabled="!dashboard.advisor_configured"
- placeholder="What should the fleet work on next?"></textarea>
- <div class="advisor-hero-actions">
- <p class="hint advisor-hero-link"><a href="/chats">Your chats</a></p>
- <button type="submit" class="btn-primary"
- stm-attribute.disabled="!dashboard.advisor_configured">Start chat</button>
- </div>
- </form>
- <p class="notice" stm-if="!dashboard.advisor_configured">The advisor is not
- configured - set the model endpoint on the <a href="/models">Admin →
- Model</a> page to enable it.</p>
- <p class="notice" stm-if="dashboard.canon_texts == 0">Canon unavailable -
- no canonical texts found. Set <code>CLANKER_CANON_DIR</code> or install
- the <code>share/clanker-saddle/canon</code> package files.</p>
- </section>
- <!-- ATTENTION FIRST (goal 41 task 3 / ux-notes §1 A+B): the
- left column is why the page exists — pinned needs-a-human
- blocks, then active tasks with their latest progress. The
- right rail carries the reference material: queue metrics,
- the saddle list and the push-bell preference strip (a
- settings action, not news — the old full-width opener card
- is gone; the preference UI now lives on /profile). Stacks
- under the framework's 55rem breakpoint. -->
- <div class="with-sidebar dash-board">
- <div class="dash-attention stack">
- <section aria-labelledby="needs-a-human-h">
- <h2 id="needs-a-human-h" class="dash-heading">Needs a human
- <span class="badge badge-warning"
- stm-if="dashboard.pinned.length > 0"
- stm-text="dashboard.pinned.length">0</span></h2>
- <p class="text-muted">Open questions, RFCs and reviews across all tasks and
- goals, newest first — follow the link to answer on the task or goal
- page.</p>
- <div class="cards" stm-if="dashboard.pinned.length > 0">
- <section stm-for-blockview-in="dashboard.pinned"
- stm-attribute.class="blockview.item_type == 'goal' ? 'card goal-card' : 'card'">
- <p>
- <span stm-if="blockview.item_type == 'goal'" class="goal-icon">🎯</span>
- <span stm-attribute.class="'badge badge-kind-' + blockview.item_type"
- stm-text="blockview.item_type">task</span>
- <span stm-attribute.class="'badge badge-' + blockview.block_type" stm-text="blockview.block_type">type</span>
- <span class="text-muted" stm-text="blockview.created_display">when</span></p>
- <p><strong stm-text="blockview.question">question</strong></p>
- <p stm-if="blockview.item_type == 'task'">
- <a stm-attribute.href="'/tasks/' + blockview.task_id" stm-text="blockview.task_title">task</a></p>
- <p stm-if="blockview.item_type == 'goal'">
- <a stm-attribute.href="'/goals/' + blockview.goal_id" stm-text="blockview.goal_title">goal</a></p>
- </section>
- </div>
- <p stm-if="dashboard.pinned.length == 0" class="text-muted">Nothing needs a
- human — no open questions, RFCs or reviews.</p>
- </section>
- <section aria-labelledby="active-tasks-h"
- stm-if="dashboard.active_tasks.length > 0">
- <h2 id="active-tasks-h" class="dash-heading">Active tasks</h2>
- <div class="table-wrap">
- <table class="table-hover">
- <thead><tr><th>Task</th><th>Status</th><th>Branch</th><th>Latest progress</th></tr></thead>
- <tbody>
- <tr stm-for-active-in="dashboard.active_tasks">
- <td><a stm-attribute.href="'/tasks/' + active.id" stm-text="active.title">task</a></td>
- <td><span stm-attribute.class="'badge badge-' + active.status" stm-text="active.status">status</span></td>
- <td stm-text="active.task_branch">branch</td>
- <td><strong stm-if="active.latest_progress.length > 0" stm-text="active.latest_progress">progress</strong>
- <span class="text-muted" stm-if="active.latest_progress.length > 0" stm-text="active.progress_display">when</span>
- <span class="text-muted" stm-if="active.latest_progress.length == 0">—</span></td>
- </tr>
- </tbody>
- </table>
- </div>
- </section>
- </div>
- <!-- The right rail: metrics, the saddle verdict list and
- the bell strip. Everything here is one-glance state or
- a settings action — nothing that outranks the
- attention column. -->
- <aside class="dash-rail stack" aria-label="Fleet status">
- <section class="card card-flat">
- <h3>Queue</h3>
- <p class="dash-metric"><strong class="num" stm-text="dashboard.ready_count">0</strong>
- ready to dispatch</p>
- <p class="text-muted"><span stm-text="dashboard.queued_count">0</span> queued ·
- <span stm-text="dashboard.active_count">0</span> active ·
- <span stm-text="dashboard.blocked_count">0</span> blocked</p>
- <p class="text-muted"><span stm-text="dashboard.done_count">0</span> done ·
- <span stm-text="dashboard.failed_count">0</span> failed</p>
- <p><a href="/tasks">Task queue</a></p>
- </section>
- <section class="card card-flat" aria-labelledby="rail-saddles-h">
- <h3 id="rail-saddles-h">Saddles</h3>
- <p class="dash-metric"><strong class="num" stm-text="dashboard.idle_saddle_count">0</strong>
- idle</p>
- <ul class="dash-saddles">
- <li stm-for-saddle-in="dashboard.saddles">
- <span stm-attribute.class="'badge badge-' + saddle.status"
- stm-text="saddle.status">status</span>
- <strong stm-text="saddle.name">name</strong>
- <span stm-if="saddle.current_goal_id > 0">·
- <a stm-attribute.href="'/goals/' + saddle.current_goal_id"
- stm-text="saddle.current_goal_title">goal</a></span>
- <span stm-if="saddle.current_goal_id == 0 && saddle.current_task_id > 0">·
- <a stm-attribute.href="'/tasks/' + saddle.current_task_id"
- stm-text="saddle.current_task_title">task</a></span>
- </li>
- <li stm-if="dashboard.saddles.length == 0" class="text-muted">No saddles
- registered.</li>
- </ul>
- <p><a href="/saddles">Manage saddles</a></p>
- </section>
- <!-- The notification bell (plan 11 task E; per-browser
- since the user-global flag lied to second
- browsers): THIS browser's own subscription decides
- Enable vs Turn off (the JS fingerprints the
- browser's endpoint and matches it against the
- slot's list), the helper text counts the user's
- OTHER subscribed browsers, and "Send test
- notification" fires the real end-to-end probe
- through this user's subscriptions. A compact
- strip in the rail (goal 41 task 3): the
- preference UI lives on /profile. -->
- <section class="card card-flat push-bell-card">
- <h3>Notifications</h3>
- <form class="stack">
- <button type="button" class="btn-sm btn-primary" data-push-bell
- data-bell-on hidden>Enable notifications</button>
- <button type="button" class="btn-sm" data-push-bell data-bell-off
- hidden>Turn off notifications</button>
- <button type="button" class="btn-sm" data-push-test
- hidden>Send test notification</button>
- <p class="hint" data-bell-help>— be told when an agent asks a question,
- raises an RFC or review</p>
- <p class="hint"><a href="/profile">Notification settings</a></p>
- </form>
- </section>
- </aside>
- </div>
- <!-- WORK STREAM (goal 41 T10, ux-notes bet 5): the page's
- PRIMARY feed — task progress, goal narrative and
- event-log highlights joined into ONE time-ordered
- stream (the slot's work_stream: the timeline window
- projected into three row grammars, newest 15). It
- replaces the old activity expander as the dashboard's
- feed; the underlying surfaces stay — the goal page
- owns the narrative, the task page's Log tab owns the
- full history. The stream keeps the ambient expander's
- quiet frame but sits OPEN by default: this is the
- news, not reference material. The localStorage
- new-since badge counts stream rows now. -->
- <details class="dash-ambient" data-activity-expander data-work-stream open>
- <summary>Work stream <span class="badge badge-info" data-activity-new hidden>new</span></summary>
- <div class="dash-ambient-body">
- <p class="text-muted">The fleet's latest work — task progress, goal
- events and highlights, newest first.</p>
- <ul class="work-stream">
- <li stm-for-item-in="dashboard.work_stream">
- <span stm-attribute.class="'badge badge-stream-' + item.kind"
- stm-text="item.event_kind">kind</span>
- <span class="work-stream-when text-muted num"
- stm-text="item.when_display">when</span>
- <span class="work-stream-text"><span stm-if="item.href.length > 0"
- class="work-stream-actor"><a stm-attribute.href="item.href"
- stm-text="item.actor">actor</a></span>
- <span stm-attribute.class="item.kind == 'log_highlight' && item.event_kind == 'comment' ? 'footnote' : ''"
- stm-text="item.text">text</span></span>
- </li>
- <li stm-if="dashboard.work_stream.length == 0"
- class="text-muted">Nothing yet.</li>
- </ul>
- <p class="text-muted"><a href="/tasks">Task queue</a> ·
- <a href="/goals">Goals</a></p>
- </div>
- </details>
- <!-- AMBIENT ZONES (goal 41 task 3; the mocks/ sampler
- review: reference material starts COLLAPSED so a visit
- for "answer the question" never scrolls past it). -->
- <div class="expander-group dash-ambient">
- <!-- The scratchpad (goal 8 task 3): jot down batches of
- changes before they are ready to be full-on goals.
- Check a note off, delete it, or promote it — promote
- creates a DRAFT goal (title = the first line, brief =
- the whole body) and jumps to its page to finish. -->
- <details stm-if="dashboard.notes.length > 0">
- <summary>Notes <span class="badge"
- stm-text="dashboard.notes.length">0</span></summary>
- <div class="dash-ambient-body">
- <table>
- <thead><tr><th>Done</th><th>Note</th><th>Created</th><th></th></tr></thead>
- <tbody>
- <tr stm-for-note-in="dashboard.notes">
- <td><button type="button" stm-action="note.toggle"
- stm-attribute.class="note.done ? 'approve' : ''"
- stm-text="note.done ? 'Done' : 'Open'">toggle</button></td>
- <td><span stm-attribute.class="note.done ? 'footnote' : ''"
- stm-text="note.body">body</span></td>
- <td class="text-muted" stm-text="note.created_display">when</td>
- <td>
- <span stm-if="note.goal_id > 0">
- <span class="goal-icon">🎯</span>
- <a stm-attribute.href="'/goals/' + note.goal_id"
- stm-text="note.goal_title">goal</a>
- </span>
- <form class="cluster" stm-if="note.goal_id == 0 && !note.done"
- stm-action="note.promote">
- <select name="project_id" required>
- <option stm-for-project-in="dashboard.projects"
- stm-attribute.value="project.id"
- stm-text="project.name">project</option>
- </select>
- <input type="text" name="branch_name" required
- placeholder="goal/branch" size="14">
- <button type="submit" class="btn-primary">Create goal</button>
- </form>
- <span class="text-muted" stm-if="note.goal_id == 0 && note.done">—</span>
- <form class="cluster" stm-action="note.delete">
- <button type="submit">Delete</button>
- </form>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- </details>
- <!-- The five newest discoveries (goal 24): the fleet's
- knowledge base at a glance; the Discoveries page owns
- the searchable listing. -->
- <details stm-if="dashboard.discoveries.length > 0">
- <summary>Latest discoveries <span class="badge"
- stm-text="dashboard.discoveries.length">0</span></summary>
- <div class="dash-ambient-body">
- <ul>
- <li stm-for-discovery-in="dashboard.discoveries" class="md footnote"
- stm-markdown="discovery.body">body</li>
- </ul>
- <p class="text-muted"><a href="/discoveries">All discoveries →</a></p>
- </div>
- </details>
- </div>
- </div>
- </div>
- </body>
- </html>
|