agent ui: answer questions inline from the per-agent page
loose-ends question rows get a textarea + send button; the operator answers as operator by POSTing to the core dashboard's /answer-question route, not the per-agent socket — keeps the operator-authority path off the agent's own socket. cross-origin POST needs a CORS shim on that route for now; drops out once the gateway makes the page same-origin. also splits deployment/ops/boundaries/gateway work into TODO-ops.md.
This commit is contained in:
parent
f8795dc029
commit
56e7eb6e73
5 changed files with 221 additions and 8 deletions
|
|
@ -22,6 +22,12 @@
|
|||
return e;
|
||||
};
|
||||
|
||||
// Base URL of the host dashboard (core backend). Set once the first
|
||||
// /api/state lands. Operator-authority actions (answering a question
|
||||
// as the operator) POST here rather than to this agent's own socket —
|
||||
// see TODO-ops.md for why the boundary lives on the core side.
|
||||
let dashboardBase = '';
|
||||
|
||||
// ─── async-form submit (shared with dashboard) ──────────────────────────
|
||||
document.addEventListener('submit', async (e) => {
|
||||
const f = e.target;
|
||||
|
|
@ -68,6 +74,7 @@
|
|||
// ↑ DASHB04RD — back-link to the host dashboard. Opens in a new
|
||||
// tab to keep the agent page anchored where the operator is.
|
||||
const dashUrl = `${location.protocol}//${location.hostname}:${dashboardPort}/`;
|
||||
dashboardBase = dashUrl;
|
||||
title.append(
|
||||
el('a', {
|
||||
href: dashUrl, target: '_blank', rel: 'noopener',
|
||||
|
|
@ -454,6 +461,7 @@
|
|||
el('span', { class: 'inbox-sep' }, t.asker + ' → ' + target), ' ',
|
||||
el('span', { class: 'inbox-ts' }, fmtAge(t.age_seconds || 0) + ' ago'),
|
||||
el('div', { class: 'inbox-body' }, t.question || ''),
|
||||
buildAnswerForm(t.id),
|
||||
);
|
||||
} else if (t.kind === 'reminder') {
|
||||
// due_at is an absolute unix-seconds value; show time-until-fire
|
||||
|
|
@ -474,6 +482,42 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Inline "answer as operator" form for a question loose-end. POSTs to
|
||||
// the host dashboard (core backend), never this agent's socket — the
|
||||
// core is the only place that can stamp `operator` as the answerer.
|
||||
function buildAnswerForm(id) {
|
||||
const wrap = el('div', { class: 'answer-form' });
|
||||
const ta = el('textarea', { rows: '2', placeholder: 'answer as operator…' });
|
||||
const btn = el('button', { type: 'button' }, 'send answer');
|
||||
const status = el('span', { class: 'answer-status' });
|
||||
btn.addEventListener('click', async () => {
|
||||
const answer = ta.value.trim();
|
||||
if (!answer) { status.textContent = 'answer required'; return; }
|
||||
if (!dashboardBase) { status.textContent = 'dashboard url unknown'; return; }
|
||||
btn.disabled = true;
|
||||
status.textContent = 'sending…';
|
||||
try {
|
||||
const resp = await fetch(dashboardBase + 'answer-question/' + id, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: 'answer=' + encodeURIComponent(answer),
|
||||
});
|
||||
if (resp.ok) {
|
||||
status.textContent = 'answered ✓';
|
||||
refreshLooseEnds();
|
||||
} else {
|
||||
status.textContent = 'failed: ' + (await resp.text());
|
||||
btn.disabled = false;
|
||||
}
|
||||
} catch (err) {
|
||||
status.textContent = 'failed: ' + err;
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
wrap.append(ta, btn, status);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function renderInbox(rows) {
|
||||
const root = $('inbox-section');
|
||||
const list = $('inbox-list');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue