home.html 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <pstm-uri>/</pstm-uri>
  5. <pstm-template>main</pstm-template>
  6. <script>
  7. // Live dashboard: the pulse subscription (see the entrypoint)
  8. // re-runs the sealed refresh action whenever task or saddle state
  9. // changes, so progress, blocks and review requests stream in.
  10. // The notification bell drives the Web Push subscribe flow (plan
  11. // 11 task E) - the shared engine lives in app.js
  12. // (window.initPushBell, goal 41 task 3: the dashboard's compact
  13. // rail strip and /profile's settings card run the same code).
  14. if (window.statum) {
  15. (function () {
  16. 'use strict';
  17. statum.addStateListener('pulse', function () {
  18. var dashboard = statum.read('dashboard');
  19. if (dashboard && dashboard.refresh) {
  20. statum.act(dashboard.refresh).catch(function () { });
  21. }
  22. });
  23. function dashboard() {
  24. return statum.read('dashboard') || {};
  25. }
  26. // The hero composer (goal 27): starts a NEW advisor chat.
  27. // statum.act rides the JSON transport, so free text (with
  28. // its plusses and quotes) survives intact; the action
  29. // answers with a navigation to the new chat page.
  30. window.startAdvisorChat = function () {
  31. var state = dashboard();
  32. if (!state.start_advisor) return;
  33. var box = document.getElementById('advisor-composer');
  34. if (!box) return;
  35. var text = box.value.replace(/^\s+|\s+$/g, '');
  36. if (!text) return;
  37. box.value = '';
  38. statum.act(state.start_advisor, { message: text }).catch(function () { });
  39. };
  40. // ---- work stream: "new since your last visit" + the row
  41. // cap ----
  42. // Goal 41 task 3; goal 41 task 10 moved the feed to the
  43. // cross-entity work stream (the slot's work_stream rows).
  44. // The framework carries no cookies (auth is slot-based)
  45. // and statum exposes no client slot WRITE, so a true
  46. // server-side last-visit is not available: the stamp
  47. // lives in localStorage, written when the dashboard tab
  48. // UNLOADS - a visit updates the baseline, a background
  49. // pulse does not (an open tab keeps its count growing
  50. // until you come back). First visit: no stamp, no badge
  51. // (everything would be "new"). The count covers the full
  52. // stream the slot carries (the server caps the stream
  53. // itself at 15 rows - the old activity table's uncap
  54. // control died with it; the expander stays for the
  55. // quiet frame).
  56. var VISIT_KEY = 'clanker.lastVisit';
  57. function readLastVisit() {
  58. try {
  59. var raw = window.localStorage.getItem(VISIT_KEY);
  60. if (!raw) return null;
  61. var stamp = parseInt(raw, 10);
  62. return isNaN(stamp) ? null : stamp;
  63. } catch (error) {
  64. return null;
  65. }
  66. }
  67. function syncActivityCount() {
  68. var state = statum.read('dashboard');
  69. var badge = document.querySelector('[data-activity-new]');
  70. if (!state || !badge) return;
  71. var rows = state.work_stream || [];
  72. var last = readLastVisit();
  73. var fresh = 0;
  74. if (last !== null) {
  75. for (var index = 0; index < rows.length; index++) {
  76. if (rows[index].at_epoch > last) fresh++;
  77. }
  78. }
  79. badge.textContent = fresh > 0
  80. ? fresh + ' new since your last visit' : '';
  81. badge.hidden = fresh === 0;
  82. }
  83. statum.afterEvaluate(syncActivityCount);
  84. window.addEventListener('beforeunload', function () {
  85. try {
  86. window.localStorage.setItem(VISIT_KEY,
  87. String(Math.floor(Date.now() / 1000)));
  88. } catch (error) { /* private mode: no baseline, no badge */ }
  89. });
  90. // The per-browser bell engine (fingerprint this browser's
  91. // subscription against the slot's endpoint list; Enable vs
  92. // Turn off follows the DISPLAYED verdict): shared with the
  93. // /profile settings card, defined in app.js.
  94. window.initPushBell({ stateType: 'dashboard' });
  95. })();
  96. }
  97. </script>
  98. </head>
  99. <body>
  100. <div class="stack">
  101. <p stm-if="dashboard == null">Loading…</p>
  102. <div stm-if="dashboard != null" class="stack">
  103. <!-- The advisor hero (goal 27; goal 41 task 3 keeps it the
  104. page's first control but gives it the samplers' shape:
  105. the heading and the description sit INSIDE the composer
  106. card, above the textarea, so the primary surface is
  107. self-describing). Starting a chat here creates it
  108. (titled from the first line) and lands on the chat
  109. page; the advisor reads fleet state, asks questions
  110. and suggests tasks/goals, but never runs commands. -->
  111. <section stm-if="dashboard != null" class="advisor-hero card">
  112. <!-- Its own block layout (.advisor-hero-form): NOT
  113. form.inline + .form-row, whose inline-flex and 34rem
  114. column cap squeeze a multi-line textarea into a
  115. cramped sliver. The composer spans the card's full
  116. width with a roomy textarea; the button sits below
  117. it, right-aligned. -->
  118. <form class="advisor-hero-form"
  119. onsubmit="window.startAdvisorChat(); return false;">
  120. <label for="advisor-composer">Ask the advisor</label>
  121. <p class="hint advisor-hero-lede">Reads your projects, tasks, goals, rules and
  122. discoveries — asks you questions and suggests work. It never runs
  123. commands.</p>
  124. <textarea id="advisor-composer" rows="3"
  125. stm-attribute.disabled="!dashboard.advisor_configured"
  126. placeholder="What should the fleet work on next?"></textarea>
  127. <div class="advisor-hero-actions">
  128. <p class="hint advisor-hero-link"><a href="/chats">Your chats</a></p>
  129. <button type="submit" class="btn-primary"
  130. stm-attribute.disabled="!dashboard.advisor_configured">Start chat</button>
  131. </div>
  132. </form>
  133. <p class="notice" stm-if="!dashboard.advisor_configured">The advisor is not
  134. configured - set the model endpoint on the <a href="/models">Admin →
  135. Model</a> page to enable it.</p>
  136. <p class="notice" stm-if="dashboard.canon_texts == 0">Canon unavailable -
  137. no canonical texts found. Set <code>CLANKER_CANON_DIR</code> or install
  138. the <code>share/clanker-saddle/canon</code> package files.</p>
  139. </section>
  140. <!-- ATTENTION FIRST (goal 41 task 3 / ux-notes §1 A+B): the
  141. left column is why the page exists — pinned needs-a-human
  142. blocks, then active tasks with their latest progress. The
  143. right rail carries the reference material: queue metrics,
  144. the saddle list and the push-bell preference strip (a
  145. settings action, not news — the old full-width opener card
  146. is gone; the preference UI now lives on /profile). Stacks
  147. under the framework's 55rem breakpoint. -->
  148. <div class="with-sidebar dash-board">
  149. <div class="dash-attention stack">
  150. <section aria-labelledby="needs-a-human-h">
  151. <h2 id="needs-a-human-h" class="dash-heading">Needs a human
  152. <span class="badge badge-warning"
  153. stm-if="dashboard.pinned.length > 0"
  154. stm-text="dashboard.pinned.length">0</span></h2>
  155. <p class="text-muted">Open questions, RFCs and reviews across all tasks and
  156. goals, newest first — follow the link to answer on the task or goal
  157. page.</p>
  158. <div class="cards" stm-if="dashboard.pinned.length > 0">
  159. <section stm-for-blockview-in="dashboard.pinned"
  160. stm-attribute.class="blockview.item_type == 'goal' ? 'card goal-card' : 'card'">
  161. <p>
  162. <span stm-if="blockview.item_type == 'goal'" class="goal-icon">🎯</span>
  163. <span stm-attribute.class="'badge badge-kind-' + blockview.item_type"
  164. stm-text="blockview.item_type">task</span>
  165. <span stm-attribute.class="'badge badge-' + blockview.block_type" stm-text="blockview.block_type">type</span>
  166. <span class="text-muted" stm-text="blockview.created_display">when</span></p>
  167. <p><strong stm-text="blockview.question">question</strong></p>
  168. <p stm-if="blockview.item_type == 'task'">
  169. <a stm-attribute.href="'/tasks/' + blockview.task_id" stm-text="blockview.task_title">task</a></p>
  170. <p stm-if="blockview.item_type == 'goal'">
  171. <a stm-attribute.href="'/goals/' + blockview.goal_id" stm-text="blockview.goal_title">goal</a></p>
  172. </section>
  173. </div>
  174. <p stm-if="dashboard.pinned.length == 0" class="text-muted">Nothing needs a
  175. human — no open questions, RFCs or reviews.</p>
  176. </section>
  177. <section aria-labelledby="active-tasks-h"
  178. stm-if="dashboard.active_tasks.length > 0">
  179. <h2 id="active-tasks-h" class="dash-heading">Active tasks</h2>
  180. <div class="table-wrap">
  181. <table class="table-hover">
  182. <thead><tr><th>Task</th><th>Status</th><th>Branch</th><th>Latest progress</th></tr></thead>
  183. <tbody>
  184. <tr stm-for-active-in="dashboard.active_tasks">
  185. <td><a stm-attribute.href="'/tasks/' + active.id" stm-text="active.title">task</a></td>
  186. <td><span stm-attribute.class="'badge badge-' + active.status" stm-text="active.status">status</span></td>
  187. <td stm-text="active.task_branch">branch</td>
  188. <td><strong stm-if="active.latest_progress.length > 0" stm-text="active.latest_progress">progress</strong>
  189. <span class="text-muted" stm-if="active.latest_progress.length > 0" stm-text="active.progress_display">when</span>
  190. <span class="text-muted" stm-if="active.latest_progress.length == 0">—</span></td>
  191. </tr>
  192. </tbody>
  193. </table>
  194. </div>
  195. </section>
  196. </div>
  197. <!-- The right rail: metrics, the saddle verdict list and
  198. the bell strip. Everything here is one-glance state or
  199. a settings action — nothing that outranks the
  200. attention column. -->
  201. <aside class="dash-rail stack" aria-label="Fleet status">
  202. <section class="card card-flat">
  203. <h3>Queue</h3>
  204. <p class="dash-metric"><strong class="num" stm-text="dashboard.ready_count">0</strong>
  205. ready to dispatch</p>
  206. <p class="text-muted"><span stm-text="dashboard.queued_count">0</span> queued ·
  207. <span stm-text="dashboard.active_count">0</span> active ·
  208. <span stm-text="dashboard.blocked_count">0</span> blocked</p>
  209. <p class="text-muted"><span stm-text="dashboard.done_count">0</span> done ·
  210. <span stm-text="dashboard.failed_count">0</span> failed</p>
  211. <p><a href="/tasks">Task queue</a></p>
  212. </section>
  213. <section class="card card-flat" aria-labelledby="rail-saddles-h">
  214. <h3 id="rail-saddles-h">Saddles</h3>
  215. <p class="dash-metric"><strong class="num" stm-text="dashboard.idle_saddle_count">0</strong>
  216. idle</p>
  217. <ul class="dash-saddles">
  218. <li stm-for-saddle-in="dashboard.saddles">
  219. <span stm-attribute.class="'badge badge-' + saddle.status"
  220. stm-text="saddle.status">status</span>
  221. <strong stm-text="saddle.name">name</strong>
  222. <span stm-if="saddle.current_goal_id > 0">·
  223. <a stm-attribute.href="'/goals/' + saddle.current_goal_id"
  224. stm-text="saddle.current_goal_title">goal</a></span>
  225. <span stm-if="saddle.current_goal_id == 0 && saddle.current_task_id > 0">·
  226. <a stm-attribute.href="'/tasks/' + saddle.current_task_id"
  227. stm-text="saddle.current_task_title">task</a></span>
  228. </li>
  229. <li stm-if="dashboard.saddles.length == 0" class="text-muted">No saddles
  230. registered.</li>
  231. </ul>
  232. <p><a href="/saddles">Manage saddles</a></p>
  233. </section>
  234. <!-- The notification bell (plan 11 task E; per-browser
  235. since the user-global flag lied to second
  236. browsers): THIS browser's own subscription decides
  237. Enable vs Turn off (the JS fingerprints the
  238. browser's endpoint and matches it against the
  239. slot's list), the helper text counts the user's
  240. OTHER subscribed browsers, and "Send test
  241. notification" fires the real end-to-end probe
  242. through this user's subscriptions. A compact
  243. strip in the rail (goal 41 task 3): the
  244. preference UI lives on /profile. -->
  245. <section class="card card-flat push-bell-card">
  246. <h3>Notifications</h3>
  247. <form class="stack">
  248. <button type="button" class="btn-sm btn-primary" data-push-bell
  249. data-bell-on hidden>Enable notifications</button>
  250. <button type="button" class="btn-sm" data-push-bell data-bell-off
  251. hidden>Turn off notifications</button>
  252. <button type="button" class="btn-sm" data-push-test
  253. hidden>Send test notification</button>
  254. <p class="hint" data-bell-help>— be told when an agent asks a question,
  255. raises an RFC or review</p>
  256. <p class="hint"><a href="/profile">Notification settings</a></p>
  257. </form>
  258. </section>
  259. </aside>
  260. </div>
  261. <!-- WORK STREAM (goal 41 T10, ux-notes bet 5): the page's
  262. PRIMARY feed — task progress, goal narrative and
  263. event-log highlights joined into ONE time-ordered
  264. stream (the slot's work_stream: the timeline window
  265. projected into three row grammars, newest 15). It
  266. replaces the old activity expander as the dashboard's
  267. feed; the underlying surfaces stay — the goal page
  268. owns the narrative, the task page's Log tab owns the
  269. full history. The stream keeps the ambient expander's
  270. quiet frame but sits OPEN by default: this is the
  271. news, not reference material. The localStorage
  272. new-since badge counts stream rows now. -->
  273. <details class="dash-ambient" data-activity-expander data-work-stream open>
  274. <summary>Work stream <span class="badge badge-info" data-activity-new hidden>new</span></summary>
  275. <div class="dash-ambient-body">
  276. <p class="text-muted">The fleet's latest work — task progress, goal
  277. events and highlights, newest first.</p>
  278. <ul class="work-stream">
  279. <li stm-for-item-in="dashboard.work_stream">
  280. <span stm-attribute.class="'badge badge-stream-' + item.kind"
  281. stm-text="item.event_kind">kind</span>
  282. <span class="work-stream-when text-muted num"
  283. stm-text="item.when_display">when</span>
  284. <span class="work-stream-text"><span stm-if="item.href.length > 0"
  285. class="work-stream-actor"><a stm-attribute.href="item.href"
  286. stm-text="item.actor">actor</a></span>
  287. <span stm-attribute.class="item.kind == 'log_highlight' && item.event_kind == 'comment' ? 'footnote' : ''"
  288. stm-text="item.text">text</span></span>
  289. </li>
  290. <li stm-if="dashboard.work_stream.length == 0"
  291. class="text-muted">Nothing yet.</li>
  292. </ul>
  293. <p class="text-muted"><a href="/tasks">Task queue</a> ·
  294. <a href="/goals">Goals</a></p>
  295. </div>
  296. </details>
  297. <!-- AMBIENT ZONES (goal 41 task 3; the mocks/ sampler
  298. review: reference material starts COLLAPSED so a visit
  299. for "answer the question" never scrolls past it). -->
  300. <div class="expander-group dash-ambient">
  301. <!-- The scratchpad (goal 8 task 3): jot down batches of
  302. changes before they are ready to be full-on goals.
  303. Check a note off, delete it, or promote it — promote
  304. creates a DRAFT goal (title = the first line, brief =
  305. the whole body) and jumps to its page to finish. -->
  306. <details stm-if="dashboard.notes.length > 0">
  307. <summary>Notes <span class="badge"
  308. stm-text="dashboard.notes.length">0</span></summary>
  309. <div class="dash-ambient-body">
  310. <table>
  311. <thead><tr><th>Done</th><th>Note</th><th>Created</th><th></th></tr></thead>
  312. <tbody>
  313. <tr stm-for-note-in="dashboard.notes">
  314. <td><button type="button" stm-action="note.toggle"
  315. stm-attribute.class="note.done ? 'approve' : ''"
  316. stm-text="note.done ? 'Done' : 'Open'">toggle</button></td>
  317. <td><span stm-attribute.class="note.done ? 'footnote' : ''"
  318. stm-text="note.body">body</span></td>
  319. <td class="text-muted" stm-text="note.created_display">when</td>
  320. <td>
  321. <span stm-if="note.goal_id > 0">
  322. <span class="goal-icon">🎯</span>
  323. <a stm-attribute.href="'/goals/' + note.goal_id"
  324. stm-text="note.goal_title">goal</a>
  325. </span>
  326. <form class="cluster" stm-if="note.goal_id == 0 && !note.done"
  327. stm-action="note.promote">
  328. <select name="project_id" required>
  329. <option stm-for-project-in="dashboard.projects"
  330. stm-attribute.value="project.id"
  331. stm-text="project.name">project</option>
  332. </select>
  333. <input type="text" name="branch_name" required
  334. placeholder="goal/branch" size="14">
  335. <button type="submit" class="btn-primary">Create goal</button>
  336. </form>
  337. <span class="text-muted" stm-if="note.goal_id == 0 && note.done">—</span>
  338. <form class="cluster" stm-action="note.delete">
  339. <button type="submit">Delete</button>
  340. </form>
  341. </td>
  342. </tr>
  343. </tbody>
  344. </table>
  345. </div>
  346. </details>
  347. <!-- The five newest discoveries (goal 24): the fleet's
  348. knowledge base at a glance; the Discoveries page owns
  349. the searchable listing. -->
  350. <details stm-if="dashboard.discoveries.length > 0">
  351. <summary>Latest discoveries <span class="badge"
  352. stm-text="dashboard.discoveries.length">0</span></summary>
  353. <div class="dash-ambient-body">
  354. <ul>
  355. <li stm-for-discovery-in="dashboard.discoveries" class="md footnote"
  356. stm-markdown="discovery.body">body</li>
  357. </ul>
  358. <p class="text-muted"><a href="/discoveries">All discoveries →</a></p>
  359. </div>
  360. </details>
  361. </div>
  362. </div>
  363. </div>
  364. </body>
  365. </html>