saddles.html 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <pstm-uri>/saddles</pstm-uri>
  5. <pstm-template>main</pstm-template>
  6. </head>
  7. <body>
  8. <!-- Goal 41 T8 (ux-notes quick win 8): each saddle card is split
  9. into a "now" HEAD (name, status badge, the one-line health
  10. verdict, current work) and an "admin" TAIL (keys, git
  11. identity, prompt note, capabilities, token, refresh/remove)
  12. folding under one "Keys &amp; admin" expander. The three
  13. health fragments (badge / daemon / up X / contacted Y) are
  14. consolidated server-side into one verdict line
  15. (saddle.health_verdict). The fleet summary line above the
  16. grid surfaces offline/stale/fenced saddles immediately, and
  17. the capability catalogue became its own h2 "Routing tags"
  18. section (recommendation C). -->
  19. <div class="stack">
  20. <div class="cluster cluster-between">
  21. <h2>Saddles</h2>
  22. <button type="button" class="btn-primary" stm-if="saddles != null && saddles.is_admin"
  23. onclick="window.openAppDialog('saddle-register-dialog')">+ Register saddle</button>
  24. </div>
  25. <p class="text-muted">Fleet health and configuration. The dispatcher pairs queued tasks
  26. with idle saddles carrying every tag a task requires.</p>
  27. <p stm-if="saddles == null">Loading…</p>
  28. <div stm-if="saddles != null" class="stack">
  29. <!-- Fleet health summary (recommendation B): one line when
  30. any saddle is offline/stale/fenced, naming the worst
  31. offenders so the dispatcher risk leads the page. -->
  32. <p stm-if="saddles.fleet_alert.length > 0" class="alert alert-warning"
  33. stm-text="saddles.fleet_alert">fleet alert</p>
  34. <div class="cards">
  35. <section class="card" stm-for-saddle-in="saddles.saddles">
  36. <div class="card-header">
  37. <h3 stm-text="saddle.name">name</h3>
  38. <p class="cluster">
  39. <span stm-attribute.class="'badge badge-' + saddle.status" stm-text="saddle.status">status</span>
  40. <span class="text-muted" stm-text="saddle.host">host</span>
  41. </p>
  42. </div>
  43. <div class="card-body stack">
  44. <!-- The one-line health verdict consolidates the
  45. old three fragments; the badge keeps the
  46. at-a-glance colour the head is for. -->
  47. <p><span stm-attribute.class="'badge badge-health-' + saddle.health"
  48. stm-text="saddle.health">healthy</span>
  49. <span class="text-muted" stm-text="saddle.health_verdict">verdict</span></p>
  50. <p stm-if="saddle.current_goal_id > 0">Supervising goal:
  51. <a stm-attribute.href="'/goals/' + saddle.current_goal_id"
  52. stm-text="saddle.current_goal_title">goal</a>
  53. <span class="text-muted" stm-if="saddle.allocated_display.length > 0" stm-text="'since ' + saddle.allocated_display">since</span></p>
  54. <p stm-if="saddle.current_task_id > 0 && saddle.current_goal_id == 0">Current task:
  55. <a stm-attribute.href="'/tasks/' + saddle.current_task_id" stm-text="saddle.current_task_title">task</a>
  56. <span class="text-muted" stm-if="saddle.allocated_display.length > 0" stm-text="'since ' + saddle.allocated_display">since</span></p>
  57. <p stm-if="saddle.current_task_id == 0" class="text-muted">No current task</p>
  58. <!-- The admin tail: everything below the fold
  59. folds under ONE expander; the inner
  60. blocks keep their own details so the
  61. disclosure stays gradual. -->
  62. <details stm-if="saddles.is_admin" class="saddle-fold">
  63. <summary>Keys &amp; admin</summary>
  64. <div class="stack saddle-admin">
  65. <p class="text-muted" stm-if="saddle.token.length > 0">API token:
  66. <code stm-text="saddle.token">token</code></p>
  67. <details stm-if="saddle.ssh_public_key.length > 0">
  68. <summary>SSH public key</summary>
  69. <p class="text-muted">Add this as a <strong>deploy key</strong> on the git server
  70. (with write access) so this saddle can clone and push.</p>
  71. <pre class="ssh-key"><code stm-text="saddle.ssh_public_key">key</code></pre>
  72. <button type="button"
  73. stm-attribute.data-clipboard="saddle.ssh_public_key"
  74. onclick="navigator.clipboard.writeText(this.getAttribute('data-clipboard')).then(function () { this.textContent = 'Copied'; }.bind(this))">Copy key</button>
  75. </details>
  76. <p stm-if="saddle.ssh_public_key.length == 0" class="text-muted">No SSH key reported yet —
  77. it appears here once the daemon generates one.</p>
  78. <details>
  79. <summary>Git identity</summary>
  80. <form stm-action="saddle.update_identity" class="cluster">
  81. <label>Committed as</label>
  82. <input type="text" name="git_name" stm-attribute.value="saddle.git_name"
  83. placeholder="saddle-1">
  84. <input type="text" name="git_email" stm-attribute.value="saddle.git_email"
  85. placeholder="saddle-1@example.net">
  86. <button type="submit" class="btn-sm">Save</button>
  87. <span class="text-muted">Empty fields fall back to the daemon's default identity.</span>
  88. </form>
  89. </details>
  90. <details>
  91. <summary>Prompt note</summary>
  92. <form stm-action="saddle.update_prompt_note" class="cluster">
  93. <label for="prompt-note">Served inside every assignment's system prompt</label>
  94. <textarea id="prompt-note" name="prompt_note" rows="3"
  95. stm-attribute.placeholder="'e.g. You have root and may install development tooling via DNF'"
  96. stm-text="saddle.prompt_note"></textarea>
  97. <button type="submit" class="btn-sm">Save</button>
  98. <span class="text-muted">A standing message about THIS machine for its agents; empty removes it (capped at 2000 chars).</span>
  99. </form>
  100. </details>
  101. <div>
  102. <p stm-if="saddle.capabilities.length > 0">Capabilities:
  103. <span stm-for-capability-in="saddle.capabilities" stm-key="capability.link_id">
  104. <span class="badge"><span stm-text="capability.name">tag</span>
  105. <button type="button" class="btn-ghost chip-remove"
  106. stm-action="capability.detach" stm-confirm="Remove this capability?">×</button></span>
  107. </span>
  108. </p>
  109. <p stm-if="saddle.capabilities.length == 0" class="text-muted">No capabilities — matches only tasks with none required.</p>
  110. <form class="cluster" stm-if="saddles.capability_options.length > 0"
  111. stm-action="saddle.attach_capability">
  112. <label stm-attribute.for="'cap-' + saddle.id">Add capability</label>
  113. <select stm-attribute.id="'cap-' + saddle.id" name="capability_id" required>
  114. <option stm-for-option-in="saddles.capability_options" stm-attribute.value="option.id"
  115. stm-text="option.name">capability</option>
  116. </select>
  117. <button type="submit" class="btn-sm">Add</button>
  118. </form>
  119. </div>
  120. <form class="cluster">
  121. <button type="button" class="btn-sm" stm-action="saddle.refresh">Refresh status</button>
  122. <button type="button" class="btn-sm btn-danger" stm-action="saddle.unregister"
  123. stm-confirm="Remove this saddle? A running task is re-queued.">Remove</button>
  124. </form>
  125. </div>
  126. </details>
  127. <!-- Non-admins see the routing tags without any
  128. admin machinery (the roles decision). -->
  129. <p stm-if="!saddles.is_admin && saddle.capabilities.length > 0">Capabilities:
  130. <span stm-for-capability-in="saddle.capabilities" stm-key="capability.link_id">
  131. <span class="badge"><span stm-text="capability.name">tag</span></span>
  132. </span>
  133. </p>
  134. </div>
  135. </section>
  136. <section class="card" stm-if="saddles.saddles.length == 0">
  137. <p class="text-muted">No saddles registered. The dispatcher pairs queued tasks with idle saddles automatically.</p>
  138. </section>
  139. </div>
  140. </div>
  141. <!-- The capability catalogue (recommendation C): its own
  142. h2-level section, a different modality than the card
  143. grid above, so the page reads as two deliberate
  144. sections. -->
  145. <div class="stack" stm-if="saddles != null">
  146. <h2 id="routing-tags">Routing tags</h2>
  147. <p class="text-muted">Tags the dispatcher routes by: a task is only assigned to a saddle carrying every
  148. tag the task requires. Manage a saddle's tags on its card above; require tags on a task's page.</p>
  149. <div class="cluster">
  150. <button type="button" class="btn-primary" stm-if="saddles.is_admin"
  151. onclick="window.openAppDialog('capability-create-dialog')">+ New tag</button>
  152. </div>
  153. <div class="table-wrap">
  154. <table class="table-hover">
  155. <thead><tr><th>Tag</th><th>Saddles carrying it</th><th>Tasks requiring it</th><th></th></tr></thead>
  156. <tbody>
  157. <tr stm-for-capability-in="saddles.capabilities">
  158. <td><span class="badge" stm-text="capability.name">tag</span></td>
  159. <td stm-text="capability.saddle_count">0</td>
  160. <td stm-text="capability.task_count">0</td>
  161. <td stm-if="saddles.is_admin"><button type="button" class="btn-sm btn-danger" stm-action="capability.delete"
  162. stm-confirm="Delete this capability everywhere?">Delete</button></td>
  163. </tr>
  164. <tr stm-if="saddles.capabilities.length == 0">
  165. <td colspan="4" class="text-muted">No capabilities defined yet.</td>
  166. </tr>
  167. </tbody>
  168. </table>
  169. </div>
  170. </div>
  171. </div>
  172. <!-- Goal 41 T8: the register form moved from a collapsed expander
  173. into a Spry <dialog> behind the "+ Register saddle" button -
  174. same fields, same stm-action (the shared openAppDialog helper
  175. closes it on the action's notify). -->
  176. <dialog id="saddle-register-dialog" class="dialog-sm">
  177. <div class="dialog-titlebar">
  178. <p class="dialog-title">Register saddle</p>
  179. <button type="button" class="dialog-close" aria-label="Close"
  180. onclick="window.closeAppDialog('saddle-register-dialog')">×</button>
  181. </div>
  182. <form stm-action="saddles.register">
  183. <div class="dialog-body form-rows">
  184. <div class="field"><label for="saddle-name">Name</label>
  185. <input type="text" id="saddle-name" name="name" required placeholder="saddle-2"></div>
  186. <div class="field"><label for="saddle-host">Host</label>
  187. <input type="text" id="saddle-host" name="host" required placeholder="localhost:7100"></div>
  188. </div>
  189. <div class="dialog-footer">
  190. <button type="button" class="btn-ghost"
  191. onclick="window.closeAppDialog('saddle-register-dialog')">Cancel</button>
  192. <button type="submit" class="btn-primary">Register</button>
  193. </div>
  194. </form>
  195. </dialog>
  196. <!-- The capability create form, same treatment. -->
  197. <dialog id="capability-create-dialog" class="dialog-sm">
  198. <div class="dialog-titlebar">
  199. <p class="dialog-title">New routing tag</p>
  200. <button type="button" class="dialog-close" aria-label="Close"
  201. onclick="window.closeAppDialog('capability-create-dialog')">×</button>
  202. </div>
  203. <form stm-action="saddles.create_capability">
  204. <div class="dialog-body form-rows">
  205. <div class="field"><label for="new-capability">Name</label>
  206. <input type="text" id="new-capability" name="name" required placeholder="gpu"></div>
  207. </div>
  208. <div class="dialog-footer">
  209. <button type="button" class="btn-ghost"
  210. onclick="window.closeAppDialog('capability-create-dialog')">Cancel</button>
  211. <button type="submit" class="btn-primary">Create tag</button>
  212. </div>
  213. </form>
  214. </dialog>
  215. </body>
  216. </html>