mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
2221 lines
125 KiB
JavaScript
2221 lines
125 KiB
JavaScript
const consoleViewDefinitions = Object.freeze({
|
|
overview: { title: 'Overview' },
|
|
'owner-briefing': { title: 'Briefing' },
|
|
'flow-watch': { title: 'Flow watch' },
|
|
work: { title: 'Work' },
|
|
'initiative-progress': { title: 'Progress' },
|
|
fleet: { title: 'Fleet' },
|
|
communications: { title: 'Conversations' },
|
|
organization: { title: 'Organization' },
|
|
evidence: { title: 'Evidence' },
|
|
delivery: { title: 'Release gates' },
|
|
});
|
|
|
|
function installConsoleViewRouting() {
|
|
const nav = document.querySelector('.console-rail');
|
|
const sections = [...document.querySelectorAll('.console-workspace > [data-console-view]')];
|
|
const links = [...(nav?.querySelectorAll('a[href^="#"]') ?? [])];
|
|
if (!nav || !sections.length || !links.length) return { sync: () => 'overview' };
|
|
|
|
let lastHash = null;
|
|
const initialHash = globalThis.location.hash;
|
|
const decodeHash = (hash) => {
|
|
try { return decodeURIComponent(String(hash ?? '').replace(/^#/, '')); } catch { return ''; }
|
|
};
|
|
|
|
function resolveView(hash) {
|
|
const fragment = decodeHash(hash);
|
|
if (consoleViewDefinitions[fragment]) return fragment;
|
|
const target = document.getElementById(fragment);
|
|
const containingView = target?.closest('[data-console-view]')?.dataset.consoleView;
|
|
return consoleViewDefinitions[containingView] ? containingView : 'overview';
|
|
}
|
|
|
|
function accountForStickyNavigation(target) {
|
|
const workspace = document.querySelector('.console-workspace');
|
|
if (!workspace || !target) return;
|
|
const railBounds = nav.getBoundingClientRect();
|
|
const workspaceBounds = workspace.getBoundingClientRect();
|
|
const overlapsWorkspace = railBounds.right > workspaceBounds.left + 1
|
|
&& workspaceBounds.right > railBounds.left + 1;
|
|
if (overlapsWorkspace) {
|
|
target.style.scrollMarginTop = `${Math.ceil(railBounds.height + 8)}px`;
|
|
} else {
|
|
target.style.removeProperty('scroll-margin-top');
|
|
}
|
|
}
|
|
|
|
function positionNestedTarget(target, targetId, view) {
|
|
const targetView = target?.closest('[data-console-view]')?.dataset.consoleView;
|
|
if (!target || targetId === view || targetView !== view) return;
|
|
for (let ancestor = target; ancestor; ancestor = ancestor.parentElement) {
|
|
if (ancestor instanceof HTMLDetailsElement) ancestor.open = true;
|
|
}
|
|
accountForStickyNavigation(target);
|
|
if (!target.hasAttribute('tabindex') && !target.matches('a[href], button, input, select, textarea, summary')) {
|
|
target.tabIndex = -1;
|
|
}
|
|
target.scrollIntoView?.({ block: 'start', behavior: 'instant' });
|
|
target.focus?.({ preventScroll: true });
|
|
}
|
|
|
|
const initialFragment = decodeHash(initialHash);
|
|
let initialPositionPending = Boolean(initialFragment && !consoleViewDefinitions[initialFragment]
|
|
&& document.getElementById(initialFragment));
|
|
let userIntentVersion = 0;
|
|
const abandonInitialPosition = () => {
|
|
initialPositionPending = false;
|
|
userIntentVersion += 1;
|
|
};
|
|
const onNavigation = () => {
|
|
abandonInitialPosition();
|
|
sync({ focus: true });
|
|
};
|
|
|
|
function sync({ focus = false, force = false } = {}) {
|
|
const hash = globalThis.location.hash || '#overview';
|
|
if (!force && hash === lastHash) return resolveView(hash);
|
|
lastHash = hash;
|
|
const view = resolveView(hash);
|
|
const definition = consoleViewDefinitions[view];
|
|
const visible = sections.filter((section) => section.dataset.consoleView === view);
|
|
sections.forEach((section) => { section.hidden = section.dataset.consoleView !== view; });
|
|
links.forEach((link) => {
|
|
const route = resolveView(link.getAttribute('href'));
|
|
if (route === view) link.setAttribute('aria-current', 'page');
|
|
else link.removeAttribute('aria-current');
|
|
});
|
|
const workspace = document.querySelector('.console-workspace');
|
|
if (workspace) workspace.dataset.currentView = view;
|
|
document.title = view === 'overview' ? 'TORCH Dashboard' : `${definition.title} | TORCH Local Fleet Console`;
|
|
const announcement = document.querySelector('#console-view-status');
|
|
if (announcement) announcement.textContent = `Viewing ${definition.title}.`;
|
|
|
|
if (focus) {
|
|
const targetId = decodeHash(hash);
|
|
const target = document.getElementById(targetId);
|
|
const targetView = target?.closest('[data-console-view]')?.dataset.consoleView;
|
|
if (target && targetId !== view && targetView === view) {
|
|
positionNestedTarget(target, targetId, view);
|
|
} else {
|
|
const labelledBy = visible[0]?.getAttribute('aria-labelledby');
|
|
const heading = (labelledBy && document.getElementById(labelledBy))
|
|
?? visible[0]?.querySelector('h1, h2');
|
|
if (heading) {
|
|
heading.tabIndex = -1;
|
|
heading.focus({ preventScroll: true });
|
|
}
|
|
if (target && targetId === view && targetView === view) {
|
|
accountForStickyNavigation(target);
|
|
target.scrollIntoView?.({ block: 'start', behavior: 'instant' });
|
|
} else {
|
|
globalThis.scrollTo?.({ top: 0, behavior: 'instant' });
|
|
}
|
|
}
|
|
}
|
|
return view;
|
|
}
|
|
|
|
globalThis.addEventListener('hashchange', onNavigation);
|
|
globalThis.addEventListener('popstate', onNavigation);
|
|
globalThis.addEventListener('wheel', abandonInitialPosition, { passive: true });
|
|
globalThis.addEventListener('touchstart', abandonInitialPosition, { passive: true });
|
|
globalThis.addEventListener('pointerdown', abandonInitialPosition, { passive: true });
|
|
globalThis.addEventListener('keydown', (event) => {
|
|
if (['ArrowDown', 'ArrowUp', 'PageDown', 'PageUp', 'Home', 'End', ' '].includes(event.key)) {
|
|
abandonInitialPosition();
|
|
}
|
|
});
|
|
sync({ focus: Boolean(initialFragment && !consoleViewDefinitions[initialFragment]) });
|
|
return {
|
|
sync,
|
|
afterInitialLayout() {
|
|
if (!initialPositionPending) return;
|
|
initialPositionPending = false;
|
|
if (globalThis.location.hash !== initialHash) return;
|
|
const targetId = decodeHash(initialHash);
|
|
const view = resolveView(initialHash);
|
|
positionNestedTarget(document.getElementById(targetId), targetId, view);
|
|
},
|
|
userIntentVersion: () => userIntentVersion,
|
|
};
|
|
}
|
|
|
|
const $ = (selector) => document.querySelector(selector);
|
|
const consoleViewRouter = installConsoleViewRouting();
|
|
const demoWorkspace = globalThis.TorchConsoleDemo;
|
|
const workViews = globalThis.TorchWorkViews;
|
|
const workProgress = globalThis.TorchWorkProgress;
|
|
const emptyWorkFilters = workViews?.emptyFilters() ?? { query: '', owner: '', state: '', priority: '', domain: '' };
|
|
const workViewState = { initialized: false, key: null, storage: null, views: [], selectedId: '', warning: null };
|
|
let runtimeProfileAgents = [];
|
|
let runtimeProfileAdapters = [];
|
|
let currentBacklog = [];
|
|
let currentBacklogHealth = [];
|
|
const refreshProtection = globalThis.TorchLiveRefresh.createProtection(document);
|
|
refreshProtection.remember();
|
|
let renderingSnapshot = false;
|
|
let heldPanels = 0;
|
|
|
|
function invalidateStalePriorityPreviews(tasks = []) {
|
|
const currentById = new Map(tasks.map((task) => [task.id, task]));
|
|
for (const form of document.querySelectorAll('.priority-change-form[data-preview-token]')) {
|
|
const task = currentById.get(form.dataset.taskId);
|
|
const currentRevision = Number(task?.revision);
|
|
const previewRevision = Number(form.dataset.revision);
|
|
if (!task || !Number.isSafeInteger(currentRevision) || currentRevision === previewRevision) continue;
|
|
|
|
const ticket = form.closest('.task-ticket');
|
|
if (!ticket) continue;
|
|
const priority = task.priority ?? 'normal';
|
|
const priorityBadge = ticket.querySelector('.ticket-topline .priority');
|
|
if (priorityBadge) {
|
|
priorityBadge.className = `priority priority-${escapeHtml(priority)}`;
|
|
priorityBadge.textContent = priority;
|
|
}
|
|
form.dataset.revision = String(currentRevision);
|
|
delete form.dataset.previewToken;
|
|
delete form.dataset.planHash;
|
|
const priorityField = form.querySelector('[name="priority"]');
|
|
const reasonField = form.querySelector('[name="reason"]');
|
|
if (priorityField) priorityField.disabled = false;
|
|
if (reasonField) reasonField.disabled = false;
|
|
const preview = form.querySelector('[data-priority-preview]');
|
|
if (preview) {
|
|
preview.replaceChildren();
|
|
preview.hidden = true;
|
|
}
|
|
priorityStatus(form, `Task evidence advanced to revision ${currentRevision}. The older preview was cleared; review the current task before previewing again.`);
|
|
}
|
|
}
|
|
|
|
const escapeHtml = (value) => {
|
|
const element = document.createElement('span');
|
|
element.textContent = value ?? '';
|
|
return element.innerHTML;
|
|
};
|
|
|
|
function localPath(value) {
|
|
const path = String(value ?? '');
|
|
return path.startsWith('/') && !path.startsWith('//') ? escapeHtml(path) : '';
|
|
}
|
|
|
|
function empty(message) {
|
|
return `<p class="empty-state">${escapeHtml(message)}</p>`;
|
|
}
|
|
|
|
function setHtml(selector, content) {
|
|
const element = $(selector);
|
|
if (renderingSnapshot && refreshProtection.protects(element)) { heldPanels += 1; return; }
|
|
if (element) {
|
|
const open = new Set([...element.querySelectorAll('details[open]')].map((details) => details.querySelector(':scope > summary')?.textContent));
|
|
element.innerHTML = content;
|
|
element.querySelectorAll('details').forEach((details) => {
|
|
if (open.has(details.querySelector(':scope > summary')?.textContent)) details.open = true;
|
|
});
|
|
}
|
|
}
|
|
|
|
function setText(selector, value) {
|
|
const element = $(selector);
|
|
if (element) element.textContent = value ?? '';
|
|
}
|
|
|
|
function compactOwnerBriefingProvenance() {
|
|
const publication = $('#owner-briefing-content')?.querySelector('.briefing-publication');
|
|
if (!publication) return;
|
|
|
|
const timestamp = publication.querySelector('p time');
|
|
const window = [...publication.querySelectorAll('.briefing-muted')]
|
|
.find((paragraph) => paragraph.textContent.trim().startsWith('Window:'));
|
|
if (timestamp || window) {
|
|
const disclosure = document.createElement('details');
|
|
disclosure.className = 'briefing-provenance';
|
|
const summary = document.createElement('summary');
|
|
summary.textContent = 'Publication time and reporting window';
|
|
disclosure.append(summary);
|
|
if (timestamp) {
|
|
const exactTime = timestamp.cloneNode(true);
|
|
exactTime.textContent = timestamp.getAttribute('datetime') || timestamp.textContent;
|
|
const published = document.createElement('p');
|
|
published.append('Published ', exactTime);
|
|
disclosure.append(published);
|
|
timestamp.textContent = ageLabel(timestamp.getAttribute('datetime'));
|
|
timestamp.title = exactTime.textContent;
|
|
}
|
|
if (window) {
|
|
const limitation = 'Recorded evidence, not independent live verification.';
|
|
if (window.textContent.includes(limitation)) {
|
|
const visibleLimitation = window.cloneNode(false);
|
|
visibleLimitation.textContent = limitation;
|
|
window.textContent = window.textContent.replace(limitation, '').trim();
|
|
window.after(visibleLimitation);
|
|
}
|
|
disclosure.append(window);
|
|
}
|
|
publication.append(disclosure);
|
|
}
|
|
|
|
for (const code of publication.querySelectorAll('code')) {
|
|
const commit = code.textContent.trim();
|
|
if (!/^[a-f0-9]{40}$/i.test(commit)) continue;
|
|
const disclosure = document.createElement('details');
|
|
disclosure.className = 'briefing-provenance';
|
|
const summary = document.createElement('summary');
|
|
summary.textContent = `Commit ${shortCommit(commit)}`;
|
|
const exact = document.createElement('code');
|
|
exact.textContent = commit;
|
|
disclosure.append(summary, exact);
|
|
code.replaceWith(disclosure);
|
|
}
|
|
}
|
|
|
|
function shortCommit(value) {
|
|
return value ? String(value).slice(0, 9) : '—';
|
|
}
|
|
|
|
function ageLabel(value) {
|
|
if (!value) return 'time not recorded';
|
|
const time = Date.parse(value);
|
|
if (!Number.isFinite(time)) return 'time unavailable';
|
|
const minutes = Math.max(0, Math.floor((Date.now() - time) / 60_000));
|
|
if (minutes < 1) return 'just now';
|
|
if (minutes < 60) return `${minutes}m ago`;
|
|
const hours = Math.floor(minutes / 60);
|
|
if (hours < 24) return `${hours}h ago`;
|
|
return `${Math.floor(hours / 24)}d ago`;
|
|
}
|
|
|
|
function taskCard(task, healthFindings = []) {
|
|
const affected = (task.affectedDomains ?? []).join(', ');
|
|
const dependencies = (task.dependencies ?? []).join(', ');
|
|
const acceptance = (task.acceptanceCriteria ?? []).map((criterion) => `<li>${escapeHtml(criterion)}</li>`).join('');
|
|
const detail = task.description || acceptance || task.observedAt;
|
|
const staleEvidence = healthFindings.find((finding) => finding.taskId === task.id
|
|
&& ['BACKLOG_OBSERVED_COMMIT_STALE', 'BACKLOG_OBSERVED_COMMIT_MISSING'].includes(finding.code));
|
|
return `<article class="task-ticket">
|
|
<div class="ticket-topline"><span class="priority priority-${escapeHtml(task.priority ?? 'normal')}">${escapeHtml(task.priority ?? 'normal')}</span></div>
|
|
<h4>${escapeHtml(task.title ?? 'Untitled task')}</h4>
|
|
<p>${escapeHtml(detail || 'No task description recorded.')}</p>
|
|
<details class="task-reference"><summary>Task reference</summary><code>${escapeHtml(task.id)}</code></details>
|
|
${staleEvidence ? `<div class="evidence-warning" role="status"><strong>Evidence needs refresh</strong><p>${escapeHtml(staleEvidence.recommendation)}</p></div>` : ''}
|
|
<dl class="ticket-meta">
|
|
<div><dt>Owner</dt><dd>${escapeHtml(task.owner ?? 'Unassigned')}</dd></div>
|
|
${affected ? `<div><dt>Domain</dt><dd>${escapeHtml(affected)}</dd></div>` : ''}
|
|
${task.feature ? `<div><dt>Feature</dt><dd>${escapeHtml(task.feature)}</dd></div>` : ''}
|
|
${task.milestone ? `<div><dt>Milestone</dt><dd>${escapeHtml(task.milestone)}</dd></div>` : ''}
|
|
${dependencies ? `<div><dt>Depends on</dt><dd>${escapeHtml(dependencies)}</dd></div>` : ''}
|
|
${task.observedAt ? `<div><dt>Observed at</dt><dd><code>${escapeHtml(shortCommit(task.observedAt))}</code></dd></div>` : ''}
|
|
</dl>
|
|
${acceptance ? `<details><summary>Acceptance criteria</summary><ul>${acceptance}</ul></details>` : ''}
|
|
<details class="priority-change">
|
|
<summary>Owner: change priority</summary>
|
|
<form class="priority-change-form" data-task-id="${escapeHtml(task.id)}" data-revision="${Number(task.revision) || 0}">
|
|
<label>New priority<select name="priority">${['urgent', 'high', 'normal', 'low'].map((value) =>
|
|
`<option value="${value}"${(task.priority ?? 'normal') === value ? ' selected' : ''}>${value}</option>`).join('')}</select></label>
|
|
<label>Why change it?<input name="reason" maxlength="240" required placeholder="Project-level reason"></label>
|
|
<button type="submit">Review change</button>
|
|
<p class="priority-change-status" role="status" aria-live="polite"></p>
|
|
<div class="priority-change-preview" data-priority-preview hidden></div>
|
|
</form>
|
|
</details>
|
|
</article>`;
|
|
}
|
|
|
|
function organizationProposalCard({ type, proposal, title, state, id }) {
|
|
const pending = state === 'proposed';
|
|
const pilotReady = type === 'hierarchy' && state === 'pilot-approved';
|
|
const concluding = type === 'hierarchy' && state === 'piloting';
|
|
const formType = concluding ? 'hierarchy-conclusion' : type;
|
|
const details = type === 'fleet'
|
|
? {
|
|
changeType: proposal.changeType,
|
|
proposer: proposal.proposer,
|
|
targetDomain: proposal.domainId,
|
|
rationale: proposal.rationale,
|
|
expectedBenefit: proposal.expectedBenefit,
|
|
baseCommit: proposal.baseCommit,
|
|
evidence: proposal.evidence,
|
|
}
|
|
: {
|
|
proposer: proposal.proposer,
|
|
proposalRevision: proposal.revision,
|
|
rationale: proposal.proposal?.rationale,
|
|
observationWindow: proposal.proposal?.observation_window,
|
|
signals: proposal.proposal?.signals,
|
|
alternatives: proposal.proposal?.alternatives,
|
|
impact: proposal.proposal?.impact,
|
|
pilot: proposal.proposal?.pilot,
|
|
proposedOrganization: proposal.proposal?.graph,
|
|
baseCommit: proposal.baseCommit,
|
|
};
|
|
const actions = concluding ? [['adopt', 'Adopt the pilot organization'], ['reverse', 'Restore prior relationships']] : type === 'fleet'
|
|
? [['approve', 'Approve for activation review'], ['reject', 'Reject']]
|
|
: [['approve-pilot', 'Approve bounded pilot plan'], ['defer', 'Defer'], ['reject', 'Reject']];
|
|
const review = pending || concluding ? `
|
|
<details class="organization-review">
|
|
<summary>${concluding ? 'Review pilot conclusion' : 'Review owner decision'}</summary>
|
|
<form class="organization-review-form" data-organization-review data-type="${formType}" data-id="${escapeHtml(id)}">
|
|
<label>Decision<select name="action" required>
|
|
<option value="">Choose a decision</option>
|
|
${actions.map(([value, label]) => `<option value="${value}">${label}</option>`).join('')}
|
|
</select></label>
|
|
<label>Reason <span>${concluding ? '(required)' : '(required for defer or reject)'}</span><textarea name="reason" maxlength="2000" rows="2" placeholder="${concluding ? 'Explain why the pilot should be adopted or reversed.' : 'Optional for approval; required when deferring or rejecting.'}"></textarea></label>
|
|
<div class="organization-review-actions">
|
|
<button type="button" data-review-preview>Preview decision</button>
|
|
<p class="organization-review-status" role="status" aria-live="polite"></p>
|
|
</div>
|
|
<section class="organization-review-preview" data-review-preview-panel aria-live="polite" hidden>
|
|
<h4 data-review-title></h4>
|
|
<p data-review-effect></p>
|
|
<details><summary>Exact proposal and evidence in this decision</summary><pre data-review-scope></pre></details>
|
|
<div class="organization-review-actions">
|
|
<button type="button" data-review-confirm hidden>Confirm decision</button>
|
|
<button type="button" class="quiet-action" data-review-edit hidden>Edit decision</button>
|
|
</div>
|
|
</section>
|
|
</form>
|
|
</details>` : '';
|
|
const activation = pilotReady ? `
|
|
<details class="hierarchy-activation">
|
|
<summary>Review and activate bounded pilot</summary>
|
|
<div class="organization-review-actions">
|
|
<button type="button" data-hierarchy-activation-preview data-id="${escapeHtml(id)}">Preview activation</button>
|
|
<p class="organization-review-status" data-hierarchy-activation-status role="status" aria-live="polite"></p>
|
|
</div>
|
|
<section class="organization-review-preview hierarchy-activation-preview" data-hierarchy-activation-panel aria-live="polite" hidden>
|
|
<h4 data-hierarchy-activation-title></h4>
|
|
<p data-hierarchy-activation-effect></p>
|
|
<details><summary>Exact graph, work, schedule, and blocker impact</summary><pre data-hierarchy-activation-scope></pre></details>
|
|
<div class="organization-review-actions">
|
|
<button type="button" class="commit-activation" data-hierarchy-activation-confirm hidden>Commit pilot activation</button>
|
|
</div>
|
|
</section>
|
|
</details>` : '';
|
|
const pilotReviews = type === 'hierarchy' ? `
|
|
<details class="pilot-review-history">
|
|
<summary>Inspect pilot comparisons (${(proposal.pilotReviews ?? []).length})</summary>
|
|
${(proposal.pilotReviews ?? []).length ? proposal.pilotReviews.map((report) => `
|
|
<article class="pilot-review-record" data-pilot-review="${escapeHtml(report.id)}">
|
|
<h4>Recommendation: ${escapeHtml(report.recommendation)}</h4>
|
|
<p>${escapeHtml(report.summary)}</p>
|
|
<p class="pilot-review-provenance">${escapeHtml(report.reviewedAt)} · ${report.window?.complete ? 'Full review window' : 'Interim review'} · Evidence is reviewer-reported, not independently verified.</p>
|
|
<div class="pilot-comparison-scroll"><table class="pilot-comparison-table">
|
|
<caption>Baseline compared with pilot observations</caption>
|
|
<thead><tr><th scope="col">Metric</th><th scope="col">Baseline</th><th scope="col">Observed</th><th scope="col">Evidence quality</th></tr></thead>
|
|
<tbody>${(report.comparisons ?? []).map((metric) => `<tr>
|
|
<th scope="row">${escapeHtml(metric.name)}<small>${escapeHtml(metric.unit)}</small></th>
|
|
<td>${escapeHtml(metric.baseline)}</td><td>${escapeHtml(metric.observed)}</td>
|
|
<td>${escapeHtml(metric.classification)}</td></tr>`).join('')}</tbody>
|
|
</table></div>
|
|
<details><summary>Criterion outcomes and evidence references</summary>
|
|
<ul>${['success', 'stop'].flatMap((group) => (report[group] ?? []).map((criterion) => `
|
|
<li>${group === 'success' ? 'Success' : 'Stop'}: ${escapeHtml(criterion.criterion)} — ${escapeHtml(criterion.outcome)}
|
|
<small>${(criterion.evidence ?? []).map(escapeHtml).join(', ')}</small></li>`)).join('')}</ul>
|
|
<ul>${(report.comparisons ?? []).map((metric) => `<li>${escapeHtml(metric.name)}:
|
|
${(metric.evidence ?? []).map(escapeHtml).join(', ')}</li>`).join('')}</ul>
|
|
<small>Review ${escapeHtml(report.id)} · Activation ${escapeHtml(shortCommit(report.activationCommit))}</small>
|
|
</details>
|
|
<p>No organization change is made by this recommendation. Adoption or reversal requires a separate owner-confirmed conclusion plan.</p>
|
|
</article>`).join('') : empty('No pilot comparisons have been recorded.')}
|
|
</details>` : '';
|
|
return `<article class="organization-proposal ${pending ? 'is-pending' : ''} ${pilotReady ? 'is-activation-ready' : ''}">
|
|
<div class="organization-proposal-heading"><div><span>${type === 'fleet' ? 'Fleet change' : 'Hierarchy proposal'}</span><h3>${escapeHtml(title ?? id)}</h3><code>${escapeHtml(id)}</code></div><strong>${escapeHtml(state)}</strong></div>
|
|
<p>${escapeHtml(type === 'fleet' ? (proposal.expectedBenefit ?? proposal.rationale ?? 'No summary provided.') : (proposal.proposal?.rationale ?? 'No rationale provided.'))}</p>
|
|
<details class="organization-proposal-details"><summary>Inspect proposal scope and evidence</summary><pre>${escapeHtml(JSON.stringify(details, null, 2))}</pre></details>
|
|
${review}
|
|
${activation}
|
|
${pilotReviews}
|
|
</article>`;
|
|
}
|
|
|
|
function viewFiltersFromControls() {
|
|
return workViews?.normalizeFilters({
|
|
query: $('#work-view-search')?.value,
|
|
owner: $('#work-view-owner')?.value,
|
|
state: $('#work-view-state')?.value,
|
|
priority: $('#work-view-priority')?.value,
|
|
domain: $('#work-view-domain')?.value,
|
|
feature: $('#work-view-feature')?.value,
|
|
milestone: $('#work-view-milestone')?.value,
|
|
}) ?? emptyWorkFilters;
|
|
}
|
|
|
|
function setViewFilterOptions(selector, emptyLabel, values, selectedValue) {
|
|
const select = $(selector);
|
|
if (!select) return;
|
|
const options = [...new Set(values.filter((value) => typeof value === 'string' && value))].sort((a, b) => a.localeCompare(b));
|
|
if (selectedValue && !options.includes(selectedValue)) options.push(selectedValue);
|
|
select.innerHTML = `<option value="">${escapeHtml(emptyLabel)}</option>${options.map((value) => {
|
|
const label = value.includes('_') ? value.replaceAll('_', ' ') : value;
|
|
return `<option value="${escapeHtml(value)}">${escapeHtml(label)}</option>`;
|
|
}).join('')}`;
|
|
select.value = selectedValue ?? '';
|
|
}
|
|
|
|
function selectedSavedView() {
|
|
return workViewState.views.find((view) => view.id === workViewState.selectedId) ?? null;
|
|
}
|
|
|
|
function renderSavedViewPicker() {
|
|
const select = $('#work-view-select');
|
|
if (!select) return;
|
|
select.innerHTML = `<option value="">All active work</option>${workViewState.views.map((view) =>
|
|
`<option value="${escapeHtml(view.id)}">${escapeHtml(view.name)}</option>`).join('')}`;
|
|
select.value = workViewState.selectedId;
|
|
const selected = selectedSavedView();
|
|
const update = $('#work-view-update');
|
|
const remove = $('#work-view-delete');
|
|
const name = $('#work-view-name');
|
|
if (update) update.hidden = !selected;
|
|
if (remove) remove.hidden = !selected;
|
|
if (name && selected && !name.dataset.edited) name.value = selected.name;
|
|
if (name && !selected && !name.dataset.edited) name.value = '';
|
|
if (workViewState.warning) setText('#work-view-status', workViewState.warning);
|
|
}
|
|
|
|
function initializeSavedViews(snapshot) {
|
|
const identity = snapshot.project?.id ?? snapshot.project?.root
|
|
?? snapshot.repository?.root ?? snapshot.project?.name ?? snapshot.repository?.name;
|
|
let key;
|
|
try { key = workViews?.storageKey(identity) ?? null; } catch { key = null; }
|
|
if (workViewState.initialized && key === workViewState.key) return;
|
|
workViewState.initialized = true;
|
|
workViewState.key = key;
|
|
workViewState.selectedId = '';
|
|
workViewState.storage = null;
|
|
workViewState.views = [];
|
|
workViewState.warning = null;
|
|
try { workViewState.storage = demoWorkspace?.storage ?? globalThis.localStorage; } catch {
|
|
workViewState.warning = 'Saved views are unavailable in this browser; the backlog remains unchanged.';
|
|
}
|
|
if (workViewState.storage && key && workViews) {
|
|
const result = workViews.readSavedViews(workViewState.storage, key);
|
|
workViewState.views = result.views;
|
|
workViewState.warning = result.warning;
|
|
}
|
|
const name = $('#work-view-name');
|
|
if (name) delete name.dataset.edited;
|
|
renderSavedViewPicker();
|
|
}
|
|
|
|
function renderWorkViewFilters(tasks = [], filters = viewFiltersFromControls()) {
|
|
setViewFilterOptions('#work-view-owner', 'Anyone', tasks.map((task) => task.owner ?? 'Unassigned'), filters.owner);
|
|
setViewFilterOptions('#work-view-state', 'Any state', tasks.map((task) => task.state), filters.state);
|
|
setViewFilterOptions('#work-view-priority', 'Any priority', tasks.map((task) => task.priority ?? 'normal'), filters.priority);
|
|
setViewFilterOptions('#work-view-domain', 'Any domain', tasks.flatMap((task) => task.affectedDomains ?? []), filters.domain);
|
|
setViewFilterOptions('#work-view-feature', 'Any feature', tasks.map((task) => task.feature).filter(Boolean), filters.feature);
|
|
setViewFilterOptions('#work-view-milestone', 'Any milestone', tasks.map((task) => task.milestone).filter(Boolean), filters.milestone);
|
|
}
|
|
|
|
function renderBacklog(tasks = [], healthFindings = []) {
|
|
currentBacklog = tasks;
|
|
currentBacklogHealth = healthFindings;
|
|
const active = tasks.filter((task) => !['completed', 'cancelled'].includes(task.state));
|
|
renderWorkViewFilters(active);
|
|
const filters = viewFiltersFromControls();
|
|
const visible = workViews?.filterTasks(active, filters) ?? active;
|
|
const selected = selectedSavedView();
|
|
setText('#task-count', `${visible.length} of ${active.length} active`);
|
|
setText('#work-view-summary', `Showing ${visible.length} of ${active.length} active tasks${selected ? ` · ${selected.name}` : ''}.`);
|
|
const lanes = [
|
|
{ title: 'Queue', states: ['proposed', 'ready'] },
|
|
{ title: 'Assigned', states: ['assigned'] },
|
|
{ title: 'In progress', states: ['in_progress'] },
|
|
{ title: 'Blocked', states: ['blocked'] },
|
|
{ title: 'Verification', states: ['verification', 'ready_to_integrate'] },
|
|
];
|
|
const known = new Set(lanes.flatMap((lane) => lane.states));
|
|
const unknownStates = [...new Set(visible.map((task) => task.state).filter((state) => !known.has(state)))];
|
|
if (unknownStates.length) lanes.push({ title: 'Other', states: unknownStates });
|
|
setHtml('#backlog-board', lanes.map((lane) => {
|
|
const items = visible.filter((task) => lane.states.includes(task.state));
|
|
return `<section class="board-lane" aria-label="${escapeHtml(lane.title)}">
|
|
<header><h3>${escapeHtml(lane.title)}</h3><span>${items.length}</span></header>
|
|
<div class="lane-items">${items.length ? items.map((task) => taskCard(task, healthFindings)).join('') : empty(visible.length ? `No ${lane.title.toLowerCase()} work.` : 'No active tasks match these filters.')}</div>
|
|
</section>`;
|
|
}).join(''));
|
|
}
|
|
|
|
function renderTaskCreateOptions(agents, tasks, installed) {
|
|
const form = $('#task-create-form');
|
|
if (renderingSnapshot && refreshProtection.protects(form)) { heldPanels += 1; return; }
|
|
const fields = form?.querySelector('[data-task-create-fields]');
|
|
const domainSelect = form?.querySelector('[name="affectedDomains"]');
|
|
const dependencySelect = form?.querySelector('[name="dependencies"]');
|
|
if (!form || !fields || !domainSelect || !dependencySelect) return;
|
|
const selectedDomains = new Set([...domainSelect.selectedOptions].map((option) => option.value));
|
|
const selectedDependencies = new Set([...dependencySelect.selectedOptions].map((option) => option.value));
|
|
const domains = agents.filter((agent) => agent.areaId && agent.areaId !== 'session-manager')
|
|
.sort((left, right) => left.areaId.localeCompare(right.areaId));
|
|
domainSelect.innerHTML = domains.length ? domains.map((agent) =>
|
|
`<option value="${escapeHtml(agent.areaId)}"${selectedDomains.has(agent.areaId) ? ' selected' : ''}>${escapeHtml(agent.title ?? agent.areaId)} · ${escapeHtml(agent.areaId)}</option>`).join('')
|
|
: '<option value="" disabled>No installed domains available</option>';
|
|
const active = tasks.filter((task) => !['completed', 'cancelled'].includes(task.state));
|
|
dependencySelect.innerHTML = active.length ? active.map((task) =>
|
|
`<option value="${escapeHtml(task.id)}"${selectedDependencies.has(task.id) ? ' selected' : ''}>${escapeHtml(task.id)} · ${escapeHtml(task.title)}</option>`).join('')
|
|
: '<option value="" disabled>No existing tasks</option>';
|
|
fields.disabled = !installed;
|
|
if (!installed) {
|
|
taskCreateStatus(form, 'Task proposals are available after this repository is installed as a Fleet. No project state was changed.');
|
|
}
|
|
}
|
|
|
|
function renderOwnerRequestOptions(agents, tasks, installed) {
|
|
const form = $('#owner-request-form');
|
|
if (renderingSnapshot && refreshProtection.protects(form)) { heldPanels += 1; return; }
|
|
const recipientSelect = $('#owner-request-recipient');
|
|
const taskSelect = $('#owner-request-task');
|
|
const body = $('#owner-request-body');
|
|
const preview = form?.querySelector('[data-owner-request-preview]');
|
|
if (!form || !recipientSelect || !taskSelect || !body || !preview) return;
|
|
const selectedRecipient = recipientSelect.value;
|
|
const selectedTask = taskSelect.value;
|
|
const recipients = [...agents].filter((agent) => agent.areaId)
|
|
.sort((left, right) => (left.areaId === 'session-manager' ? -1 : right.areaId === 'session-manager' ? 1 : left.areaId.localeCompare(right.areaId)));
|
|
recipientSelect.innerHTML = recipients.length
|
|
? `<option value="">Choose an identity</option>${recipients.map((agent) => `<option value="${escapeHtml(agent.areaId)}"${selectedRecipient === agent.areaId ? ' selected' : ''}>${escapeHtml(agent.title ?? agent.areaId)} · ${escapeHtml(agent.areaId)}</option>`).join('')}`
|
|
: '<option value="">No installed identities</option>';
|
|
const activeTasks = tasks.filter((task) => !['completed', 'cancelled'].includes(task.state));
|
|
taskSelect.innerHTML = `<option value="">No task reference</option>${activeTasks.map((task) => `<option value="${escapeHtml(task.id)}"${selectedTask === task.id ? ' selected' : ''}>${escapeHtml(task.id)} · ${escapeHtml(task.title)}</option>`).join('')}`;
|
|
const enabled = installed && recipients.length > 0;
|
|
recipientSelect.disabled = !enabled;
|
|
taskSelect.disabled = !enabled;
|
|
body.disabled = !enabled;
|
|
preview.disabled = !enabled;
|
|
const status = form.querySelector('.owner-request-status');
|
|
if (!enabled && status) {
|
|
status.textContent = installed ? 'No persistent identities are available in this Fleet.' : 'Install a reviewed Fleet before sending owner requests.';
|
|
} else if (status && !status.textContent) {
|
|
status.textContent = 'A request waits in the selected inbox; it does not start the agent.';
|
|
}
|
|
}
|
|
|
|
function profileDescription(profile) {
|
|
return [profile?.runtime, profile?.model, profile?.reasoning].filter(Boolean).join(' / ') || 'Adapter defaults';
|
|
}
|
|
|
|
function runtimeProfileStatus(form, message, error = false) {
|
|
const status = form.querySelector('.runtime-profile-status');
|
|
if (!status) return;
|
|
status.textContent = message;
|
|
status.classList.toggle('is-error', error);
|
|
}
|
|
|
|
function runtimeProfileInputs(form, agent, runtime) {
|
|
const adapter = runtimeProfileAdapters.find((entry) => entry.name === runtime);
|
|
const selectedRuntime = runtime || agent?.runtime || '';
|
|
form.querySelector('[name="runtime"]').value = selectedRuntime;
|
|
form.querySelector('[name="model"]').value = runtime === agent?.runtime
|
|
? (agent?.model ?? adapter?.model ?? '') : (adapter?.model ?? '');
|
|
form.querySelector('[name="reasoning"]').value = runtime === agent?.runtime
|
|
? (agent?.reasoning ?? adapter?.reasoning ?? '') : (adapter?.reasoning ?? '');
|
|
form.dataset.profileArea = agent?.areaId ?? '';
|
|
form.dataset.profileRuntime = selectedRuntime;
|
|
}
|
|
|
|
function renderRuntimeProfileEditor(agents, adapters, installed) {
|
|
const form = $('#runtime-profile-form');
|
|
if (renderingSnapshot && refreshProtection.protects(form)) { heldPanels += 1; return; }
|
|
const areaSelect = form?.querySelector('[name="areaId"]');
|
|
const runtimeSelect = form?.querySelector('[name="runtime"]');
|
|
const previewButton = form?.querySelector('[data-runtime-profile-preview]');
|
|
if (!form || !areaSelect || !runtimeSelect || !previewButton) return;
|
|
if (form.dataset.previewToken) return;
|
|
form.dataset.installed = String(installed);
|
|
runtimeProfileAgents = [...agents].filter((agent) => agent.areaId)
|
|
.sort((left, right) => (left.areaId === 'session-manager' ? -1 : right.areaId === 'session-manager' ? 1 : left.areaId.localeCompare(right.areaId)));
|
|
runtimeProfileAdapters = [...adapters];
|
|
const priorArea = areaSelect.value;
|
|
areaSelect.innerHTML = runtimeProfileAgents.length
|
|
? runtimeProfileAgents.map((agent) => `<option value="${escapeHtml(agent.areaId)}">${escapeHtml(agent.title ?? agent.areaId)} · ${escapeHtml(agent.areaId)}</option>`).join('')
|
|
: '<option value="">No installed identities</option>';
|
|
const selectedAgent = runtimeProfileAgents.find((agent) => agent.areaId === priorArea) ?? runtimeProfileAgents[0];
|
|
areaSelect.value = selectedAgent?.areaId ?? '';
|
|
const currentRuntime = selectedAgent?.runtime ?? '';
|
|
const runtimeIsListed = runtimeProfileAdapters.some((adapter) => adapter.name === currentRuntime);
|
|
const options = runtimeProfileAdapters.map((adapter) => `<option value="${escapeHtml(adapter.name)}"${adapter.available ? '' : ' disabled'}>${escapeHtml(adapter.title ?? adapter.name)}${adapter.available ? '' : ` · unavailable (${escapeHtml(adapter.status ?? 'not trusted')})`}</option>`);
|
|
if (currentRuntime && !runtimeIsListed) options.unshift(`<option value="${escapeHtml(currentRuntime)}" disabled>Current adapter ${escapeHtml(currentRuntime)} · unavailable</option>`);
|
|
runtimeSelect.innerHTML = options.length ? options.join('') : '<option value="">No adapters available</option>';
|
|
const selectedRuntime = currentRuntime;
|
|
runtimeSelect.value = selectedRuntime;
|
|
const enabled = Boolean(installed && selectedAgent && runtimeProfileAdapters.some((adapter) => adapter.available));
|
|
for (const field of form.querySelectorAll('select, input')) field.disabled = !enabled;
|
|
previewButton.disabled = !enabled;
|
|
runtimeProfileInputs(form, selectedAgent, selectedRuntime);
|
|
runtimeProfileStatus(form, enabled
|
|
? 'Changes apply to future launch plans. Review the exact profile before saving.'
|
|
: (installed ? 'No selectable runtime adapter is available for this Fleet.' : 'Install a reviewed Fleet before changing profiles.'));
|
|
}
|
|
|
|
function clearRuntimeProfilePreview(form) {
|
|
delete form.dataset.previewToken;
|
|
delete form.dataset.planHash;
|
|
delete form.dataset.profilePayload;
|
|
for (const field of form.querySelectorAll('select, input')) field.disabled = false;
|
|
form.querySelector('[data-runtime-profile-preview]').hidden = false;
|
|
form.querySelector('[data-runtime-profile-confirm]').hidden = true;
|
|
form.querySelector('[data-runtime-profile-edit]').hidden = true;
|
|
form.querySelector('[data-runtime-profile-review]').hidden = true;
|
|
const enabled = form.dataset.installed === 'true' && runtimeProfileAgents.length > 0;
|
|
for (const field of form.querySelectorAll('select, input')) field.disabled = !enabled;
|
|
form.querySelector('[data-runtime-profile-preview]').disabled = !enabled;
|
|
}
|
|
|
|
function initiativeColumn(title, groups) {
|
|
return `<section class="initiative-column" aria-label="${escapeHtml(title)}">
|
|
<header><h3>${escapeHtml(title)}</h3><span>${groups.length} ${groups.length === 1 ? 'group' : 'groups'}</span></header>
|
|
${groups.length ? `<div class="initiative-list">${groups.map((group) => {
|
|
const counts = group.counts;
|
|
const countCopy = `${counts.completed}/${group.active} active complete · ${counts.blocked} blocked · ${counts.review} in review`;
|
|
return `<details class="initiative-row">
|
|
<summary><strong>${escapeHtml(group.name)}</strong><span class="initiative-state${group.status === 'At risk' ? ' is-risk' : ''}">${escapeHtml(group.status)}</span><progress max="100" value="${group.progress}" aria-label="${escapeHtml(group.name)} completion"></progress><span class="initiative-count">${escapeHtml(countCopy)}</span></summary>
|
|
<div class="initiative-details"><p>${group.tasks.length} linked ${group.tasks.length === 1 ? 'task' : 'tasks'} · canceled tasks are excluded from the completion denominator.</p><ul>${group.tasks.map((task) => `<li><span><code>${escapeHtml(task.id)}</code> ${escapeHtml(task.title)}</span><span>${escapeHtml(task.state)}</span><span>${escapeHtml(task.owner ?? 'Unassigned')}</span></li>`).join('')}</ul></div>
|
|
</details>`;
|
|
}).join('')}</div>` : empty(`No backlog work is linked to a ${title.toLowerCase().replace(/s$/, '')} yet.`)}
|
|
</section>`;
|
|
}
|
|
|
|
function renderInitiativeProgress(tasks = []) {
|
|
const rollup = workProgress?.summarizeInitiatives(tasks) ?? { features: [], milestones: [] };
|
|
const hasGroups = rollup.features.length + rollup.milestones.length > 0;
|
|
const explanation = hasGroups ? ''
|
|
: empty('No feature or milestone labels are recorded on backlog items. The Session Manager can classify work; this view will derive progress from those same tasks.');
|
|
setHtml('#initiative-rollup', `${explanation}<div class="initiative-rollup-columns">${initiativeColumn('Features', rollup.features)}${initiativeColumn('Milestones', rollup.milestones)}</div>`);
|
|
}
|
|
|
|
function roleLinks(roleIds, rolesById) {
|
|
if (!roleIds?.length) return 'None recorded';
|
|
return roleIds.map((id) => {
|
|
const target = rolesById.get(id);
|
|
const title = target?.title ?? id;
|
|
return `<a href="#organization-role-${escapeHtml(id)}">${escapeHtml(title)}</a>`;
|
|
}).join('');
|
|
}
|
|
|
|
function lifecycleIdentityTitle(identityId, agentsById) {
|
|
return agentsById.get(identityId)?.title ?? identityId;
|
|
}
|
|
|
|
function lifecycleOrderList(order = [], agentsById, managersByIdentity = {}) {
|
|
if (!order.length) return empty('No session identities are available in this roster.');
|
|
return `<ol class="lifecycle-order">${order.map((identityId, index) => {
|
|
const managerIds = managersByIdentity[identityId] ?? [];
|
|
const managerText = managerIds.length
|
|
? `Reports to ${managerIds.map((managerId) => escapeHtml(lifecycleIdentityTitle(managerId, agentsById))).join(', ')}`
|
|
: 'No fleet manager dependency';
|
|
return `<li><span class="lifecycle-order-index" aria-hidden="true">${index + 1}</span><div><strong>${escapeHtml(lifecycleIdentityTitle(identityId, agentsById))}</strong><small>${managerText}</small></div></li>`;
|
|
}).join('')}</ol>`;
|
|
}
|
|
|
|
function lifecycleBlockerText(blocker, agentsById) {
|
|
const area = lifecycleIdentityTitle(blocker.areaId, agentsById);
|
|
if (blocker.code === 'FLEET_STARTUP_ROLE_MISSING') return `${area} has no approved organization role.`;
|
|
if (blocker.code === 'FLEET_STARTUP_MANAGER_MISSING') {
|
|
return `${area} reports to ${lifecycleIdentityTitle(blocker.managerId, agentsById)}, which is not in the Fleet roster.`;
|
|
}
|
|
if (blocker.code === 'FLEET_STARTUP_IDENTITY_CYCLE') {
|
|
const identities = (blocker.areaIds ?? []).map((id) => lifecycleIdentityTitle(id, agentsById));
|
|
return `Reporting cycle prevents a safe order: ${identities.join(', ')}.`;
|
|
}
|
|
return `Lifecycle order is blocked: ${blocker.code}.`;
|
|
}
|
|
|
|
function renderLifecycleOrder(lifecycle, agentsById) {
|
|
if (!lifecycle) {
|
|
setHtml('#organization-lifecycle', empty('Lifecycle order is available after a Fleet is installed.'));
|
|
return;
|
|
}
|
|
const blockers = lifecycle.blockers ?? [];
|
|
const startup = lifecycleOrderList(lifecycle.startupOrder, agentsById, lifecycle.managerIdsByIdentity);
|
|
const shutdown = lifecycleOrderList(lifecycle.shutdownOrder, agentsById, lifecycle.managerIdsByIdentity);
|
|
const blockerMarkup = blockers.length
|
|
? `<ul class="lifecycle-blockers" aria-label="Lifecycle blockers">${blockers.map((blocker) => `<li>${escapeHtml(lifecycleBlockerText(blocker, agentsById))}</li>`).join('')}</ul>`
|
|
: '<p class="lifecycle-clear">No hierarchy blockers. These are planned orders only; no sessions were started or stopped.</p>';
|
|
setHtml('#organization-lifecycle', `<div class="lifecycle-heading"><div><h3>Session lifecycle order</h3><p>Derived from approved reporting lines. Independent sessions keep their configured roster order.</p></div><span class="lifecycle-readonly">Preview only</span></div>
|
|
<div class="lifecycle-orders">
|
|
<section aria-labelledby="lifecycle-start-title"><h4 id="lifecycle-start-title">Start · managers first</h4>${startup}</section>
|
|
<section aria-labelledby="lifecycle-stop-title"><h4 id="lifecycle-stop-title">Wind down · reports before managers</h4>${shutdown}</section>
|
|
</div>${blockerMarkup}`);
|
|
}
|
|
|
|
function renderOrganization(organization, agents) {
|
|
const graph = organization?.graph;
|
|
if (!graph?.roles?.length) {
|
|
setText('#organization-revision', 'No active graph');
|
|
setHtml('#organization-map', empty(organization?.status === 'not-installed'
|
|
? 'This repository is still in intake. Design and approve a Fleet to establish its organization.'
|
|
: 'No approved organization graph is available in this snapshot.'));
|
|
renderLifecycleOrder(organization?.lifecycle ?? null, new Map(agents.map((agent) => [agent.areaId, agent])));
|
|
return;
|
|
}
|
|
|
|
setText('#organization-revision', `Revision ${graph.revision} · ${graph.roles.length} ${graph.roles.length === 1 ? 'role' : 'roles'}`);
|
|
const rolesById = new Map(graph.roles.map((role) => [role.id, role]));
|
|
const agentsById = new Map(agents.map((agent) => [agent.areaId, agent]));
|
|
renderLifecycleOrder(organization.lifecycle, agentsById);
|
|
const ownersByIdentity = new Map();
|
|
for (const owner of graph.implementation_owners ?? []) {
|
|
if (!ownersByIdentity.has(owner.identity_id)) ownersByIdentity.set(owner.identity_id, []);
|
|
ownersByIdentity.get(owner.identity_id).push(owner.surface);
|
|
}
|
|
setHtml('#organization-map', `<p class="organization-note">Reporting, coordination, and implementation are separate relationships. A role does not create an identity or grant owner-only approval.</p>
|
|
<div class="organization-roles" role="list" aria-label="Approved organization roles">${graph.roles.map((role) => {
|
|
const agent = role.identity_id === 'owner' ? null : agentsById.get(role.identity_id);
|
|
const identityStatus = role.identity_id === 'owner'
|
|
? 'Human owner'
|
|
: (agent ? (agent.state ?? 'offline') : 'Identity missing');
|
|
const responsibilities = (role.responsibilities ?? []).map((item) => `<li>${escapeHtml(item)}</li>`).join('');
|
|
const surfaces = ownersByIdentity.get(role.identity_id) ?? [];
|
|
return `<article id="organization-role-${escapeHtml(role.id)}" class="organization-role kind-${escapeHtml(role.kind)}" role="listitem">
|
|
<header class="organization-role-heading"><div><h3>${escapeHtml(role.title)}</h3><p>${escapeHtml(role.kind)} · <code>${escapeHtml(role.identity_id)}</code></p></div><span class="organization-state">${escapeHtml(identityStatus)}</span></header>
|
|
${responsibilities ? `<ul class="organization-responsibilities">${responsibilities}</ul>` : ''}
|
|
<dl class="organization-relations">
|
|
<div><dt>Reports to</dt><dd>${roleLinks(role.reports_to, rolesById)}</dd></div>
|
|
<div><dt>Coordinates</dt><dd>${roleLinks(role.coordinates, rolesById)}</dd></div>
|
|
<div><dt>Works directly with</dt><dd>${roleLinks(role.consults_with, rolesById)}</dd></div>
|
|
<div><dt>Authority</dt><dd>${escapeHtml((role.authority ?? []).join(', ') || 'None recorded')}</dd></div>
|
|
${surfaces.length ? `<div><dt>Implementation</dt><dd>${surfaces.map(escapeHtml).join(', ')}</dd></div>` : ''}
|
|
</dl>
|
|
</article>`;
|
|
}).join('')}</div>`);
|
|
}
|
|
|
|
function cadenceCopy(trigger) {
|
|
if (trigger?.type === 'interval') {
|
|
const seconds = trigger.seconds;
|
|
if (seconds % 3600 === 0) return `Configured every ${seconds / 3600} ${seconds === 3600 ? 'hour' : 'hours'}`;
|
|
if (seconds % 60 === 0) return `Configured every ${seconds / 60} minutes`;
|
|
return `Configured every ${seconds} seconds`;
|
|
}
|
|
if (trigger?.type === 'cron') return 'Configured cron cadence';
|
|
return 'No check-in cadence configured';
|
|
}
|
|
|
|
function renderScheduleLauncher(snapshot) {
|
|
const details = $('[data-schedule-launcher]');
|
|
if (renderingSnapshot && refreshProtection.protects(details)) { heldPanels += 1; return; }
|
|
const button = details?.querySelector('[data-schedule-launcher-preview]');
|
|
const systemSchedules = (snapshot.schedules ?? []).filter((schedule) => schedule.lifetime === 'system');
|
|
const state = snapshot.scheduleLauncher;
|
|
const status = $('#schedule-launcher-state');
|
|
if (!details || !button || !status) return;
|
|
details.dataset.action = state?.installed ? 'reconcile' : 'install';
|
|
button.textContent = state?.installed
|
|
? (state.stale ? 'Review timer refresh' : 'Timer files match configuration')
|
|
: 'Review timer setup';
|
|
button.disabled = snapshot.mode !== 'installed' || systemSchedules.length === 0
|
|
|| Boolean(state?.installed && !state.stale);
|
|
if (snapshot.mode !== 'installed') status.textContent = 'Install and review a Fleet before configuring a persistent timer.';
|
|
else if (!systemSchedules.length) status.textContent = 'No persistent system schedules are configured.';
|
|
else if (!state?.installed) status.textContent = 'Timer not installed. Preview lists the schedules and unit files that setup would enable.';
|
|
else if (state.stale) status.textContent = `Installed files or their configuration digest need review. Systemd running state is not queried. Current digest: ${state.currentConfigDigest}.`;
|
|
else status.textContent = `TORCH-owned unit files match the current configuration (${state.currentConfigDigest}). Systemd running state is not queried.`;
|
|
const launcher = state ?? {};
|
|
const wakes = snapshot.managerWakes;
|
|
setText('#manager-wake-count', wakes?.available ? `${wakes.blockingCount} awaiting inspection` : 'Not available');
|
|
setHtml('#manager-wake-list', !wakes?.available
|
|
? empty(wakes?.reason ?? 'Manager launch records are unavailable.')
|
|
: wakes.reservations.length ? wakes.reservations.map((record) => `
|
|
<div class="list-row"><div><strong>${escapeHtml(record.managerId)}: ${record.blocksManagerWake ? 'Launch outcome unknown' : escapeHtml(record.outcome)}</strong>
|
|
<small>Schedule ${escapeHtml(record.scheduleId)} · Reserved ${escapeHtml(record.reservedAt)} · Presence ${escapeHtml(record.managerState ?? 'unavailable')}</small>
|
|
<small>Reservation ${escapeHtml(record.id)} · Message ${escapeHtml(record.messageId)}</small>
|
|
${record.blocksManagerWake ? '<p>Inspect the runtime before owner-approved CLI recovery. No automatic retry or budget refund.</p>' : ''}
|
|
</div><span>${record.blocksManagerWake ? 'Wake blocked' : 'Recorded'}</span></div>
|
|
`).join('') : empty('No manager launches have been reserved.'));
|
|
setHtml('#schedule-list', (snapshot.schedules ?? []).length ? snapshot.schedules.map((schedule) => `
|
|
<div class="list-row"><div><strong>${escapeHtml(schedule.id)}</strong><small>${escapeHtml(schedule.owner)} · ${escapeHtml(schedule.lifetime)} · ${escapeHtml(cadenceCopy(schedule.trigger))}</small></div><span>${escapeHtml(schedule.behavior)}</span></div>
|
|
`).join('') : empty('No schedules configured.'));
|
|
button.dataset.launcherReady = String(snapshot.mode === 'installed' && systemSchedules.length > 0
|
|
&& (!launcher.installed || launcher.stale));
|
|
}
|
|
|
|
function clearScheduleLauncherPreview(message = 'Review cleared. Check the current timer plan before confirming.') {
|
|
const details = $('[data-schedule-launcher]');
|
|
if (!details) return;
|
|
delete details.dataset.previewToken;
|
|
delete details.dataset.planHash;
|
|
const button = details.querySelector('[data-schedule-launcher-preview]');
|
|
const panel = details.querySelector('[data-schedule-launcher-review]');
|
|
if (button) button.hidden = false;
|
|
if (panel) panel.hidden = true;
|
|
const confirm = details.querySelector('[data-schedule-launcher-confirm]');
|
|
const edit = details.querySelector('[data-schedule-launcher-edit]');
|
|
if (confirm) { confirm.hidden = true; confirm.disabled = false; }
|
|
if (edit) edit.hidden = true;
|
|
const status = details.querySelector('[data-schedule-launcher-status]');
|
|
if (status) { status.textContent = message; status.classList.remove('is-error'); }
|
|
}
|
|
|
|
function scheduleLauncherStatus(message, error = false) {
|
|
const status = $('[data-schedule-launcher-status]');
|
|
if (!status) return;
|
|
status.textContent = message;
|
|
status.classList.toggle('is-error', error);
|
|
}
|
|
|
|
function showScheduleLauncherPlan(details, result) {
|
|
const { plan } = result;
|
|
const panel = details.querySelector('[data-schedule-launcher-review]');
|
|
details.dataset.previewToken = result.token;
|
|
details.dataset.planHash = result.planHash;
|
|
details.dataset.action = plan.action;
|
|
details.querySelector('[data-schedule-launcher-preview]').hidden = true;
|
|
panel.querySelector('[data-schedule-launcher-title]').textContent = plan.action === 'install'
|
|
? 'Install the persistent TORCH schedule timer'
|
|
: 'Refresh the existing TORCH schedule timer';
|
|
const dailyBudget = plan.runtimeWakeBudget?.max_invocations_per_day;
|
|
panel.querySelector('[data-schedule-launcher-effect]').textContent = plan.effect
|
|
+ (dailyBudget ? ` All managers share a limit of ${dailyBudget} wake invocations per UTC day.` : '');
|
|
setHtml('[data-schedule-launcher-schedules]', plan.systemSchedules.length
|
|
? `<ul class="schedule-launcher-items">${plan.systemSchedules.map((schedule) => `
|
|
<li><strong>${escapeHtml(schedule.title)} · ${escapeHtml(schedule.id)}</strong><small>${escapeHtml(schedule.owner)} · ${escapeHtml(cadenceCopy(schedule.trigger))} · ${escapeHtml(schedule.behavior)}</small>${schedule.action.type === 'manager-check-in' ? `<small>${escapeHtml(!schedule.action.wake?.enabled ? 'Inbox delivery only' : schedule.action.wake.budget_mode === 'invocation-count' ? 'Count-limited wake · No USD spending ceiling' : `Hard spending ceiling: $${schedule.action.wake.max_usd_per_invocation} per invocation`)}</small>` : ''}<small>Action: ${escapeHtml(JSON.stringify(schedule.action))}</small></li>
|
|
`).join('')}</ul>` : empty('No system schedules are configured.'));
|
|
setHtml('[data-schedule-launcher-files]', plan.files.length
|
|
? `<div class="schedule-launcher-files"><strong>Files and current review</strong>${plan.files.map((file) => `<div>${escapeHtml(file.name)} · ${escapeHtml(file.path)} · SHA-256 ${escapeHtml(file.sha256)}</div>`).join('')}<div>Configuration digest ${escapeHtml(plan.digest)}</div>${plan.updates?.length ? `<div>${plan.updates.filter((entry) => entry.changed).length} owned unit file(s) will change.</div>` : ''}</div>`
|
|
: empty('No unit files are available in this plan.'));
|
|
const confirm = panel.querySelector('[data-schedule-launcher-confirm]');
|
|
confirm.textContent = plan.action === 'install' ? 'Install and start timer' : 'Refresh and start timer';
|
|
confirm.disabled = !plan.canProceed;
|
|
confirm.hidden = !plan.canProceed;
|
|
const edit = panel.querySelector('[data-schedule-launcher-edit]');
|
|
edit.hidden = false;
|
|
panel.hidden = false;
|
|
if (plan.blockers?.length) scheduleLauncherStatus(`Cannot proceed: ${plan.blockers.map((blocker) => blocker.code).join(', ')}.`, true);
|
|
else scheduleLauncherStatus('Review each schedule and path. Nothing has changed yet.');
|
|
}
|
|
|
|
function stateClass(state) {
|
|
return ['waiting', 'stale', 'missing', 'offline'].includes(state) ? `is-${state}` : '';
|
|
}
|
|
|
|
function renderFlowWatch(snapshot) {
|
|
const agents = new Map((snapshot.agents ?? []).map((agent) => [agent.areaId, agent]));
|
|
const checkIns = snapshot.managerCheckIns;
|
|
const managers = checkIns?.managers ?? [];
|
|
const attentionCount = managers.filter((manager) => manager.attentionRequired).length;
|
|
setText('#manager-checkin-count', checkIns?.available
|
|
? `${attentionCount} of ${managers.length} need attention` : 'Not available');
|
|
setHtml('#manager-checkin-list', checkIns?.available
|
|
? (managers.length ? managers.map((manager) => {
|
|
const reports = manager.directReports ?? [];
|
|
const reportsWithApprovals = new Map((manager.approvalWaits ?? []).map((approval) => [approval.requester, approval]));
|
|
const findings = manager.findings ?? [];
|
|
return `<article class="manager-watch">
|
|
<header class="manager-watch-heading"><div><strong>${escapeHtml(manager.title ?? manager.managerId)}</strong><small> · ${escapeHtml(manager.managerId)} · ${escapeHtml(manager.state ?? 'unknown')}</small></div><span>${findings.length + reportsWithApprovals.size} ${findings.length + reportsWithApprovals.size === 1 ? 'signal' : 'signals'}</span></header>
|
|
<p class="manager-cadence">${escapeHtml(cadenceCopy(manager.cadence))} · timer installation not verified</p>
|
|
${reports.length ? reports.map((report) => {
|
|
const approval = reportsWithApprovals.get(report.areaId);
|
|
const label = approval ? `waiting on ${approval.approver === 'owner' ? 'owner' : (agents.get(approval.approver)?.title ?? approval.approver)}`
|
|
: report.state === 'waiting' ? 'wait reason not recorded'
|
|
: report.state === 'offline' ? 'offline' : report.state === 'stale' ? 'stale'
|
|
: report.state === 'missing' ? 'identity missing' : report.state;
|
|
const task = report.task ? ` · ${report.task}` : '';
|
|
const cls = approval || report.state === 'waiting' ? 'is-waiting' : stateClass(report.state);
|
|
return `<div class="flow-report"><span>${escapeHtml(report.title)}<small>${escapeHtml(task)}</small></span><span class="flow-state ${cls}">${escapeHtml(label)}</span></div>`;
|
|
}).join('') : empty('No direct reports in the active organization graph.')}
|
|
${findings.some((finding) => finding.type === 'unacknowledged-message')
|
|
? `<p class="manager-cadence">${findings.filter((finding) => finding.type === 'unacknowledged-message').reduce((sum, finding) => sum + finding.count, 0)} direct-report message(s) still need acknowledgement.</p>` : ''}
|
|
</article>`;
|
|
}).join('') : empty('No manager roles with direct reports are configured.'))
|
|
: empty(checkIns?.reason ?? 'Manager check-in observations are not available.'));
|
|
|
|
const approvalState = snapshot.approvalRequests;
|
|
const pending = (approvalState?.items ?? []).filter((approval) => approval.status === 'pending');
|
|
setText('#approval-request-count', approvalState?.available
|
|
? `${approvalState.pendingCount ?? pending.length} open` : 'Not available');
|
|
setHtml('#approval-request-list', approvalState?.available
|
|
? (pending.length ? pending.map((approval) => {
|
|
const requester = agents.get(approval.requester);
|
|
const approver = approval.approver === 'owner' ? 'Project owner'
|
|
: (agents.get(approval.approver)?.title ?? approval.approver);
|
|
const waitClass = approval.approver === 'owner' ? 'wait-owner'
|
|
: approval.approver === 'session-manager' ? 'wait-manager' : '';
|
|
const refs = [approval.task, approval.evidence].filter(Boolean).map((reference) => escapeHtml(reference)).join(' · ');
|
|
return `<article class="approval-item ${waitClass}" id="approval-${escapeHtml(encodeURIComponent(approval.id))}">
|
|
<h4>${escapeHtml(approval.title)}</h4>
|
|
<p>${escapeHtml(requester?.title ?? approval.requester)} is waiting on <strong class="approval-target">${escapeHtml(approver)}</strong>${refs ? ` · ${refs}` : ''}</p>
|
|
<details><summary>Request details · ${escapeHtml(ageLabel(approval.createdAt))}</summary><p>${escapeHtml(approval.summary)}</p></details>
|
|
${approval.approver === 'owner' ? `<form class="approval-decision-form" data-approval-id="${escapeHtml(approval.id)}" data-revision="${Number(approval.revision) || 0}">
|
|
<label>Owner decision<select name="decision" required><option value="">Choose…</option><option value="approved">Approve</option><option value="rejected">Reject</option></select></label>
|
|
<label>Note <span>(optional)</span><textarea name="note" maxlength="2000" rows="2" placeholder="Context the requester should receive"></textarea></label>
|
|
<div class="approval-decision-actions"><button type="button" data-approval-preview>Review decision</button><p class="approval-decision-status" role="status" aria-live="polite"></p></div>
|
|
<section class="approval-decision-preview" data-approval-preview-panel aria-live="polite" hidden>
|
|
<h4 data-approval-preview-title></h4><p data-approval-preview-effect></p>
|
|
<details><summary>Exact request and evidence</summary><pre data-approval-preview-scope></pre></details>
|
|
<div class="approval-decision-actions"><button type="button" data-approval-confirm hidden>Confirm decision</button><button type="button" class="quiet-action" data-approval-edit hidden>Change decision</button></div>
|
|
</section>
|
|
</form>` : `<p class="approval-owner-boundary">Decision belongs to ${escapeHtml(approver)}. The owner Console will not transfer that authority.</p>`}
|
|
</article>`;
|
|
}).join('') : empty('No open approval requests.'))
|
|
: empty(approvalState?.reason ?? 'Structured approval requests are not available.'));
|
|
}
|
|
|
|
function attentionGroups(snapshot) {
|
|
return globalThis.TorchAttentionProjection.groups(snapshot);
|
|
}
|
|
function attentionSummary(groups) {
|
|
return globalThis.TorchAttentionProjection.summary(groups);
|
|
}
|
|
function renderAttentionItem(item) {
|
|
const severity = ({ urgent: 'Urgent', review: 'Review', decision: 'Decision required', info: 'Progress' })[item.tone] ?? 'Unknown';
|
|
return `<article class="attention-item tone-${escapeHtml(item.tone)}" data-attention-record>
|
|
<span class="attention-mark" aria-hidden="true"></span><div class="attention-copy">
|
|
<strong>${escapeHtml(item.title)}</strong><p>${escapeHtml(item.detail)}</p>
|
|
<small>Severity: ${escapeHtml(severity)} · Owner: ${escapeHtml(item.owner)}${item.waitSince ? ` · Waiting ${escapeHtml(ageLabel(item.waitSince))}` : ''}</small>
|
|
${item.evidence.length > 120
|
|
? `<details class="attention-evidence"><summary>Evidence and references</summary><p>${escapeHtml(item.evidence)}</p></details>`
|
|
: `<small class="attention-evidence-short">Evidence: ${escapeHtml(item.evidence)}</small>`}
|
|
${item.observations ? `<details class="attention-cohort-details"><summary>Recheck observations (${item.observations.length})</summary><ul>${item.observations.map((observation) => `<li>
|
|
<code>${escapeHtml(observation.taskId ?? 'Task reference not recorded')}</code> · ${escapeHtml(observation.code)}
|
|
· Owner: ${escapeHtml(observation.owner ?? 'Owner not recorded')}
|
|
· Observed source/revision: ${escapeHtml(observation.observedAt ?? 'Not recorded')}
|
|
· Current observed commit: ${escapeHtml(observation.currentObservedCommit ?? 'Not recorded')}
|
|
· Reproduction: ${escapeHtml(observation.reproductionStatus ?? 'UNKNOWN')}
|
|
· Sources: ${escapeHtml(observation.sources.join(', ') || 'Not recorded')}
|
|
</li>`).join('')}</ul></details>` : ''}
|
|
<a href="${escapeHtml(item.href)}">${escapeHtml(item.action)}</a>
|
|
${item.decisionApprovalId ? `<div class="attention-quick-actions" role="group" aria-label="Owner decision shortcuts for ${escapeHtml(item.title)}">
|
|
<button type="button" data-attention-decision="approved" data-attention-approval="${escapeHtml(item.decisionApprovalId)}">Approve</button>
|
|
<button type="button" class="quiet-action" data-attention-decision="rejected" data-attention-approval="${escapeHtml(item.decisionApprovalId)}">Reject</button>
|
|
<p class="attention-action-status" role="status" aria-live="polite" hidden></p>
|
|
</div>` : ''}
|
|
${item.requestOwner ? `<a href="#owner-request-form" data-attention-recipient="${escapeHtml(item.requestOwner)}">Request review from ${escapeHtml(item.owner)}</a>` : ''}
|
|
</div></article>`;
|
|
}
|
|
function renderAttentionGroup(key, title, description, items, idPrefix) {
|
|
const headingId = `${idPrefix}-${key}-title`;
|
|
return `<section class="attention-group attention-group-${key}" aria-labelledby="${headingId}">
|
|
<div class="attention-group-heading"><h3 id="${headingId}">${title}</h3><span>${items.length}</span></div>
|
|
${items.length ? items.map(renderAttentionItem).join('') : `<p class="attention-empty">${escapeHtml(description)}</p>`}
|
|
</section>`;
|
|
}
|
|
function applyFleetAttentionFilter(query = '') {
|
|
const list = $('#fleet-attention-list');
|
|
if (!list) return;
|
|
const normalized = query.trim().toLocaleLowerCase();
|
|
const records = [...list.querySelectorAll('[data-attention-record]')];
|
|
let visible = 0;
|
|
for (const record of records) {
|
|
const matches = !normalized || record.textContent.toLocaleLowerCase().includes(normalized);
|
|
record.hidden = !matches;
|
|
if (matches) visible += 1;
|
|
}
|
|
for (const group of list.querySelectorAll('.attention-group')) {
|
|
group.hidden = ![...group.querySelectorAll('[data-attention-record]')].some((record) => !record.hidden);
|
|
}
|
|
setText('#fleet-attention-result-count', normalized
|
|
? `${visible} of ${records.length} findings match.`
|
|
: `${records.length} Fleet and advisory findings available.`);
|
|
}
|
|
function renderAttention(groups) {
|
|
const definitions = [
|
|
['owner', 'Waiting on you', 'Only requests that name the project owner as decision-maker.'],
|
|
['fleet', 'Fleet handling', 'A named domain owns the next step; unknown ownership stays explicit.'],
|
|
['advisory', 'Advisory / recheck cohorts', 'Observed evidence grouped for rechecking; these findings do not assign an owner action.'],
|
|
['arbiter', 'Arbiter handling', 'No observed item has structured evidence assigning its next step exclusively to an arbiter.'],
|
|
];
|
|
const summary = attentionSummary(groups);
|
|
const fleetPreview = globalThis.TorchAttentionProjection.fleetPreview(groups);
|
|
const remaining = Math.max(0, summary.fleetTotal - fleetPreview.length);
|
|
const priorQuery = $('#fleet-attention-filter')?.value ?? '';
|
|
setText('#attention-count', `${summary.actionable} actionable`);
|
|
setText('#owner-decision-count', `${summary.ownerDecisions} owner decision${summary.ownerDecisions === 1 ? '' : 's'}`);
|
|
setText('#urgent-hazard-count', `${summary.urgentHazards} urgent hazard${summary.urgentHazards === 1 ? '' : 's'}`);
|
|
setText('#advisory-count', summary.advisoryReferences
|
|
? `${summary.advisoryCohorts} cohort${summary.advisoryCohorts === 1 ? '' : 's'} · ${summary.advisoryReferences} task references`
|
|
: `${summary.advisoryCohorts} advisory cohort${summary.advisoryCohorts === 1 ? '' : 's'}`);
|
|
setText('#fleet-preview-count', `Showing ${fleetPreview.length} of ${summary.fleetTotal} Fleet findings; ${remaining} remaining`);
|
|
setHtml('#attention-list', definitions.map(([key, title, description]) => renderAttentionGroup(
|
|
key, title, description, key === 'fleet' ? fleetPreview : groups[key] ?? [], 'overview-attention',
|
|
)).join(''));
|
|
setHtml('#fleet-attention-list', [
|
|
renderAttentionGroup('fleet', 'Fleet handling', 'No Fleet-owned findings are recorded.', groups.fleet ?? [], 'fleet-queue'),
|
|
renderAttentionGroup('advisory', 'Advisory / recheck cohorts', 'No advisory cohorts are recorded.', groups.advisory ?? [], 'fleet-queue'),
|
|
].join(''));
|
|
const filter = $('#fleet-attention-filter');
|
|
if (filter) filter.value = priorQuery;
|
|
applyFleetAttentionFilter(priorQuery);
|
|
}
|
|
|
|
function render(snapshot) {
|
|
const continuation = snapshot.continuation;
|
|
setText('#continuation-status', continuation?.available
|
|
? `Automatic turns: ${continuation.attempts}/${continuation.maxTurnsPerDay} used today (UTC); ${continuation.remainingTurns} left. ${continuation.dailyOverride?.day === continuation.day ? `Owner-approved extra allowance expires at UTC midnight; normal daily cap ${continuation.configuredMaxTurnsPerDay}. ` : ''}Concurrency policy: up to ${continuation.maxConcurrency ?? 3}. ${continuation.stopReason === 'daily-turn-cap'
|
|
? 'No further automatic starts: the owner-controlled daily cap is reached. Running turns may finish.'
|
|
: continuation.stopReason === 'paused' ? 'Owner policy is paused; no new automatic starts.' : 'Policy permits new eligible turns; this does not verify timer installation.'} ${continuation.held?.length ?? 0} unchanged-work hold(s) need Manager coordination. This is not measured token or cost usage.`
|
|
: 'Automatic continuation policy is unavailable; do not assume the fleet can start more work.');
|
|
const project = snapshot.project ?? snapshot.repository;
|
|
const agents = snapshot.agents ?? [];
|
|
const tasks = snapshot.backlog ?? [];
|
|
initializeSavedViews(snapshot);
|
|
const active = tasks.filter((task) => !['completed', 'cancelled'].includes(task.state));
|
|
const working = active.filter((task) => task.state === 'in_progress').length;
|
|
const blocked = active.filter((task) => task.state === 'blocked').length;
|
|
const attention = attentionGroups(snapshot);
|
|
const branch = snapshot.repository?.branch ?? 'branch not recorded';
|
|
|
|
setText('#project-name', project?.name ?? project?.id ?? 'Not installed');
|
|
setText('#project-branch', branch);
|
|
setText('#project-branch-detail', branch);
|
|
setText('#project-commit', shortCommit(snapshot.repository?.head));
|
|
setText('#fleet-health', snapshot.mode === 'installed'
|
|
? (snapshot.doctor?.healthy ? 'Healthy' : 'Review findings') : 'Not installed');
|
|
setText('#project-dirty', snapshot.repository?.dirty ? 'Uncommitted changes' : 'Clean at snapshot');
|
|
setText('#context-state', snapshot.contextLocality?.measured ? 'Measured' : 'Not measured');
|
|
setText('#recoverability-state', snapshot.mode === 'installed'
|
|
? (snapshot.recoverability?.level ?? 'Unknown') : 'Not installed');
|
|
setText('#agent-count', `${agents.length} ${agents.length === 1 ? 'identity' : 'identities'}`);
|
|
setText('#snapshot-time', `Updated ${ageLabel(snapshot.generatedAt)}`);
|
|
setText('#snapshot-provenance', demoWorkspace ? 'Sample project · Changes stay in this tab' : `Read-only snapshot · ${snapshot.schema ?? 'schema unavailable'}`);
|
|
const parts = [`${agents.length} persistent ${agents.length === 1 ? 'identity' : 'identities'}`,
|
|
`${working} ${working === 1 ? 'task' : 'tasks'} in progress`];
|
|
if (blocked) parts.push(`${blocked} blocked`);
|
|
const attentionCounts = attentionSummary(attention);
|
|
setText('#project-summary', snapshot.mode === 'installed'
|
|
? `${parts.join(' · ')}. ${attentionCounts.ownerDecisions} owner decision${attentionCounts.ownerDecisions === 1 ? '' : 's'} · ${attentionCounts.urgentHazards} urgent hazard${attentionCounts.urgentHazards === 1 ? '' : 's'} · ${attentionCounts.actionable} actionable across the observed queues.`
|
|
: 'This repository has not been installed as a TORCH Fleet. The view is showing repository intake only.');
|
|
renderTaskCreateOptions(agents, tasks, snapshot.mode === 'installed');
|
|
renderOwnerRequestOptions(agents, tasks, snapshot.mode === 'installed');
|
|
renderRuntimeProfileEditor(agents, snapshot.runtimeAdapters ?? [], snapshot.mode === 'installed');
|
|
renderFlowWatch(snapshot);
|
|
setHtml('#owner-briefing-content', globalThis.TorchOwnerDigest.render(snapshot.ownerDigest));
|
|
compactOwnerBriefingProvenance();
|
|
|
|
renderAttention(attention);
|
|
|
|
renderBacklog(tasks, snapshot.backlogHealth?.findings ?? []);
|
|
setHtml('#activity-review-content', globalThis.TorchTaskActivity.render(snapshot.backlogActivity));
|
|
renderInitiativeProgress(tasks);
|
|
setHtml('#agent-list', agents.length ? agents.map((agent) => {
|
|
const profile = [agent.runtime, agent.model, agent.reasoning].filter(Boolean).join(' / ') || 'Profile not recorded';
|
|
const live = agent.state !== 'offline';
|
|
return `<article class="agent-row">
|
|
<span class="presence ${escapeHtml(agent.state)}" aria-label="${live ? 'Active' : 'Offline'}"></span>
|
|
<div class="agent-copy"><strong>${escapeHtml(agent.title ?? agent.areaId)}</strong><span>${escapeHtml(agent.summary ?? profile)}</span><small>${escapeHtml(agent.areaId)} · ${escapeHtml(profile)}</small></div>
|
|
<div class="agent-state"><strong>${escapeHtml(agent.state ?? 'unknown')}</strong><small>${escapeHtml(agent.executor?.state === 'active' ? 'executor verified live' : ageLabel(agent.heartbeatAt))}</small></div>
|
|
</article>`;
|
|
}).join('') : empty('Install an approved Fleet to see persistent identities.'));
|
|
|
|
const domains = snapshot.organization?.domains ?? [];
|
|
setHtml('#ownership-list', domains.length ? domains.map((domain) => `
|
|
<div class="list-row"><div><strong>${escapeHtml(domain.title ?? domain.id)}</strong><small>${escapeHtml((domain.owned_paths ?? []).join(', ') || 'No implementation paths assigned')}</small></div><span>${escapeHtml((domain.neighbours ?? []).join(', ') || 'No peer links')}</span></div>
|
|
`).join('') : empty('No approved domain ownership graph.'));
|
|
renderOrganization(snapshot.organization, agents);
|
|
|
|
const messages = snapshot.messages?.recent ?? [];
|
|
setText('#message-count', `${snapshot.messages?.unacknowledged ?? 0} unacknowledged`);
|
|
setHtml('#message-list', messages.length ? messages.map((message) => {
|
|
const refs = [message.task, message.path, message.commit].filter(Boolean);
|
|
const ack = (message.acknowledgedBy ?? []).length
|
|
? `Acknowledged by ${message.acknowledgedBy.join(', ')}` : 'Awaiting acknowledgement';
|
|
return `<article class="conversation-item" id="message-${escapeHtml(message.id)}"><div class="conversation-meta"><strong>${escapeHtml(message.sender)} to ${escapeHtml(message.recipient)}</strong><span>${escapeHtml(ageLabel(message.createdAt))}</span></div><p>${escapeHtml(message.body)}</p><small>${escapeHtml(ack)}${refs.length ? ` · ${escapeHtml(refs.join(' · '))}` : ''}</small></article>`;
|
|
}).join('') : empty('No durable messages yet. Runtime-native private reasoning is not shown here.'));
|
|
|
|
setHtml('#check-list', globalThis.TorchCheckEvidence.render(snapshot.checks));
|
|
|
|
const artifacts = snapshot.artifacts?.items ?? [];
|
|
setHtml('#artifact-list', artifacts.length ? artifacts.map((artifact) => `
|
|
<article class="artifact-item">
|
|
${localPath(artifact.thumbnailUrl ?? artifact.url) ? `<img src="${localPath(artifact.thumbnailUrl ?? artifact.url)}" alt="${escapeHtml(artifact.alt ?? 'Project review artifact')}">` : '<div class="artifact-unavailable">Image unavailable or failed its integrity check</div>'}
|
|
<div class="artifact-caption">
|
|
<strong>${escapeHtml(artifact.title ?? artifact.name)}</strong>
|
|
<dl class="artifact-provenance">
|
|
<div><dt>Task</dt><dd>${escapeHtml(artifact.taskId ?? 'Not recorded')}</dd></div>
|
|
<div><dt>Identity</dt><dd>${escapeHtml(artifact.identityId ?? 'Not recorded')}</dd></div>
|
|
${artifact.sessionId ? `<div><dt>Runtime session</dt><dd>${escapeHtml(artifact.sessionId)}</dd></div>` : ''}
|
|
<div><dt>Commit</dt><dd><code>${escapeHtml(shortCommit(artifact.commit))}</code></dd></div>
|
|
</dl>
|
|
<small>${escapeHtml(artifact.integrity === 'available' ? 'Catalogued image' : 'Unavailable evidence')} · ${escapeHtml(ageLabel(artifact.createdAt))}</small>
|
|
</div>
|
|
<section class="artifact-feedback" aria-label="Owner feedback">
|
|
<h4>Review notes <span>${(artifact.feedback ?? []).length}</span></h4>
|
|
${(artifact.feedback ?? []).length ? artifact.feedback.map((comment) => `<blockquote><p>${escapeHtml(comment.body)}</p><footer>Owner · ${escapeHtml(ageLabel(comment.createdAt))} · sent to ${escapeHtml(comment.recipient)}</footer></blockquote>`).join('') : '<p class="no-feedback">No owner feedback on this image.</p>'}
|
|
<form class="artifact-feedback-form" data-artifact-id="${escapeHtml(artifact.id)}">
|
|
<label for="feedback-body-${escapeHtml(artifact.id)}">Send a note to <code>${escapeHtml(artifact.identityId ?? 'the responsible identity')}</code></label>
|
|
<textarea id="feedback-body-${escapeHtml(artifact.id)}" name="body" rows="3" maxlength="4000" required placeholder="Describe what should change or what looks right."></textarea>
|
|
<div class="feedback-form-actions"><button type="submit">Review feedback</button><span>Nothing is sent until you confirm the preview.</span></div>
|
|
<p class="feedback-form-status" role="status" aria-live="polite"></p>
|
|
<div class="feedback-preview" data-feedback-preview hidden></div>
|
|
</form>
|
|
</section>
|
|
</article>
|
|
`).join('') : empty(snapshot.artifacts?.available === false
|
|
? (snapshot.artifacts?.reason ?? 'Artifact review is not connected to this snapshot yet. Images need task, session, and commit provenance.')
|
|
: 'No linked screenshots or review artifacts are recorded.'));
|
|
|
|
const integrations = snapshot.integration ?? [];
|
|
setHtml('#integration-list', integrations.length ? integrations.map((item) => `
|
|
<div class="list-row"><div><strong>#${Number(item.queueOrder) || '—'} · ${escapeHtml(item.sourceArea)} · ${escapeHtml(shortCommit(item.sourceCommit))}</strong><small>${item.authorizedBy ? `Authorized by ${escapeHtml(item.authorizedBy)} · ` : ''}${escapeHtml(item.reason ?? 'Awaiting checks or landing review')}</small></div><span>${escapeHtml(item.state)}</span></div>
|
|
`).join('') : empty('No pending integration requests.'));
|
|
const deliveries = snapshot.deliveries ?? [];
|
|
setHtml('#delivery-list', deliveries.length ? deliveries.map((item) => `
|
|
<div class="list-row"><div><strong>${escapeHtml(item.label)}</strong><small>${escapeHtml(item.sourceArea)} · ${escapeHtml(shortCommit(item.commit))}</small></div><span>${escapeHtml(item.state)}</span></div>
|
|
`).join('') : empty('No delivery records. Integration does not imply deployment.'));
|
|
setHtml('#operation-outcomes', globalThis.TorchOperationOutcomes.render(snapshot));
|
|
|
|
const changes = snapshot.fleetChanges ?? [];
|
|
const proposals = snapshot.hierarchyProposals ?? [];
|
|
const allChanges = [
|
|
...changes.map((change) => organizationProposalCard({ type: 'fleet', proposal: change, title: change.title, state: change.state, id: change.id })),
|
|
...proposals.map((proposal) => organizationProposalCard({ type: 'hierarchy', proposal, title: proposal.proposal?.title, state: proposal.state, id: proposal.id })),
|
|
];
|
|
const pending = [...changes, ...proposals].filter((change) => change.state === 'proposed');
|
|
setText('#change-count', `${pending.length} pending`);
|
|
setHtml('#change-list', allChanges.length ? allChanges.join('') : empty('No Fleet or hierarchy proposals. The active organization remains in force.'));
|
|
|
|
const worktrees = snapshot.worktrees ?? [];
|
|
setHtml('#worktree-list', worktrees.length ? worktrees.map((worktree) => `
|
|
<div class="list-row" id="worktree-${escapeHtml(encodeURIComponent(worktree.area))}"><div><strong>${escapeHtml(worktree.area)}</strong><small>${escapeHtml(worktree.branch)} · ${escapeHtml(worktree.path)}</small></div><span>${escapeHtml(`${worktree.ahead ?? 0} ahead / ${worktree.behind ?? 0} behind`)}</span></div>
|
|
`).join('') : empty('No managed worktrees have been created.'));
|
|
|
|
const holders = snapshot.resources?.holders ?? [];
|
|
const waiters = snapshot.resources?.waiters ?? [];
|
|
setHtml('#resource-list', holders.length || waiters.length
|
|
? [...holders.map((item) => `<div class="list-row"><strong>${escapeHtml(item.resourceId)}</strong><span>${escapeHtml(item.areaId)} holds</span></div>`),
|
|
...waiters.map((item) => `<div class="list-row"><strong>${escapeHtml(item.resourceId)}</strong><span>${escapeHtml(item.areaId)} waits</span></div>`)].join('')
|
|
: empty('All configured resources are available.'));
|
|
renderScheduleLauncher(snapshot);
|
|
const providers = snapshot.providers ?? {};
|
|
setHtml('#provider-list', [
|
|
`<div class="list-row"><strong>Forge</strong><span>${escapeHtml(snapshot.mode === 'installed' ? `${providers.forge?.provider ?? 'none'} · ${providers.forge?.status ?? 'unknown'}` : 'not configured')}</span></div>`,
|
|
...(providers.runtimes ?? []).map((runtime) => `<div class="list-row"><strong>${escapeHtml(runtime.name)}</strong><span>${escapeHtml(runtime.configured ? 'configured' : 'unavailable')}</span></div>`),
|
|
`<div class="list-row"><strong>Release</strong><span>${escapeHtml(providers.delivery?.release?.provider ?? 'not configured')}</span></div>`,
|
|
`<div class="list-row"><strong>Deployment</strong><span>${escapeHtml(providers.delivery?.deployment?.provider ?? 'not configured')}</span></div>`,
|
|
].join(''));
|
|
|
|
const decisions = snapshot.decisions?.content ?? '';
|
|
setHtml('#decision-list', decisions.trim()
|
|
? `<p class="decision-copy">${escapeHtml(decisions)}</p>` : empty('No tracked decisions.'));
|
|
const locality = snapshot.contextLocality;
|
|
setHtml('#context-detail', locality?.measured
|
|
? (locality.samples ?? []).map((sample) => `<div class="list-row"><strong>${escapeHtml(sample.areaId)}</strong><span>${escapeHtml(String(sample.cacheRead ?? 0))} cached tokens</span></div>`).join('')
|
|
: `<p>${escapeHtml(locality?.reason ?? 'Runtime usage reporting is not available.')}</p><small>TORCH compares cache activity with verified outcomes, not token count alone.</small>`);
|
|
}
|
|
|
|
let liveUpdatesPaused = false;
|
|
let appliedRefreshes = 0;
|
|
const refreshScheduler = globalThis.TorchLiveRefresh.createScheduler({
|
|
load: async () => {
|
|
if (demoWorkspace) return demoWorkspace.snapshot();
|
|
const response = await fetch('/api/snapshot', { cache: 'no-store', signal: AbortSignal.timeout(10_000) });
|
|
const snapshot = await response.json();
|
|
if (!response.ok) throw new Error(snapshot.message ?? snapshot.error);
|
|
if (snapshot.schema !== 'torch.dev/observation/v1alpha1') throw new Error('Snapshot schema is unavailable or unsupported');
|
|
return snapshot;
|
|
},
|
|
apply: (snapshot) => {
|
|
const viewportBeforeRender = appliedRefreshes > 0
|
|
? { left: globalThis.scrollX, top: globalThis.scrollY } : null;
|
|
const userIntentAtRender = consoleViewRouter.userIntentVersion();
|
|
const editedBeforeRender = refreshProtection.dirty();
|
|
heldPanels = 0;
|
|
renderingSnapshot = true;
|
|
try { render(snapshot); } finally { renderingSnapshot = false; }
|
|
invalidateStalePriorityPreviews(snapshot.backlog ?? []);
|
|
refreshProtection.remember({ preserve: editedBeforeRender });
|
|
$('#console-error').hidden = true;
|
|
const status = $('#live-refresh-status');
|
|
const renderGeneration = ++appliedRefreshes;
|
|
status.textContent = `${liveUpdatesPaused ? 'Automatic updates paused.' : 'Updates every 15 seconds while visible.'}${heldPanels ? ' Edited forms and active previews retained; their panels may show older state.' : ''}`;
|
|
globalThis.requestAnimationFrame(() => globalThis.requestAnimationFrame(() => {
|
|
if (renderGeneration !== appliedRefreshes) return;
|
|
if (viewportBeforeRender && consoleViewRouter.userIntentVersion() === userIntentAtRender) {
|
|
globalThis.scrollTo?.(viewportBeforeRender.left, viewportBeforeRender.top);
|
|
}
|
|
if (renderGeneration === 1) consoleViewRouter.afterInitialLayout();
|
|
status.dataset.generation = String(renderGeneration);
|
|
}));
|
|
},
|
|
onError: (caught) => {
|
|
const error = $('#console-error');
|
|
error.textContent = `Snapshot unavailable: ${caught.message}. Last displayed evidence retained.`;
|
|
error.hidden = false;
|
|
},
|
|
});
|
|
function refresh({ completed = null, background = false } = {}) {
|
|
if (completed) refreshProtection.complete(completed);
|
|
return refreshScheduler.request({ supersede: !background });
|
|
}
|
|
|
|
async function feedbackRequest(path, payload) {
|
|
if (demoWorkspace) return demoWorkspace.request(path, payload);
|
|
const response = await fetch(path, {
|
|
method: 'POST', cache: 'no-store', credentials: 'same-origin',
|
|
headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload),
|
|
});
|
|
const result = await response.json();
|
|
if (!response.ok) throw new Error(result.message ?? result.error ?? `Request failed (${response.status})`);
|
|
return result;
|
|
}
|
|
|
|
async function consoleActionRequest(path, payload) {
|
|
if (demoWorkspace) return demoWorkspace.request(path, payload);
|
|
const response = await fetch(path, {
|
|
method: 'POST', cache: 'no-store', credentials: 'same-origin',
|
|
headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload),
|
|
});
|
|
const result = await response.json();
|
|
if (!response.ok) throw new Error(result.message ?? result.error ?? `Request failed (${response.status})`);
|
|
return result;
|
|
}
|
|
|
|
function approvalDecisionStatus(form, message, error = false) {
|
|
const status = form.querySelector('.approval-decision-status');
|
|
if (!status) return;
|
|
status.textContent = message;
|
|
status.classList.toggle('is-error', error);
|
|
}
|
|
|
|
function clearApprovalDecision(form, message = 'Preview cleared. Review the current request before deciding.') {
|
|
delete form.dataset.previewToken;
|
|
delete form.dataset.planHash;
|
|
form.querySelector('[name="decision"]').disabled = false;
|
|
form.querySelector('[name="note"]').disabled = false;
|
|
form.querySelector('[data-approval-preview]').disabled = false;
|
|
form.querySelector('[data-approval-preview-panel]').hidden = true;
|
|
form.querySelector('[data-approval-confirm]').hidden = true;
|
|
form.querySelector('[data-approval-edit]').hidden = true;
|
|
approvalDecisionStatus(form, message);
|
|
}
|
|
|
|
$('#approval-request-list')?.addEventListener('click', async (event) => {
|
|
const edit = event.target.closest('[data-approval-edit]');
|
|
if (edit) {
|
|
const form = edit.closest('.approval-decision-form');
|
|
if (form) clearApprovalDecision(form);
|
|
return;
|
|
}
|
|
const previewButton = event.target.closest('[data-approval-preview]');
|
|
const confirmButton = event.target.closest('[data-approval-confirm]');
|
|
const form = event.target.closest('.approval-decision-form');
|
|
if (!form) return;
|
|
if (previewButton) {
|
|
const decision = form.querySelector('[name="decision"]').value;
|
|
const note = form.querySelector('[name="note"]').value.trim();
|
|
if (!decision) {
|
|
approvalDecisionStatus(form, 'Choose approve or reject first.', true);
|
|
form.querySelector('[name="decision"]').focus();
|
|
return;
|
|
}
|
|
previewButton.disabled = true;
|
|
approvalDecisionStatus(form, 'Rechecking the named approver and current request revision…');
|
|
try {
|
|
const id = form.dataset.approvalId;
|
|
const result = await consoleActionRequest(`/api/approvals/${encodeURIComponent(id)}/decision/preview`, { decision, note });
|
|
const { plan } = result;
|
|
form.dataset.previewToken = result.token ?? '';
|
|
form.dataset.planHash = result.planHash;
|
|
form.querySelector('[name="decision"]').disabled = true;
|
|
form.querySelector('[name="note"]').disabled = true;
|
|
form.querySelector('[data-approval-preview-panel]').hidden = false;
|
|
form.querySelector('[data-approval-preview-title]').textContent = `Confirm ${plan.decision} · ${plan.title}`;
|
|
form.querySelector('[data-approval-preview-effect]').textContent = plan.effect;
|
|
form.querySelector('[data-approval-preview-scope]').textContent = JSON.stringify({
|
|
approvalId: plan.approvalId, requester: plan.requester, approver: plan.approver,
|
|
revision: plan.revision, task: plan.task, evidence: plan.evidence, note: plan.note,
|
|
}, null, 2);
|
|
form.querySelector('[data-approval-confirm]').textContent = `Confirm ${plan.decision}`;
|
|
form.querySelector('[data-approval-confirm]').hidden = false;
|
|
form.querySelector('[data-approval-edit]').hidden = false;
|
|
approvalDecisionStatus(form, 'Preview is bound to this request revision for five minutes.');
|
|
} catch (error) {
|
|
approvalDecisionStatus(form, error.message, true);
|
|
} finally { previewButton.disabled = false; }
|
|
return;
|
|
}
|
|
if (!confirmButton) return;
|
|
confirmButton.disabled = true;
|
|
approvalDecisionStatus(form, 'Recording the owner decision and notifying the requester…');
|
|
try {
|
|
const id = form.dataset.approvalId;
|
|
const result = await consoleActionRequest(`/api/approvals/${encodeURIComponent(id)}/decision`, {
|
|
decision: form.querySelector('[name="decision"]').value,
|
|
note: form.querySelector('[name="note"]').value.trim(),
|
|
token: form.dataset.previewToken, planHash: form.dataset.planHash,
|
|
});
|
|
approvalDecisionStatus(form, `${result.approval.status} and requester notified.`);
|
|
await refresh({ completed: form });
|
|
} catch (error) {
|
|
approvalDecisionStatus(form, error.message, true);
|
|
confirmButton.disabled = false;
|
|
}
|
|
});
|
|
|
|
function priorityStatus(form, message, error = false) {
|
|
const status = form.querySelector('.priority-change-status');
|
|
if (!status) return;
|
|
status.textContent = message;
|
|
status.classList.toggle('is-error', error);
|
|
}
|
|
|
|
function taskCreateStatus(form, message, error = false) {
|
|
const status = form?.querySelector('.task-create-status');
|
|
if (!status) return;
|
|
status.textContent = message;
|
|
status.classList.toggle('is-error', error);
|
|
}
|
|
|
|
function taskCreatePayload(form) {
|
|
const data = new FormData(form);
|
|
return {
|
|
title: String(data.get('title') ?? '').trim(),
|
|
description: String(data.get('description') ?? '').trim(),
|
|
priority: String(data.get('priority') ?? 'normal'),
|
|
affectedDomains: [...form.querySelector('[name="affectedDomains"]').selectedOptions].map((option) => option.value),
|
|
dependencies: [...form.querySelector('[name="dependencies"]').selectedOptions].map((option) => option.value),
|
|
acceptanceCriteria: String(data.get('acceptanceCriteria') ?? '').split('\n').map((item) => item.trim()).filter(Boolean),
|
|
feature: String(data.get('feature') ?? '').trim(),
|
|
milestone: String(data.get('milestone') ?? '').trim(),
|
|
};
|
|
}
|
|
|
|
function taskCreatePreviewMarkup(plan) {
|
|
const task = plan.task;
|
|
const criteria = task.acceptanceCriteria.map((item) => `<li>${escapeHtml(item)}</li>`).join('');
|
|
return `<div><strong>Review this proposal</strong><p>This is the exact task TORCH will add.</p></div>
|
|
<h4>${escapeHtml(task.title)}</h4><p>${escapeHtml(task.description)}</p>
|
|
<dl><div><dt>Initial state</dt><dd>Proposed · unassigned</dd></div><div><dt>Priority</dt><dd>${escapeHtml(task.priority)}</dd></div>
|
|
<div><dt>Suggested domains</dt><dd>${task.affectedDomains.length ? escapeHtml(task.affectedDomains.join(', ')) : 'Session Manager triage'}</dd></div>
|
|
<div><dt>Dependencies</dt><dd>${task.dependencies.length ? escapeHtml(task.dependencies.join(', ')) : 'None'}</dd></div>
|
|
<div><dt>Feature / milestone</dt><dd>${escapeHtml([task.feature, task.milestone].filter(Boolean).join(' / ') || 'Not labeled')}</dd></div>
|
|
<div><dt>Observed commit</dt><dd><code>${escapeHtml(shortCommit(task.observedAt))}</code></dd></div></dl>
|
|
<div><strong>Acceptance criteria</strong><ul>${criteria}</ul></div>
|
|
<p>${escapeHtml(plan.authority)} ${escapeHtml(plan.effect)}</p>
|
|
<div class="feedback-form-actions"><button type="button" data-task-create-confirm>Create proposed task</button><button type="button" class="quiet-action" data-task-create-edit>Edit proposal</button></div>`;
|
|
}
|
|
|
|
$('#task-create-form')?.addEventListener('submit', async (event) => {
|
|
const form = event.target.closest('#task-create-form');
|
|
if (!form) return;
|
|
event.preventDefault();
|
|
const button = form.querySelector('button[type="submit"]');
|
|
if (!button) return;
|
|
button.disabled = true;
|
|
taskCreateStatus(form, 'Preparing the exact task proposal…');
|
|
try {
|
|
const payload = taskCreatePayload(form);
|
|
const result = await consoleActionRequest('/api/backlog/tasks/preview', payload);
|
|
form.dataset.createPayload = JSON.stringify(payload);
|
|
form.dataset.previewToken = result.token;
|
|
form.dataset.planHash = result.planHash;
|
|
form.querySelector('[data-task-create-fields]').disabled = true;
|
|
const preview = form.querySelector('[data-task-create-preview]');
|
|
preview.innerHTML = taskCreatePreviewMarkup(result.plan);
|
|
preview.hidden = false;
|
|
taskCreateStatus(form, 'Review routing hints and the proposed, unassigned effect before confirming.');
|
|
} catch (error) {
|
|
taskCreateStatus(form, error.message, true);
|
|
} finally { button.disabled = false; }
|
|
});
|
|
|
|
$('#task-create-form')?.addEventListener('click', async (event) => {
|
|
const form = event.currentTarget;
|
|
const edit = event.target.closest('[data-task-create-edit]');
|
|
if (edit) {
|
|
delete form.dataset.createPayload;
|
|
delete form.dataset.previewToken;
|
|
delete form.dataset.planHash;
|
|
form.querySelector('[data-task-create-fields]').disabled = false;
|
|
form.querySelector('[data-task-create-preview]').hidden = true;
|
|
taskCreateStatus(form, 'Preview cleared. Review the edited proposal before creating it.');
|
|
return;
|
|
}
|
|
const confirm = event.target.closest('[data-task-create-confirm]');
|
|
if (!confirm) return;
|
|
const token = form.dataset.previewToken;
|
|
const planHash = form.dataset.planHash;
|
|
if (!token || !planHash || !form.dataset.createPayload) return;
|
|
confirm.disabled = true;
|
|
taskCreateStatus(form, 'Creating the reviewed task proposal…');
|
|
try {
|
|
const payload = JSON.parse(form.dataset.createPayload);
|
|
const result = await consoleActionRequest('/api/backlog/tasks', { ...payload, token, planHash });
|
|
setText('#task-create-action-status', `${result.task.id} added as proposed and unassigned. The Session Manager retains triage; no agent was started.`);
|
|
$('#task-create-action-status').hidden = false;
|
|
form.reset();
|
|
form.querySelector('[data-task-create-fields]').disabled = false;
|
|
form.querySelector('[data-task-create-preview]').hidden = true;
|
|
delete form.dataset.createPayload;
|
|
delete form.dataset.previewToken;
|
|
delete form.dataset.planHash;
|
|
form.closest('details').open = false;
|
|
taskCreateStatus(form, 'Task proposal created.');
|
|
await refresh({ completed: form });
|
|
} catch (error) {
|
|
delete form.dataset.createPayload;
|
|
delete form.dataset.previewToken;
|
|
delete form.dataset.planHash;
|
|
form.querySelector('[data-task-create-fields]').disabled = false;
|
|
form.querySelector('[data-task-create-preview]').hidden = true;
|
|
taskCreateStatus(form, `${error.message} Review the task list before trying again.`, true);
|
|
} finally { confirm.disabled = false; }
|
|
});
|
|
|
|
$('#backlog-board')?.addEventListener('submit', async (event) => {
|
|
const form = event.target.closest('.priority-change-form');
|
|
if (!form) return;
|
|
event.preventDefault();
|
|
const priority = form.querySelector('[name="priority"]')?.value;
|
|
const reason = form.querySelector('[name="reason"]')?.value.trim();
|
|
const expectedRevision = Number(form.dataset.revision);
|
|
const button = form.querySelector('button[type="submit"]');
|
|
if (!priority || !reason || !button) return;
|
|
button.disabled = true;
|
|
priorityStatus(form, 'Preparing the exact priority-change preview…');
|
|
try {
|
|
const result = await consoleActionRequest(`/api/backlog/tasks/${encodeURIComponent(form.dataset.taskId)}/priority/preview`, {
|
|
priority, reason, expectedRevision,
|
|
});
|
|
const plan = result.plan;
|
|
form.dataset.previewToken = result.token;
|
|
form.dataset.planHash = result.planHash;
|
|
form.querySelector('[name="priority"]').disabled = true;
|
|
form.querySelector('[name="reason"]').disabled = true;
|
|
const preview = form.querySelector('[data-priority-preview]');
|
|
preview.innerHTML = `<div class="feedback-preview-heading"><strong>Review before applying</strong><span>One-time preview · 5 minute limit</span></div>
|
|
<dl class="feedback-preview-target"><div><dt>Task</dt><dd><code>${escapeHtml(plan.taskId)}</code> · ${escapeHtml(plan.title)}</dd></div><div><dt>Priority</dt><dd>${escapeHtml(plan.from)} → <strong>${escapeHtml(plan.to)}</strong></dd></div><div><dt>Revision</dt><dd>${plan.revision}</dd></div></dl>
|
|
<blockquote><p>${escapeHtml(plan.reason)}</p></blockquote><p>${escapeHtml(plan.effect)}</p>
|
|
<div class="feedback-form-actions"><button type="button" data-priority-confirm>Apply reviewed priority</button><button type="button" class="quiet-action" data-priority-edit>Edit</button></div>`;
|
|
preview.hidden = false;
|
|
priorityStatus(form, 'Review the task, revision, priority, reason, and effect before applying.');
|
|
} catch (error) { priorityStatus(form, error.message, true); }
|
|
finally { button.disabled = false; }
|
|
});
|
|
|
|
$('#backlog-board')?.addEventListener('click', async (event) => {
|
|
const edit = event.target.closest('[data-priority-edit]');
|
|
if (edit) {
|
|
const form = edit.closest('.priority-change-form');
|
|
if (!form) return;
|
|
delete form.dataset.previewToken;
|
|
delete form.dataset.planHash;
|
|
form.querySelector('[name="priority"]').disabled = false;
|
|
form.querySelector('[name="reason"]').disabled = false;
|
|
form.querySelector('[data-priority-preview]').hidden = true;
|
|
priorityStatus(form, 'Preview cleared. Review the edited priority again before applying.');
|
|
return;
|
|
}
|
|
const confirm = event.target.closest('[data-priority-confirm]');
|
|
if (!confirm) return;
|
|
const form = confirm.closest('.priority-change-form');
|
|
const token = form?.dataset.previewToken;
|
|
const planHash = form?.dataset.planHash;
|
|
if (!form || !token || !planHash) return;
|
|
confirm.disabled = true;
|
|
priorityStatus(form, 'Applying the reviewed priority change…');
|
|
try {
|
|
const result = await consoleActionRequest(`/api/backlog/tasks/${encodeURIComponent(form.dataset.taskId)}/priority`, {
|
|
priority: form.querySelector('[name="priority"]').value,
|
|
reason: form.querySelector('[name="reason"]').value.trim(),
|
|
expectedRevision: Number(form.dataset.revision), token, planHash,
|
|
});
|
|
const actionStatus = $('#priority-action-status');
|
|
if (actionStatus) {
|
|
actionStatus.textContent = `${result.task.id} priority is now ${result.task.priority}; task state and owner were unchanged.`;
|
|
actionStatus.hidden = false;
|
|
}
|
|
await refresh({ completed: form });
|
|
} catch (error) {
|
|
priorityStatus(form, error.message, true);
|
|
confirm.disabled = false;
|
|
}
|
|
});
|
|
|
|
function workViewStatus(message) {
|
|
setText('#work-view-status', message);
|
|
}
|
|
|
|
function applyWorkViewFilters(filters) {
|
|
const normalized = workViews.normalizeFilters(filters);
|
|
const active = currentBacklog.filter((task) => !['completed', 'cancelled'].includes(task.state));
|
|
renderWorkViewFilters(active, normalized);
|
|
const values = {
|
|
'#work-view-search': normalized.query,
|
|
'#work-view-owner': normalized.owner,
|
|
'#work-view-state': normalized.state,
|
|
'#work-view-priority': normalized.priority,
|
|
'#work-view-domain': normalized.domain,
|
|
'#work-view-feature': normalized.feature,
|
|
'#work-view-milestone': normalized.milestone,
|
|
};
|
|
for (const [selector, value] of Object.entries(values)) {
|
|
const control = $(selector);
|
|
if (control) control.value = value;
|
|
}
|
|
renderBacklog(currentBacklog, currentBacklogHealth);
|
|
}
|
|
|
|
function persistCurrentView({ id = null } = {}) {
|
|
if (!workViewState.storage || !workViewState.key || !workViews) {
|
|
workViewStatus('This browser cannot save views. The project backlog was not changed.');
|
|
return;
|
|
}
|
|
const name = $('#work-view-name')?.value ?? '';
|
|
const result = workViews.saveView({
|
|
storage: workViewState.storage, key: workViewState.key, views: workViewState.views,
|
|
id, name, filters: viewFiltersFromControls(),
|
|
});
|
|
if (!result.ok) {
|
|
workViewStatus(result.message);
|
|
return;
|
|
}
|
|
workViewState.views = result.views;
|
|
workViewState.selectedId = result.view.id;
|
|
workViewState.warning = null;
|
|
const nameInput = $('#work-view-name');
|
|
if (nameInput) {
|
|
nameInput.value = result.view.name;
|
|
delete nameInput.dataset.edited;
|
|
}
|
|
renderSavedViewPicker();
|
|
workViewStatus(`Saved “${result.view.name}” in this browser for this project.`);
|
|
renderBacklog(currentBacklog, currentBacklogHealth);
|
|
}
|
|
|
|
function removeSelectedView() {
|
|
if (!workViewState.storage || !workViewState.key || !workViewState.selectedId || !workViews) {
|
|
workViewStatus('Choose a saved view before deleting it.');
|
|
return;
|
|
}
|
|
const selected = selectedSavedView();
|
|
const result = workViews.deleteView({
|
|
storage: workViewState.storage, key: workViewState.key,
|
|
views: workViewState.views, id: workViewState.selectedId,
|
|
});
|
|
if (!result.ok) {
|
|
workViewStatus(result.message);
|
|
return;
|
|
}
|
|
workViewState.views = result.views;
|
|
workViewState.selectedId = '';
|
|
const nameInput = $('#work-view-name');
|
|
if (nameInput) {
|
|
nameInput.value = '';
|
|
delete nameInput.dataset.edited;
|
|
}
|
|
renderSavedViewPicker();
|
|
applyWorkViewFilters(emptyWorkFilters);
|
|
workViewStatus(`Deleted “${selected?.name ?? 'saved view'}”. Project tasks were not changed.`);
|
|
}
|
|
|
|
const workViewTools = $('.work-view-tools');
|
|
workViewTools?.addEventListener('input', (event) => {
|
|
if (event.target.matches('#work-view-name')) {
|
|
event.target.dataset.edited = 'true';
|
|
return;
|
|
}
|
|
if (!event.target.matches('#work-view-search')) return;
|
|
renderBacklog(currentBacklog, currentBacklogHealth);
|
|
workViewStatus('Filter edits are temporary until you save or update a view.');
|
|
});
|
|
workViewTools?.addEventListener('change', (event) => {
|
|
if (event.target.matches('#work-view-select')) {
|
|
const selected = workViewState.views.find((view) => view.id === event.target.value) ?? null;
|
|
workViewState.selectedId = selected?.id ?? '';
|
|
const nameInput = $('#work-view-name');
|
|
if (nameInput) {
|
|
nameInput.value = selected?.name ?? '';
|
|
delete nameInput.dataset.edited;
|
|
}
|
|
renderSavedViewPicker();
|
|
applyWorkViewFilters(selected?.filters ?? emptyWorkFilters);
|
|
workViewStatus(selected ? `Loaded “${selected.name}”. Edit its filters, then update it or save a copy.` : 'Showing all active work.');
|
|
return;
|
|
}
|
|
if (!event.target.matches('#work-view-owner, #work-view-state, #work-view-priority, #work-view-domain, #work-view-feature, #work-view-milestone')) return;
|
|
renderBacklog(currentBacklog, currentBacklogHealth);
|
|
workViewStatus('Filter edits are temporary until you save or update a view.');
|
|
});
|
|
$('#work-view-save')?.addEventListener('click', () => persistCurrentView());
|
|
$('#work-view-update')?.addEventListener('click', () => persistCurrentView({ id: workViewState.selectedId }));
|
|
$('#work-view-delete')?.addEventListener('click', removeSelectedView);
|
|
$('#work-view-clear')?.addEventListener('click', () => {
|
|
workViewState.selectedId = '';
|
|
const nameInput = $('#work-view-name');
|
|
if (nameInput) {
|
|
nameInput.value = '';
|
|
delete nameInput.dataset.edited;
|
|
}
|
|
renderSavedViewPicker();
|
|
applyWorkViewFilters(emptyWorkFilters);
|
|
workViewStatus('Filters cleared. All active work is shown.');
|
|
});
|
|
|
|
function ownerRequestStatus(form, message, error = false) {
|
|
const status = form.querySelector('.owner-request-status');
|
|
if (!status) return;
|
|
status.textContent = message;
|
|
status.classList.toggle('is-error', error);
|
|
}
|
|
|
|
function clearOwnerRequestPreview(form, message = 'Preview cleared. Review the exact request before sending.') {
|
|
delete form.dataset.previewToken;
|
|
delete form.dataset.planHash;
|
|
form.querySelector('[name="recipient"]').disabled = false;
|
|
form.querySelector('[name="taskId"]').disabled = false;
|
|
form.querySelector('[name="body"]').disabled = false;
|
|
form.querySelector('[data-owner-request-preview]').disabled = false;
|
|
form.querySelector('[data-owner-request-preview]').hidden = false;
|
|
form.querySelector('[data-owner-request-review]').hidden = true;
|
|
form.querySelector('[data-owner-request-confirm]').disabled = false;
|
|
form.querySelector('[data-owner-request-confirm]').hidden = true;
|
|
form.querySelector('[data-owner-request-edit]').hidden = true;
|
|
ownerRequestStatus(form, message);
|
|
}
|
|
|
|
$('#owner-request-form')?.addEventListener('click', async (event) => {
|
|
const edit = event.target.closest('[data-owner-request-edit]');
|
|
if (edit) {
|
|
clearOwnerRequestPreview(edit.closest('[data-owner-request]'));
|
|
return;
|
|
}
|
|
|
|
const previewButton = event.target.closest('[data-owner-request-preview]');
|
|
if (previewButton) {
|
|
const form = previewButton.closest('[data-owner-request]');
|
|
if (!form) return;
|
|
const recipient = form.querySelector('[name="recipient"]').value;
|
|
const body = form.querySelector('[name="body"]').value.trim();
|
|
if (!recipient || !body) {
|
|
ownerRequestStatus(form, 'Choose an identity and write the request before previewing.', true);
|
|
return;
|
|
}
|
|
previewButton.disabled = true;
|
|
ownerRequestStatus(form, 'Checking the selected identity, task, and repository…');
|
|
try {
|
|
const taskId = form.querySelector('[name="taskId"]').value;
|
|
const result = await consoleActionRequest('/api/owner-requests/preview', { recipient, body, taskId });
|
|
const plan = result.plan;
|
|
form.dataset.previewToken = result.token;
|
|
form.dataset.planHash = result.planHash;
|
|
form.querySelector('[name="recipient"]').disabled = true;
|
|
form.querySelector('[name="taskId"]').disabled = true;
|
|
form.querySelector('[name="body"]').disabled = true;
|
|
previewButton.hidden = true;
|
|
const panel = form.querySelector('[data-owner-request-review]');
|
|
panel.querySelector('[data-owner-request-title]').textContent = `Request to ${plan.recipientTitle} · ${plan.recipient}`;
|
|
panel.querySelector('[data-owner-request-effect]').textContent = `${plan.effect} Repository: ${shortCommit(plan.currentHead)}${plan.task ? ` · Linked task: ${plan.task.id} (${plan.task.state}, owner ${plan.task.owner ?? 'unassigned'})` : ''}.`;
|
|
panel.querySelector('[data-owner-request-exact]').textContent = plan.body;
|
|
panel.querySelector('[data-owner-request-confirm]').hidden = false;
|
|
panel.querySelector('[data-owner-request-edit]').hidden = false;
|
|
panel.hidden = false;
|
|
ownerRequestStatus(form, 'Preview ready. Check the recipient, task link, and exact request.');
|
|
} catch (error) {
|
|
ownerRequestStatus(form, error.message, true);
|
|
} finally { previewButton.disabled = false; }
|
|
return;
|
|
}
|
|
|
|
const confirm = event.target.closest('[data-owner-request-confirm]');
|
|
if (!confirm) return;
|
|
const form = confirm.closest('[data-owner-request]');
|
|
const token = form?.dataset.previewToken;
|
|
const planHash = form?.dataset.planHash;
|
|
if (!form || !token || !planHash) return;
|
|
confirm.disabled = true;
|
|
ownerRequestStatus(form, 'Sending the reviewed durable request…');
|
|
try {
|
|
const result = await consoleActionRequest('/api/owner-requests', {
|
|
recipient: form.querySelector('[name="recipient"]').value,
|
|
taskId: form.querySelector('[name="taskId"]').value,
|
|
body: form.querySelector('[name="body"]').value.trim(), token, planHash,
|
|
});
|
|
const status = $('#owner-request-action-status');
|
|
if (status) {
|
|
status.textContent = `Sent durable request ${result.message.id} to ${result.message.recipient}. It does not start or wake the agent.`;
|
|
status.hidden = false;
|
|
}
|
|
form.querySelector('[name="body"]').value = '';
|
|
clearOwnerRequestPreview(form, 'Request sent. You can send another request when ready.');
|
|
await refresh({ completed: form });
|
|
} catch (error) {
|
|
ownerRequestStatus(form, error.message, true);
|
|
confirm.disabled = false;
|
|
}
|
|
});
|
|
|
|
const runtimeProfileForm = $('#runtime-profile-form');
|
|
runtimeProfileForm?.querySelector('[name="areaId"]')?.addEventListener('change', () => {
|
|
const agent = runtimeProfileAgents.find((entry) => entry.areaId === runtimeProfileForm.querySelector('[name="areaId"]').value);
|
|
runtimeProfileInputs(runtimeProfileForm, agent, agent?.runtime);
|
|
});
|
|
runtimeProfileForm?.querySelector('[name="runtime"]')?.addEventListener('change', () => {
|
|
const agent = runtimeProfileAgents.find((entry) => entry.areaId === runtimeProfileForm.querySelector('[name="areaId"]').value);
|
|
runtimeProfileInputs(runtimeProfileForm, agent, runtimeProfileForm.querySelector('[name="runtime"]').value);
|
|
});
|
|
runtimeProfileForm?.addEventListener('click', async (event) => {
|
|
const edit = event.target.closest('[data-runtime-profile-edit]');
|
|
if (edit) {
|
|
clearRuntimeProfilePreview(runtimeProfileForm);
|
|
runtimeProfileStatus(runtimeProfileForm, 'Preview cleared. Review the exact profile before saving.');
|
|
return;
|
|
}
|
|
|
|
const previewButton = event.target.closest('[data-runtime-profile-preview]');
|
|
if (previewButton) {
|
|
const payload = {
|
|
areaId: runtimeProfileForm.querySelector('[name="areaId"]').value,
|
|
runtime: runtimeProfileForm.querySelector('[name="runtime"]').value,
|
|
model: runtimeProfileForm.querySelector('[name="model"]').value.trim(),
|
|
reasoning: runtimeProfileForm.querySelector('[name="reasoning"]').value.trim(),
|
|
};
|
|
if (!payload.areaId || !payload.runtime) {
|
|
runtimeProfileStatus(runtimeProfileForm, 'Choose an identity and an available agent.', true);
|
|
return;
|
|
}
|
|
previewButton.disabled = true;
|
|
runtimeProfileStatus(runtimeProfileForm, 'Validating this profile with its selected local adapter…');
|
|
try {
|
|
const result = await consoleActionRequest('/api/runtime-profiles/preview', payload);
|
|
const plan = result.plan;
|
|
runtimeProfileForm.dataset.profilePayload = JSON.stringify(payload);
|
|
runtimeProfileForm.dataset.previewToken = result.token;
|
|
runtimeProfileForm.dataset.planHash = result.planHash;
|
|
for (const field of runtimeProfileForm.querySelectorAll('select, input')) field.disabled = true;
|
|
previewButton.hidden = true;
|
|
const panel = runtimeProfileForm.querySelector('[data-runtime-profile-review]');
|
|
panel.querySelector('[data-profile-before]').textContent = profileDescription(plan.beforeProfile);
|
|
panel.querySelector('[data-profile-after]').textContent = profileDescription(plan.profile);
|
|
panel.querySelector('[data-profile-effect]').textContent = `${plan.effect} Reviewed at ${shortCommit(plan.currentHead)}.${plan.resetLaunchPolicy ? ' Switching adapters also clears the prior adapter-specific launch-policy override; the new adapter default applies.' : ''}`;
|
|
panel.querySelector('[data-runtime-profile-confirm]').hidden = false;
|
|
panel.querySelector('[data-runtime-profile-edit]').hidden = false;
|
|
panel.hidden = false;
|
|
runtimeProfileStatus(runtimeProfileForm, 'Review the current and next-launch profiles. Nothing has changed yet.');
|
|
} catch (error) {
|
|
runtimeProfileStatus(runtimeProfileForm, error.message, true);
|
|
} finally { previewButton.disabled = false; }
|
|
return;
|
|
}
|
|
|
|
const confirm = event.target.closest('[data-runtime-profile-confirm]');
|
|
if (!confirm) return;
|
|
const token = runtimeProfileForm.dataset.previewToken;
|
|
const planHash = runtimeProfileForm.dataset.planHash;
|
|
let payload;
|
|
try { payload = JSON.parse(runtimeProfileForm.dataset.profilePayload ?? ''); } catch { return; }
|
|
if (!token || !planHash) return;
|
|
confirm.disabled = true;
|
|
runtimeProfileStatus(runtimeProfileForm, 'Saving the reviewed profile for future launches…');
|
|
try {
|
|
const result = await consoleActionRequest('/api/runtime-profiles', { ...payload, token, planHash });
|
|
const status = $('#runtime-profile-action-status');
|
|
if (status) {
|
|
status.textContent = `Updated ${result.profile.areaId} for future launches. Any running session remains unchanged.`;
|
|
status.hidden = false;
|
|
}
|
|
clearRuntimeProfilePreview(runtimeProfileForm);
|
|
await refresh({ completed: runtimeProfileForm });
|
|
} catch (error) {
|
|
runtimeProfileStatus(runtimeProfileForm, error.message, true);
|
|
confirm.disabled = false;
|
|
}
|
|
});
|
|
|
|
const scheduleLauncherDetails = $('[data-schedule-launcher]');
|
|
scheduleLauncherDetails?.addEventListener('click', async (event) => {
|
|
const edit = event.target.closest('[data-schedule-launcher-edit]');
|
|
if (edit) {
|
|
clearScheduleLauncherPreview();
|
|
return;
|
|
}
|
|
|
|
const previewButton = event.target.closest('[data-schedule-launcher-preview]');
|
|
if (previewButton) {
|
|
const action = scheduleLauncherDetails.dataset.action;
|
|
if (!['install', 'reconcile'].includes(action) || previewButton.dataset.launcherReady !== 'true') return;
|
|
previewButton.disabled = true;
|
|
scheduleLauncherStatus('Checking current ownership, unit files, and schedule configuration…');
|
|
try {
|
|
const result = await consoleActionRequest('/api/schedule-launcher/preview', { action });
|
|
showScheduleLauncherPlan(scheduleLauncherDetails, result);
|
|
} catch (error) {
|
|
scheduleLauncherStatus(error.message, true);
|
|
} finally { previewButton.disabled = false; }
|
|
return;
|
|
}
|
|
|
|
const confirm = event.target.closest('[data-schedule-launcher-confirm]');
|
|
if (!confirm) return;
|
|
const token = scheduleLauncherDetails.dataset.previewToken;
|
|
const planHash = scheduleLauncherDetails.dataset.planHash;
|
|
const action = scheduleLauncherDetails.dataset.action;
|
|
if (!token || !planHash || !['install', 'reconcile'].includes(action)) return;
|
|
confirm.disabled = true;
|
|
scheduleLauncherStatus('Applying the reviewed timer setup…');
|
|
try {
|
|
const result = await consoleActionRequest('/api/schedule-launcher', { action, token, planHash });
|
|
const status = $('#schedule-launcher-action-status');
|
|
if (status) {
|
|
const verb = result.action === 'install' ? 'Installed' : 'Refreshed';
|
|
status.textContent = `${verb} the TORCH schedule timer for ${result.systemSchedules.length} system schedule(s). User systemd accepted the enable/start request; active state is not re-queried here. Audit ${result.auditEventId}.`;
|
|
status.hidden = false;
|
|
}
|
|
clearScheduleLauncherPreview('Timer action completed. The refreshed snapshot below reports owned files and configuration digest.');
|
|
await refresh({ completed: scheduleLauncherDetails });
|
|
} catch (error) {
|
|
scheduleLauncherStatus(error.message, true);
|
|
confirm.disabled = false;
|
|
}
|
|
});
|
|
|
|
function feedbackStatus(form, message, error = false) {
|
|
const status = form.querySelector('.feedback-form-status');
|
|
if (!status) return;
|
|
status.textContent = message;
|
|
status.classList.toggle('is-error', error);
|
|
}
|
|
|
|
function showFeedbackPreview(form, result) {
|
|
const preview = form.querySelector('[data-feedback-preview]');
|
|
const textarea = form.querySelector('textarea[name="body"]');
|
|
if (!preview || !textarea) return;
|
|
const plan = result.preview;
|
|
form.dataset.previewToken = result.token;
|
|
form.dataset.planHash = result.planHash;
|
|
textarea.disabled = true;
|
|
preview.innerHTML = `<div class="feedback-preview-heading"><strong>Review before sending</strong><span>One-time preview · 5 minute limit</span></div>
|
|
<dl class="feedback-preview-target"><div><dt>To</dt><dd>${escapeHtml(plan.recipient)}</dd></div><div><dt>Task</dt><dd>${escapeHtml(plan.taskId)}</dd></div><div><dt>Commit</dt><dd><code>${escapeHtml(shortCommit(plan.commit))}</code></dd></div></dl>
|
|
<blockquote><p>${escapeHtml(plan.body)}</p></blockquote>
|
|
<p>This creates one durable message linked to this artifact. It does not change task status, priority, or ownership.</p>
|
|
<div class="feedback-form-actions"><button type="button" data-feedback-confirm>Send to ${escapeHtml(plan.recipient)}</button><button type="button" class="quiet-action" data-feedback-edit>Change message</button></div>`;
|
|
preview.hidden = false;
|
|
feedbackStatus(form, 'Preview ready. Check the recipient and references before sending.');
|
|
}
|
|
|
|
$('#artifact-list')?.addEventListener('submit', async (event) => {
|
|
const form = event.target.closest('.artifact-feedback-form');
|
|
if (!form) return;
|
|
event.preventDefault();
|
|
const textarea = form.querySelector('textarea[name="body"]');
|
|
const button = form.querySelector('button[type="submit"]');
|
|
if (!textarea || !button || !textarea.value.trim()) return;
|
|
button.disabled = true;
|
|
feedbackStatus(form, 'Preparing a review preview…');
|
|
try {
|
|
const artifactId = form.dataset.artifactId;
|
|
const result = await feedbackRequest(`/api/artifacts/${encodeURIComponent(artifactId)}/feedback/preview`, {
|
|
body: textarea.value,
|
|
});
|
|
showFeedbackPreview(form, result);
|
|
} catch (error) {
|
|
feedbackStatus(form, error.message, true);
|
|
} finally { button.disabled = false; }
|
|
});
|
|
|
|
$('#artifact-list')?.addEventListener('click', async (event) => {
|
|
const edit = event.target.closest('[data-feedback-edit]');
|
|
if (edit) {
|
|
const form = edit.closest('.artifact-feedback-form');
|
|
const textarea = form?.querySelector('textarea[name="body"]');
|
|
const preview = form?.querySelector('[data-feedback-preview]');
|
|
if (form && textarea && preview) {
|
|
delete form.dataset.previewToken;
|
|
delete form.dataset.planHash;
|
|
textarea.disabled = false;
|
|
preview.hidden = true;
|
|
feedbackStatus(form, 'Preview cleared. Review the edited message again before sending.');
|
|
textarea.focus();
|
|
}
|
|
return;
|
|
}
|
|
|
|
const confirm = event.target.closest('[data-feedback-confirm]');
|
|
if (!confirm) return;
|
|
const form = confirm.closest('.artifact-feedback-form');
|
|
const textarea = form?.querySelector('textarea[name="body"]');
|
|
const token = form?.dataset.previewToken;
|
|
const planHash = form?.dataset.planHash;
|
|
if (!form || !textarea || !token || !planHash) return;
|
|
confirm.disabled = true;
|
|
feedbackStatus(form, 'Sending the reviewed message…');
|
|
try {
|
|
const artifactId = form.dataset.artifactId;
|
|
const result = await feedbackRequest(`/api/artifacts/${encodeURIComponent(artifactId)}/feedback`, {
|
|
body: textarea.value, token, planHash,
|
|
});
|
|
const actionStatus = $('#feedback-action-status');
|
|
if (actionStatus) {
|
|
actionStatus.textContent = `Feedback sent to ${result.feedback.recipient} for ${result.feedback.taskId} at ${shortCommit(result.feedback.commit)}.`;
|
|
actionStatus.hidden = false;
|
|
}
|
|
await refresh({ completed: form });
|
|
} catch (error) {
|
|
feedbackStatus(form, error.message, true);
|
|
confirm.disabled = false;
|
|
}
|
|
});
|
|
|
|
function organizationReviewStatus(form, message, error = false) {
|
|
const status = form.querySelector('.organization-review-status');
|
|
if (!status) return;
|
|
status.textContent = message;
|
|
status.classList.toggle('is-error', error);
|
|
}
|
|
|
|
function clearOrganizationReview(form, message = 'Preview cleared. Review the current proposal before deciding.') {
|
|
delete form.dataset.previewToken;
|
|
delete form.dataset.planHash;
|
|
form.querySelector('[name="action"]').disabled = false;
|
|
form.querySelector('[name="reason"]').disabled = false;
|
|
form.querySelector('[data-review-preview]').disabled = false;
|
|
form.querySelector('[data-review-preview-panel]').hidden = true;
|
|
form.querySelector('[data-review-confirm]').hidden = true;
|
|
form.querySelector('[data-review-edit]').hidden = true;
|
|
organizationReviewStatus(form, message);
|
|
}
|
|
|
|
$('#change-list')?.addEventListener('click', async (event) => {
|
|
const activationPreviewButton = event.target.closest('[data-hierarchy-activation-preview]');
|
|
if (activationPreviewButton) {
|
|
const card = activationPreviewButton.closest('.hierarchy-activation');
|
|
const id = activationPreviewButton.dataset.id;
|
|
const status = card?.querySelector('[data-hierarchy-activation-status]');
|
|
const setStatus = (message, error = false) => {
|
|
if (!status) return;
|
|
status.textContent = message;
|
|
status.classList.toggle('is-error', error);
|
|
};
|
|
activationPreviewButton.disabled = true;
|
|
setStatus('Rechecking owner approval, repository state, and pilot blockers…');
|
|
try {
|
|
const result = await consoleActionRequest(`/api/organization-proposals/hierarchy/${encodeURIComponent(id)}/activate/preview`, {});
|
|
const { plan } = result;
|
|
const panel = card.querySelector('[data-hierarchy-activation-panel]');
|
|
card.dataset.previewToken = result.token ?? '';
|
|
card.dataset.planHash = result.planHash;
|
|
panel.querySelector('[data-hierarchy-activation-title]').textContent = `${plan.confirmationLabel} · ${plan.title}`;
|
|
panel.querySelector('[data-hierarchy-activation-effect]').textContent = plan.effect;
|
|
panel.querySelector('[data-hierarchy-activation-scope]').textContent = JSON.stringify({
|
|
ownerIdentity: plan.ownerIdentity,
|
|
currentHead: plan.currentHead,
|
|
pilotPlan: plan.pilotPlan,
|
|
}, null, 2);
|
|
const confirm = panel.querySelector('[data-hierarchy-activation-confirm]');
|
|
confirm.textContent = plan.confirmationLabel;
|
|
confirm.hidden = !result.token || !plan.pilotPlan.canProceed;
|
|
panel.hidden = false;
|
|
setStatus(plan.pilotPlan.canProceed
|
|
? 'Ready. Review the exact files and commit effect, then explicitly confirm.'
|
|
: `Blocked by ${plan.pilotPlan.blockers.length} current condition${plan.pilotPlan.blockers.length === 1 ? '' : 's'}. Resolve them, then preview again.`,
|
|
!plan.pilotPlan.canProceed);
|
|
} catch (error) {
|
|
setStatus(error.message, true);
|
|
} finally { activationPreviewButton.disabled = false; }
|
|
return;
|
|
}
|
|
|
|
const activationConfirm = event.target.closest('[data-hierarchy-activation-confirm]');
|
|
if (activationConfirm) {
|
|
const card = activationConfirm.closest('.hierarchy-activation');
|
|
const proposalCard = activationConfirm.closest('.organization-proposal');
|
|
const id = proposalCard?.querySelector('[data-hierarchy-activation-preview]')?.dataset.id;
|
|
const token = card?.dataset.previewToken;
|
|
const planHash = card?.dataset.planHash;
|
|
const status = card?.querySelector('[data-hierarchy-activation-status]');
|
|
if (!id || !token || !planHash) return;
|
|
activationConfirm.disabled = true;
|
|
if (status) {
|
|
status.textContent = 'Committing the reviewed hierarchy pilot…';
|
|
status.classList.remove('is-error');
|
|
}
|
|
try {
|
|
const result = await consoleActionRequest(`/api/organization-proposals/hierarchy/${encodeURIComponent(id)}/activate`, { token, planHash });
|
|
const state = $('#organization-action-status');
|
|
if (state) {
|
|
const timerNote = result.proposal.timerReconciliationRequired
|
|
? ' Reconcile the installed timer separately before trusting the new cadence.'
|
|
: ' No timer was installed or changed.';
|
|
state.textContent = `${id} is piloting at ${shortCommit(result.proposal.activationCommit)}. No agent or runtime was started.${timerNote}`;
|
|
state.hidden = false;
|
|
}
|
|
await refresh({ completed: card });
|
|
} catch (error) {
|
|
if (status) {
|
|
status.textContent = error.message;
|
|
status.classList.add('is-error');
|
|
}
|
|
activationConfirm.disabled = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
const edit = event.target.closest('[data-review-edit]');
|
|
if (edit) {
|
|
const form = edit.closest('[data-organization-review]');
|
|
if (form) clearOrganizationReview(form);
|
|
return;
|
|
}
|
|
|
|
const previewButton = event.target.closest('[data-review-preview]');
|
|
if (previewButton) {
|
|
const form = previewButton.closest('[data-organization-review]');
|
|
const action = form?.querySelector('[name="action"]')?.value;
|
|
if (!form || !action) {
|
|
if (form) organizationReviewStatus(form, 'Choose a decision first.', true);
|
|
return;
|
|
}
|
|
previewButton.disabled = true;
|
|
organizationReviewStatus(form, 'Checking current proposal and owner authority…');
|
|
try {
|
|
const type = form.dataset.type;
|
|
const id = form.dataset.id;
|
|
const reason = form.querySelector('[name="reason"]').value.trim();
|
|
const result = await consoleActionRequest(`/api/organization-proposals/${encodeURIComponent(type)}/${encodeURIComponent(id)}/preview`, {
|
|
action, reason,
|
|
});
|
|
const plan = result.plan;
|
|
form.dataset.previewToken = result.token;
|
|
form.dataset.planHash = result.planHash;
|
|
form.querySelector('[name="action"]').disabled = true;
|
|
form.querySelector('[name="reason"]').disabled = true;
|
|
const panel = form.querySelector('[data-review-preview-panel]');
|
|
panel.querySelector('[data-review-title]').textContent = `${plan.confirmationLabel} · ${plan.title}`;
|
|
panel.querySelector('[data-review-effect]').textContent = `${plan.effect} Current state: ${plan.currentState}. Repository: ${shortCommit(plan.currentHead)}.`;
|
|
panel.querySelector('[data-review-scope]').textContent = JSON.stringify({
|
|
type: plan.type, id: plan.id, action: plan.action, reason: plan.reason,
|
|
proposalRevision: plan.proposalRevision,
|
|
baseCommit: plan.baseCommit, currentHead: plan.currentHead,
|
|
ownerIdentity: plan.ownerIdentity,
|
|
activeGraphRevision: plan.activeGraphRevision,
|
|
activeGraphDigest: plan.activeGraphDigest,
|
|
scope: plan.scope ?? {
|
|
impact: plan.impact, observationWindow: plan.observationWindow,
|
|
signals: plan.signals, alternatives: plan.alternatives,
|
|
pilot: plan.pilot, roleChanges: plan.roleChanges,
|
|
managerCheckInSchedules: plan.managerCheckInSchedules,
|
|
timerReconciliation: plan.timerReconciliation,
|
|
},
|
|
expectedBenefit: plan.expectedBenefit, evidence: plan.evidence, effect: plan.effect,
|
|
}, null, 2);
|
|
panel.querySelector('[data-review-confirm]').textContent = plan.confirmationLabel;
|
|
panel.querySelector('[data-review-confirm]').hidden = !result.token || plan.canProceed === false;
|
|
panel.querySelector('[data-review-edit]').hidden = false;
|
|
panel.hidden = false;
|
|
organizationReviewStatus(form, plan.canProceed === false
|
|
? 'Blocked. Inspect the current conditions in the plan; no confirmation is available.'
|
|
: 'Preview ready. Read the effect and exact proposal scope before confirming.', plan.canProceed === false);
|
|
} catch (error) {
|
|
organizationReviewStatus(form, error.message, true);
|
|
} finally { previewButton.disabled = false; }
|
|
return;
|
|
}
|
|
|
|
const confirm = event.target.closest('[data-review-confirm]');
|
|
if (!confirm) return;
|
|
const form = confirm.closest('[data-organization-review]');
|
|
const token = form?.dataset.previewToken;
|
|
const planHash = form?.dataset.planHash;
|
|
if (!form || !token || !planHash) return;
|
|
confirm.disabled = true;
|
|
organizationReviewStatus(form, 'Recording the reviewed owner decision…');
|
|
try {
|
|
const result = await consoleActionRequest(`/api/organization-proposals/${encodeURIComponent(form.dataset.type)}/${encodeURIComponent(form.dataset.id)}`, {
|
|
action: form.querySelector('[name="action"]').value,
|
|
reason: form.querySelector('[name="reason"]').value.trim(), token, planHash,
|
|
});
|
|
const status = $('#organization-action-status');
|
|
if (status) {
|
|
const note = result.type === 'hierarchy-conclusion'
|
|
? demoWorkspace
|
|
? ' Demo decision only. No Git commit, agent or timer was changed.'
|
|
: ' One scoped local conclusion commit was recorded. No agent or timer was started or stopped; review obsolete schedules separately.'
|
|
: result.type === 'fleet'
|
|
? ' This does not provision or start the proposed identity.'
|
|
: result.decision === 'approve-pilot'
|
|
? ' This approves planning only; after reviewing a fresh hierarchy-plan, the owner separately runs hierarchy-activate --by owner --yes. Activation does not start identities or timers.'
|
|
: ' The active organization remains unchanged.';
|
|
status.textContent = `${result.proposal.id} recorded as ${result.proposal.state}.${note}`;
|
|
status.hidden = false;
|
|
}
|
|
await refresh({ completed: form });
|
|
} catch (error) {
|
|
organizationReviewStatus(form, error.message, true);
|
|
confirm.disabled = false;
|
|
}
|
|
});
|
|
|
|
$('#attention-list')?.addEventListener('click', (event) => {
|
|
const decisionButton = event.target.closest('[data-attention-decision]');
|
|
if (decisionButton) {
|
|
const card = decisionButton.closest('.attention-item');
|
|
const status = card?.querySelector('.attention-action-status');
|
|
const approvalId = decisionButton.dataset.attentionApproval;
|
|
const decision = decisionButton.dataset.attentionDecision;
|
|
const form = [...document.querySelectorAll('#approval-request-list .approval-decision-form')]
|
|
.find((candidate) => candidate.dataset.approvalId === approvalId);
|
|
const previewButton = form?.querySelector('[data-approval-preview]');
|
|
const shortcut = globalThis.TorchAttentionActions.prepareOwnerDecisionShortcut(form, decision, previewButton ? (approvalForm) => {
|
|
approvalForm.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
previewButton.click();
|
|
} : null);
|
|
if (!shortcut.started) {
|
|
if (status) {
|
|
status.textContent = shortcut.reason;
|
|
status.hidden = false;
|
|
}
|
|
return;
|
|
}
|
|
if (status) {
|
|
status.textContent = `Opening the guarded ${decision} preview for this owner-addressed request. Confirm only after reviewing its current evidence.`;
|
|
status.hidden = false;
|
|
}
|
|
return;
|
|
}
|
|
const link = event.target.closest('a[href^="#"]');
|
|
const target = link && document.getElementById(link.getAttribute('href').slice(1));
|
|
const recipient = link?.dataset.attentionRecipient;
|
|
if (recipient) {
|
|
const form = $('#owner-request-form');
|
|
const recipientSelect = form?.elements.namedItem('recipient');
|
|
const body = form?.elements.namedItem('body');
|
|
const status = form?.querySelector('.owner-request-status');
|
|
const hasRecipient = [...(recipientSelect?.options ?? [])].some((option) => option.value === recipient);
|
|
if (form && recipientSelect && body && hasRecipient && !recipientSelect.value && !body.value.trim()) {
|
|
recipientSelect.value = recipient;
|
|
if (status) status.textContent = `Selected ${recipient}. Add context and review the preview before sending anything.`;
|
|
}
|
|
}
|
|
for (let parent = target?.parentElement; parent; parent = parent.parentElement) {
|
|
if (parent.tagName === 'DETAILS') parent.open = true;
|
|
}
|
|
});
|
|
|
|
$('#refresh-console')?.addEventListener('click', () => refresh());
|
|
$('#fleet-attention-filter')?.addEventListener('input', (event) => applyFleetAttentionFilter(event.currentTarget.value));
|
|
$('#toggle-live-refresh')?.addEventListener('click', () => {
|
|
liveUpdatesPaused = !liveUpdatesPaused;
|
|
$('#toggle-live-refresh').textContent = liveUpdatesPaused ? 'Resume updates' : 'Pause updates';
|
|
$('#toggle-live-refresh').setAttribute('aria-pressed', String(liveUpdatesPaused));
|
|
$('#live-refresh-status').textContent = liveUpdatesPaused ? 'Automatic updates paused. Manual refresh preserves edits.' : 'Updates every 15 seconds while visible.';
|
|
if (!liveUpdatesPaused && document.visibilityState === 'visible') refresh();
|
|
});
|
|
let liveRefreshTimer = null;
|
|
function startLiveRefreshTimer() {
|
|
if (liveRefreshTimer !== null) return;
|
|
liveRefreshTimer = setInterval(() => {
|
|
if (!liveUpdatesPaused && document.visibilityState === 'visible') refresh({ background: true });
|
|
}, 15_000);
|
|
}
|
|
startLiveRefreshTimer();
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (!liveUpdatesPaused && document.visibilityState === 'visible') refresh();
|
|
});
|
|
globalThis.addEventListener('pagehide', () => {
|
|
clearInterval(liveRefreshTimer);
|
|
liveRefreshTimer = null;
|
|
});
|
|
globalThis.addEventListener('pageshow', (event) => {
|
|
startLiveRefreshTimer();
|
|
if (event.persisted && !liveUpdatesPaused && document.visibilityState === 'visible') refresh();
|
|
});
|
|
if (demoWorkspace) {
|
|
$('#demo-banner').hidden = false;
|
|
$('#reset-demo')?.addEventListener('click', () => {
|
|
demoWorkspace.reset();
|
|
globalThis.location.reload();
|
|
});
|
|
}
|
|
consoleViewRouter.sync({ force: true });
|
|
refresh();
|