dashboard: point frontend at the /api/* backend routes
Phase 2 of the dashboard route consolidation. The backend now double-registers every bare top-level route (approve/deny/kill/restart/start/rebuild/destroy/ update-all/answer-question/cancel-question/purge-tombstone/matrix-account-login/ cancel-reminder/retry-reminder/request-spawn/op-send/meta-update/dashboard-stream/ dashboard-history) at an additional /api/<same> path. Switch every dashboard-pkg fetch / EventSource / form action to the /api/ form so "backend = /api/*" holds on the frontend side too. Bare paths still answer, so this is independently deployable; once it ships the backend can drop the bare registrations (phase 3). webhook/knowledge stays a distinct webhook prefix (forge-driven, no SPA caller). app.js's /rebuild,/start are the agent harness UI (different server) and are untouched. Part of #1846 (phase 2).
This commit is contained in:
parent
d3e3c4a41e
commit
905b38b6c7
9 changed files with 36 additions and 36 deletions
|
|
@ -252,7 +252,7 @@ export function renderApprovals() {
|
|||
if (savedSpawnName) spawnNameInput.value = savedSpawnName;
|
||||
if (spawnHadFocus) spawnNameInput.focus();
|
||||
const spawn = el('form', {
|
||||
method: 'POST', action: '/request-spawn',
|
||||
method: 'POST', action: '/api/request-spawn',
|
||||
class: 'spawnform', 'data-async': '', 'data-no-refresh': '',
|
||||
});
|
||||
spawn.append(
|
||||
|
|
@ -371,13 +371,13 @@ export function renderApprovals() {
|
|||
// on the POST and is surfaced to the manager via
|
||||
// HelperEvent::ApprovalResolved { note }.
|
||||
const denyForm = el('form', {
|
||||
method: 'POST', action: '/deny/' + a.id,
|
||||
method: 'POST', action: '/api/deny/' + a.id,
|
||||
class: 'inline', 'data-async': '', 'data-no-refresh': '',
|
||||
'data-prompt': 'reason for denying (optional, sent to manager):',
|
||||
});
|
||||
denyForm.append(el('button', { type: 'submit', class: 'btn btn-deny' }, 'DENY'));
|
||||
li.append(el('div', { class: 'approval-actions' },
|
||||
form('/approve/' + a.id, 'btn-approve', '◆ APPR0VE', null, {}, { noRefresh: true }),
|
||||
form('/api/approve/' + a.id, 'btn-approve', '◆ APPR0VE', null, {}, { noRefresh: true }),
|
||||
denyForm,
|
||||
));
|
||||
|
||||
|
|
@ -542,7 +542,7 @@ function buildQuestionLi(q) {
|
|||
appendLinkified(qBody, q.question, q.question_refs);
|
||||
li.append(head, qBody);
|
||||
const f = el('form', {
|
||||
method: 'POST', action: '/answer-question/' + q.id,
|
||||
method: 'POST', action: '/api/answer-question/' + q.id,
|
||||
class: 'qform', 'data-async': '', 'data-no-refresh': '',
|
||||
});
|
||||
const hasOptions = q.options && q.options.length;
|
||||
|
|
@ -611,7 +611,7 @@ function buildQuestionLi(q) {
|
|||
// merge-on-submit handler attached to the main form.
|
||||
const cancelTargetLabel = q.target ? q.target : 'asker';
|
||||
const cancelForm = el('form', {
|
||||
method: 'POST', action: '/cancel-question/' + q.id,
|
||||
method: 'POST', action: '/api/cancel-question/' + q.id,
|
||||
class: 'qform-cancel', 'data-async': '', 'data-no-refresh': '',
|
||||
'data-confirm': `cancel this question? ${cancelTargetLabel} will see `
|
||||
+ '"[cancelled]" as the answer.',
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
|
||||
<!-- R3BU1LD QU3U3: pending + running rebuilds, meta-updates, and
|
||||
first-spawns. Driven by /api/state.rebuild_queue cold-load +
|
||||
live `rebuild_queue_changed` over /dashboard/stream. Default tab. -->
|
||||
live `rebuild_queue_changed` over /api/dashboard/stream. Default tab. -->
|
||||
<section class="core-pane" id="core-pane-rebuild" data-tab-pane="rebuild"
|
||||
role="tabpanel" aria-labelledby="core-tab-rebuild">
|
||||
<p class="meta">pending + running rebuilds, meta-updates, and first-spawns. one runs at a time; meta-update cascades nest under their parent. dedup: re-enqueueing a still-queued op collapses into the existing entry.</p>
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ function renderMetaInputs(s) {
|
|||
}
|
||||
const f = el('form', {
|
||||
method: 'POST',
|
||||
action: '/meta-update',
|
||||
action: '/api/meta-update',
|
||||
class: 'meta-inputs-form',
|
||||
'data-async': '',
|
||||
'data-no-refresh': '',
|
||||
|
|
@ -331,7 +331,7 @@ function renderTombstones(s) {
|
|||
|
||||
const actions = el('div', { class: 'actions' });
|
||||
const respawn = el('form', {
|
||||
method: 'POST', action: '/request-spawn',
|
||||
method: 'POST', action: '/api/request-spawn',
|
||||
class: 'inline', 'data-async': '',
|
||||
'data-confirm': 'queue spawn approval for ' + t.name + '? state will be reused.',
|
||||
});
|
||||
|
|
@ -341,7 +341,7 @@ function renderTombstones(s) {
|
|||
);
|
||||
actions.append(respawn);
|
||||
actions.append(form(
|
||||
'/purge-tombstone/' + t.name, 'btn-destroy', 'PURG3',
|
||||
'/api/purge-tombstone/' + t.name, 'btn-destroy', 'PURG3',
|
||||
'PURGE ' + t.name + '? config history, claude creds, '
|
||||
+ 'and notes are all WIPED. no undo.',
|
||||
{}, { noRefresh: true },
|
||||
|
|
@ -545,7 +545,7 @@ async function init() {
|
|||
|
||||
await refreshState();
|
||||
|
||||
const es = openStream('/dashboard/stream');
|
||||
const es = openStream('/api/dashboard/stream');
|
||||
if (es) {
|
||||
es.onmessage = (e) => {
|
||||
let ev;
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ import {
|
|||
termCreate({
|
||||
logEl: flow,
|
||||
pillAnchor: flowMain,
|
||||
historyUrl: '/dashboard/history',
|
||||
historyUrl: '/api/dashboard/history',
|
||||
// Server-side filter — only the kinds this page actually renders
|
||||
// or routes (sent/delivered → broker terminal,
|
||||
// container_state_changed/_removed → local autocomplete cache).
|
||||
|
|
@ -218,7 +218,7 @@ import {
|
|||
// JSON-serialise is skipped entirely on irrelevant kinds. The
|
||||
// dashboard tabs page (tabs.js) keeps the unfiltered subscribe
|
||||
// since it routes every mutation kind into its derived stores.
|
||||
streamUrl: '/dashboard/stream?kinds=sent,delivered,container_state_changed,container_removed',
|
||||
streamUrl: '/api/dashboard/stream?kinds=sent,delivered,container_state_changed,container_removed',
|
||||
// Route through the SharedWorker — see docs/web-ui.md (SSE
|
||||
// multiplexing paragraph). Worker keys on the full URL incl.
|
||||
// query string, so this filtered subscribe is its own upstream
|
||||
|
|
@ -383,7 +383,7 @@ import {
|
|||
// /op-send returns 200. The SSE channel carries the resulting
|
||||
// MessageEvent → the terminal renders the sent row on its own;
|
||||
// no /api/state refetch needed.
|
||||
const resp = await fetch('/op-send', {
|
||||
const resp = await fetch('/api/op-send', {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams(fd),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@ import { createTabStrip } from '@hive/shared/tabs.js';
|
|||
if (buildRefreshTimer) clearTimeout(buildRefreshTimer);
|
||||
buildRefreshTimer = setTimeout(fetchBuild, 2000);
|
||||
};
|
||||
const es = openStream('/dashboard/stream');
|
||||
const es = openStream('/api/dashboard/stream');
|
||||
if (es) {
|
||||
es.onmessage = (e) => {
|
||||
let ev;
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ async function submitLogin(e) {
|
|||
btn.textContent = 'logging in…';
|
||||
|
||||
try {
|
||||
const resp = await fetch('/matrix-account-login', {
|
||||
const resp = await fetch('/api/matrix-account-login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams(fd),
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ function renderReminders(rows) {
|
|||
// the row up again on its next 5s tick. No data-no-refresh
|
||||
// — the resulting refreshState re-fires refreshReminders.
|
||||
const retryForm = el('form', {
|
||||
method: 'POST', action: '/retry-reminder/' + r.id,
|
||||
method: 'POST', action: '/api/retry-reminder/' + r.id,
|
||||
class: 'inline', 'data-async': '',
|
||||
});
|
||||
retryForm.append(el('button',
|
||||
|
|
@ -96,7 +96,7 @@ function renderReminders(rows) {
|
|||
actions.append(retryForm);
|
||||
}
|
||||
const cancelForm = el('form', {
|
||||
method: 'POST', action: '/cancel-reminder/' + r.id,
|
||||
method: 'POST', action: '/api/cancel-reminder/' + r.id,
|
||||
class: 'inline', 'data-async': '',
|
||||
'data-confirm': `cancel reminder ${r.id} for ${r.agent}? this drops the queued delivery; no undo.`,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// every server-sent event out to every connected tab via MessagePort.
|
||||
//
|
||||
// Problem this solves: every dashboard / agent tab opens its own
|
||||
// `EventSource('/dashboard/stream')`. Browsers cap concurrent
|
||||
// `EventSource('/api/dashboard/stream')`. Browsers cap concurrent
|
||||
// connections per host (~6), and Firefox throttles / disconnects
|
||||
// background tabs when many are open. The result: tabs silently
|
||||
// drop the SSE, fall behind, and only catch up on focus.
|
||||
|
|
@ -15,8 +15,8 @@
|
|||
// Wire protocol (port.postMessage payloads):
|
||||
//
|
||||
// tab → worker
|
||||
// { kind: 'subscribe', url: '/dashboard/stream' }
|
||||
// { kind: 'unsubscribe', url: '/dashboard/stream' }
|
||||
// { kind: 'subscribe', url: '/api/dashboard/stream' }
|
||||
// { kind: 'unsubscribe', url: '/api/dashboard/stream' }
|
||||
//
|
||||
// worker → tab
|
||||
// { kind: 'open', url: '...' } relayed from EventSource.onopen,
|
||||
|
|
|
|||
|
|
@ -364,16 +364,16 @@ window.marked = marked;
|
|||
// Show only actions that are applicable in the current state.
|
||||
if (c.running) {
|
||||
dropdown.append(
|
||||
menuItem('↺ R3ST4RT', { action: '/restart/', confirm: `restart ${c.name}?` }),
|
||||
menuItem('■ ST0P', { action: '/kill/', confirm: `stop ${c.name}?`, confirmLabel: '■ stop', graceful: true }),
|
||||
menuItem('↺ R3ST4RT', { action: '/api/restart/', confirm: `restart ${c.name}?` }),
|
||||
menuItem('■ ST0P', { action: '/api/kill/', confirm: `stop ${c.name}?`, confirmLabel: '■ stop', graceful: true }),
|
||||
);
|
||||
} else {
|
||||
dropdown.append(
|
||||
menuItem('▶ ST4RT', { action: '/start/', confirm: `start ${c.name}?` }),
|
||||
menuItem('▶ ST4RT', { action: '/api/start/', confirm: `start ${c.name}?` }),
|
||||
);
|
||||
}
|
||||
dropdown.append(
|
||||
menuItem('↻ R3BU1LD', { action: '/rebuild/', confirm: `rebuild ${c.name}? hot-reloads the container.` }),
|
||||
menuItem('↻ R3BU1LD', { action: '/api/rebuild/', confirm: `rebuild ${c.name}? hot-reloads the container.` }),
|
||||
menuSep(),
|
||||
// Deep-link to the AGENT log tab pre-filtered to this container.
|
||||
// The ?agent= param is read by logs.js on load and pre-selects this
|
||||
|
|
@ -387,11 +387,11 @@ window.marked = marked;
|
|||
dropdown.append(
|
||||
menuSep(),
|
||||
menuItem('DESTR0Y', {
|
||||
action: '/destroy/',
|
||||
action: '/api/destroy/',
|
||||
confirm: `destroy ${c.name}? container removed; state + creds kept.`,
|
||||
}),
|
||||
menuItem('PURG3', {
|
||||
action: '/destroy/',
|
||||
action: '/api/destroy/',
|
||||
body: { purge: 'on' },
|
||||
confirm: `PURGE ${c.name}? WIPES container, config history, claude creds, and notes. no undo.`,
|
||||
}),
|
||||
|
|
@ -722,7 +722,7 @@ window.marked = marked;
|
|||
}
|
||||
if (c.needs_update) {
|
||||
head.append(form(
|
||||
'/rebuild/' + c.name, 'badge badge-warn btn-inline', 'needs update ↻',
|
||||
'/api/rebuild/' + c.name, 'badge badge-warn btn-inline', 'needs update ↻',
|
||||
'rebuild ' + c.name + '? hot-reloads the container.',
|
||||
{}, { noRefresh: true },
|
||||
));
|
||||
|
|
@ -802,7 +802,7 @@ window.marked = marked;
|
|||
|
||||
if (anyStale) {
|
||||
root.append(form(
|
||||
'/update-all', 'btn-rebuild', '↻ UPD4TE 4LL',
|
||||
'/api/update-all', 'btn-rebuild', '↻ UPD4TE 4LL',
|
||||
'rebuild every stale container?',
|
||||
{}, { noRefresh: true },
|
||||
));
|
||||
|
|
@ -1001,32 +1001,32 @@ window.marked = marked;
|
|||
}
|
||||
|
||||
addBulkButton(actions, 'btn-restart', '↺ R3ST4RT', allRunning, selected, {
|
||||
action: '/restart/',
|
||||
action: '/api/restart/',
|
||||
confirm: (names) => `restart ${names.length} agent${names.length === 1 ? '' : 's'} (${names.join(', ')})?`,
|
||||
disabledTitle: why('↺ R3ST4RT', stoppedNames.map((n) => `\`${n}\` is stopped`)),
|
||||
});
|
||||
addBulkButton(actions, 'btn-stop', '■ ST0P', allRunning, selected, {
|
||||
action: '/kill/',
|
||||
action: '/api/kill/',
|
||||
confirm: (names) => `stop ${names.length} agent${names.length === 1 ? '' : 's'} (${names.join(', ')})?`,
|
||||
confirmLabel: '■ stop',
|
||||
graceful: true,
|
||||
disabledTitle: why('■ ST0P', stoppedNames.map((n) => `\`${n}\` is already stopped`)),
|
||||
});
|
||||
addBulkButton(actions, 'btn-start', '▶ ST4RT', allStopped, selected, {
|
||||
action: '/start/',
|
||||
action: '/api/start/',
|
||||
confirm: (names) => `start ${names.length} agent${names.length === 1 ? '' : 's'} (${names.join(', ')})?`,
|
||||
disabledTitle: why('▶ ST4RT', runningNames.map((n) => `\`${n}\` is already running`)),
|
||||
});
|
||||
addBulkButton(actions, 'btn-rebuild', '↻ R3BU1LD', true, selected, {
|
||||
action: '/rebuild/',
|
||||
action: '/api/rebuild/',
|
||||
confirm: (names) => `rebuild ${names.length} agent${names.length === 1 ? '' : 's'} (${names.join(', ')})? hot-reloads each container.`,
|
||||
});
|
||||
addBulkButton(actions, 'btn-destroy', 'DESTR0Y', true, selected, {
|
||||
action: '/destroy/',
|
||||
action: '/api/destroy/',
|
||||
confirm: (names) => `destroy ${names.length} agent${names.length === 1 ? '' : 's'} (${names.join(', ')})? containers are removed; state + creds kept.`,
|
||||
});
|
||||
addBulkButton(actions, 'btn-destroy', 'PURG3', true, selected, {
|
||||
action: '/destroy/',
|
||||
action: '/api/destroy/',
|
||||
body: { purge: 'on' },
|
||||
confirm: (names) => `PURGE ${names.length} agent${names.length === 1 ? '' : 's'} (${names.join(', ')})? containers, config history, claude creds, and notes are all WIPED. no undo.`,
|
||||
});
|
||||
|
|
@ -1487,7 +1487,7 @@ window.marked = marked;
|
|||
// lost during the disconnect window (same pattern as flow.js's
|
||||
// onStreamOpen).
|
||||
//
|
||||
// Both /dashboard.html and /flow.html subscribe to `/dashboard/stream`
|
||||
// Both /dashboard.html and /flow.html subscribe to `/api/dashboard/stream`
|
||||
// and filter client-side — the dashboard ignores broker traffic
|
||||
// and the inbox ignores mutation events.
|
||||
const MUTATION_HANDLERS = {
|
||||
|
|
@ -1514,7 +1514,7 @@ window.marked = marked;
|
|||
// paragraph) for the design + Firefox throttling motivation.
|
||||
// `openStream` returns an EventSource-shaped facade with a graceful
|
||||
// direct-EventSource fallback when SharedWorker isn't supported.
|
||||
const es = openStream('/dashboard/stream');
|
||||
const es = openStream('/api/dashboard/stream');
|
||||
es.onmessage = (e) => {
|
||||
let ev;
|
||||
try { ev = JSON.parse(e.data); } catch { return; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue