| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <pstm-uri>/saddles</pstm-uri>
- <pstm-template>main</pstm-template>
- </head>
- <body>
- <!-- Goal 41 T8 (ux-notes quick win 8): each saddle card is split
- into a "now" HEAD (name, status badge, the one-line health
- verdict, current work) and an "admin" TAIL (keys, git
- identity, prompt note, capabilities, token, refresh/remove)
- folding under one "Keys & admin" expander. The three
- health fragments (badge / daemon / up X / contacted Y) are
- consolidated server-side into one verdict line
- (saddle.health_verdict). The fleet summary line above the
- grid surfaces offline/stale/fenced saddles immediately, and
- the capability catalogue became its own h2 "Routing tags"
- section (recommendation C). -->
- <div class="stack">
- <div class="cluster cluster-between">
- <h2>Saddles</h2>
- <button type="button" class="btn-primary" stm-if="saddles != null && saddles.is_admin"
- onclick="window.openAppDialog('saddle-register-dialog')">+ Register saddle</button>
- </div>
- <p class="text-muted">Fleet health and configuration. The dispatcher pairs queued tasks
- with idle saddles carrying every tag a task requires.</p>
- <p stm-if="saddles == null">Loading…</p>
- <div stm-if="saddles != null" class="stack">
- <!-- Fleet health summary (recommendation B): one line when
- any saddle is offline/stale/fenced, naming the worst
- offenders so the dispatcher risk leads the page. -->
- <p stm-if="saddles.fleet_alert.length > 0" class="alert alert-warning"
- stm-text="saddles.fleet_alert">fleet alert</p>
- <div class="cards">
- <section class="card" stm-for-saddle-in="saddles.saddles">
- <div class="card-header">
- <h3 stm-text="saddle.name">name</h3>
- <p class="cluster">
- <span stm-attribute.class="'badge badge-' + saddle.status" stm-text="saddle.status">status</span>
- <span class="text-muted" stm-text="saddle.host">host</span>
- </p>
- </div>
- <div class="card-body stack">
- <!-- The one-line health verdict consolidates the
- old three fragments; the badge keeps the
- at-a-glance colour the head is for. -->
- <p><span stm-attribute.class="'badge badge-health-' + saddle.health"
- stm-text="saddle.health">healthy</span>
- <span class="text-muted" stm-text="saddle.health_verdict">verdict</span></p>
- <p stm-if="saddle.current_goal_id > 0">Supervising goal:
- <a stm-attribute.href="'/goals/' + saddle.current_goal_id"
- stm-text="saddle.current_goal_title">goal</a>
- <span class="text-muted" stm-if="saddle.allocated_display.length > 0" stm-text="'since ' + saddle.allocated_display">since</span></p>
- <p stm-if="saddle.current_task_id > 0 && saddle.current_goal_id == 0">Current task:
- <a stm-attribute.href="'/tasks/' + saddle.current_task_id" stm-text="saddle.current_task_title">task</a>
- <span class="text-muted" stm-if="saddle.allocated_display.length > 0" stm-text="'since ' + saddle.allocated_display">since</span></p>
- <p stm-if="saddle.current_task_id == 0" class="text-muted">No current task</p>
- <!-- The admin tail: everything below the fold
- folds under ONE expander; the inner
- blocks keep their own details so the
- disclosure stays gradual. -->
- <details stm-if="saddles.is_admin" class="saddle-fold">
- <summary>Keys & admin</summary>
- <div class="stack saddle-admin">
- <p class="text-muted" stm-if="saddle.token.length > 0">API token:
- <code stm-text="saddle.token">token</code></p>
- <details stm-if="saddle.ssh_public_key.length > 0">
- <summary>SSH public key</summary>
- <p class="text-muted">Add this as a <strong>deploy key</strong> on the git server
- (with write access) so this saddle can clone and push.</p>
- <pre class="ssh-key"><code stm-text="saddle.ssh_public_key">key</code></pre>
- <button type="button"
- stm-attribute.data-clipboard="saddle.ssh_public_key"
- onclick="navigator.clipboard.writeText(this.getAttribute('data-clipboard')).then(function () { this.textContent = 'Copied'; }.bind(this))">Copy key</button>
- </details>
- <p stm-if="saddle.ssh_public_key.length == 0" class="text-muted">No SSH key reported yet —
- it appears here once the daemon generates one.</p>
- <details>
- <summary>Git identity</summary>
- <form stm-action="saddle.update_identity" class="cluster">
- <label>Committed as</label>
- <input type="text" name="git_name" stm-attribute.value="saddle.git_name"
- placeholder="saddle-1">
- <input type="text" name="git_email" stm-attribute.value="saddle.git_email"
- placeholder="saddle-1@example.net">
- <button type="submit" class="btn-sm">Save</button>
- <span class="text-muted">Empty fields fall back to the daemon's default identity.</span>
- </form>
- </details>
- <details>
- <summary>Prompt note</summary>
- <form stm-action="saddle.update_prompt_note" class="cluster">
- <label for="prompt-note">Served inside every assignment's system prompt</label>
- <textarea id="prompt-note" name="prompt_note" rows="3"
- stm-attribute.placeholder="'e.g. You have root and may install development tooling via DNF'"
- stm-text="saddle.prompt_note"></textarea>
- <button type="submit" class="btn-sm">Save</button>
- <span class="text-muted">A standing message about THIS machine for its agents; empty removes it (capped at 2000 chars).</span>
- </form>
- </details>
- <div>
- <p stm-if="saddle.capabilities.length > 0">Capabilities:
- <span stm-for-capability-in="saddle.capabilities" stm-key="capability.link_id">
- <span class="badge"><span stm-text="capability.name">tag</span>
- <button type="button" class="btn-ghost chip-remove"
- stm-action="capability.detach" stm-confirm="Remove this capability?">×</button></span>
- </span>
- </p>
- <p stm-if="saddle.capabilities.length == 0" class="text-muted">No capabilities — matches only tasks with none required.</p>
- <form class="cluster" stm-if="saddles.capability_options.length > 0"
- stm-action="saddle.attach_capability">
- <label stm-attribute.for="'cap-' + saddle.id">Add capability</label>
- <select stm-attribute.id="'cap-' + saddle.id" name="capability_id" required>
- <option stm-for-option-in="saddles.capability_options" stm-attribute.value="option.id"
- stm-text="option.name">capability</option>
- </select>
- <button type="submit" class="btn-sm">Add</button>
- </form>
- </div>
- <form class="cluster">
- <button type="button" class="btn-sm" stm-action="saddle.refresh">Refresh status</button>
- <button type="button" class="btn-sm btn-danger" stm-action="saddle.unregister"
- stm-confirm="Remove this saddle? A running task is re-queued.">Remove</button>
- </form>
- </div>
- </details>
- <!-- Non-admins see the routing tags without any
- admin machinery (the roles decision). -->
- <p stm-if="!saddles.is_admin && saddle.capabilities.length > 0">Capabilities:
- <span stm-for-capability-in="saddle.capabilities" stm-key="capability.link_id">
- <span class="badge"><span stm-text="capability.name">tag</span></span>
- </span>
- </p>
- </div>
- </section>
- <section class="card" stm-if="saddles.saddles.length == 0">
- <p class="text-muted">No saddles registered. The dispatcher pairs queued tasks with idle saddles automatically.</p>
- </section>
- </div>
- </div>
- <!-- The capability catalogue (recommendation C): its own
- h2-level section, a different modality than the card
- grid above, so the page reads as two deliberate
- sections. -->
- <div class="stack" stm-if="saddles != null">
- <h2 id="routing-tags">Routing tags</h2>
- <p class="text-muted">Tags the dispatcher routes by: a task is only assigned to a saddle carrying every
- tag the task requires. Manage a saddle's tags on its card above; require tags on a task's page.</p>
- <div class="cluster">
- <button type="button" class="btn-primary" stm-if="saddles.is_admin"
- onclick="window.openAppDialog('capability-create-dialog')">+ New tag</button>
- </div>
- <div class="table-wrap">
- <table class="table-hover">
- <thead><tr><th>Tag</th><th>Saddles carrying it</th><th>Tasks requiring it</th><th></th></tr></thead>
- <tbody>
- <tr stm-for-capability-in="saddles.capabilities">
- <td><span class="badge" stm-text="capability.name">tag</span></td>
- <td stm-text="capability.saddle_count">0</td>
- <td stm-text="capability.task_count">0</td>
- <td stm-if="saddles.is_admin"><button type="button" class="btn-sm btn-danger" stm-action="capability.delete"
- stm-confirm="Delete this capability everywhere?">Delete</button></td>
- </tr>
- <tr stm-if="saddles.capabilities.length == 0">
- <td colspan="4" class="text-muted">No capabilities defined yet.</td>
- </tr>
- </tbody>
- </table>
- </div>
- </div>
- </div>
- <!-- Goal 41 T8: the register form moved from a collapsed expander
- into a Spry <dialog> behind the "+ Register saddle" button -
- same fields, same stm-action (the shared openAppDialog helper
- closes it on the action's notify). -->
- <dialog id="saddle-register-dialog" class="dialog-sm">
- <div class="dialog-titlebar">
- <p class="dialog-title">Register saddle</p>
- <button type="button" class="dialog-close" aria-label="Close"
- onclick="window.closeAppDialog('saddle-register-dialog')">×</button>
- </div>
- <form stm-action="saddles.register">
- <div class="dialog-body form-rows">
- <div class="field"><label for="saddle-name">Name</label>
- <input type="text" id="saddle-name" name="name" required placeholder="saddle-2"></div>
- <div class="field"><label for="saddle-host">Host</label>
- <input type="text" id="saddle-host" name="host" required placeholder="localhost:7100"></div>
- </div>
- <div class="dialog-footer">
- <button type="button" class="btn-ghost"
- onclick="window.closeAppDialog('saddle-register-dialog')">Cancel</button>
- <button type="submit" class="btn-primary">Register</button>
- </div>
- </form>
- </dialog>
- <!-- The capability create form, same treatment. -->
- <dialog id="capability-create-dialog" class="dialog-sm">
- <div class="dialog-titlebar">
- <p class="dialog-title">New routing tag</p>
- <button type="button" class="dialog-close" aria-label="Close"
- onclick="window.closeAppDialog('capability-create-dialog')">×</button>
- </div>
- <form stm-action="saddles.create_capability">
- <div class="dialog-body form-rows">
- <div class="field"><label for="new-capability">Name</label>
- <input type="text" id="new-capability" name="name" required placeholder="gpu"></div>
- </div>
- <div class="dialog-footer">
- <button type="button" class="btn-ghost"
- onclick="window.closeAppDialog('capability-create-dialog')">Cancel</button>
- <button type="submit" class="btn-primary">Create tag</button>
- </div>
- </form>
- </dialog>
- </body>
- </html>
|