Files
FarmHouse-Holdings/engine/static/index.html
T
fez5587andClaude Fable 5 eff5cb4510 Live worker logs: see what an in-progress worker is doing/thinking
- GET /companies/{slug}/work-items/{id}/log proxies the Hermes task log
  (agent reasoning blocks + tool calls), tail-limited, served as text
- dashboard: every dispatched work item row gets a "log" link
- office: click a working employee sprite to open their live log

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 15:08:07 -05:00

164 lines
8.3 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>FarmHouse Holdings</title>
<style>
:root { --bg:#101418; --card:#1a2027; --text:#d8dee6; --dim:#8a94a0; --line:#2a323c;
--ok:#4caf7d; --warn:#e0b354; --bad:#e06c5c; --run:#5c9de0; }
* { box-sizing:border-box; margin:0; }
body { background:var(--bg); color:var(--text); font:14px/1.5 system-ui, sans-serif; padding:1.5rem; }
h1 { font-size:1.2rem; margin-bottom:1rem; display:flex; align-items:center; gap:.75rem; }
h2 { font-size:.95rem; color:var(--dim); margin:1.2rem 0 .5rem; text-transform:uppercase; letter-spacing:.05em; }
.grid { display:grid; grid-template-columns:1fr 1fr; gap:1.5rem; align-items:start; }
@media (max-width:900px){ .grid{grid-template-columns:1fr} }
.card { background:var(--card); border:1px solid var(--line); border-radius:8px; padding:1rem; margin-bottom:1rem; }
table { width:100%; border-collapse:collapse; }
td,th { padding:.35rem .5rem; text-align:left; border-bottom:1px solid var(--line); vertical-align:top; }
th { color:var(--dim); font-weight:500; font-size:.8rem; }
.pill { display:inline-block; padding:.05rem .5rem; border-radius:99px; font-size:.75rem; border:1px solid var(--line); }
.pill.done,.pill.idle,.pill.active { color:var(--ok); border-color:var(--ok); }
.pill.in_progress,.pill.working { color:var(--run); border-color:var(--run); }
.pill.blocked { color:var(--bad); border-color:var(--bad); }
.pill.ready,.pill.backlog { color:var(--warn); border-color:var(--warn); }
.feed { max-height:32rem; overflow-y:auto; font-size:.8rem; }
.feed .ev { padding:.3rem 0; border-bottom:1px solid var(--line); }
.feed .t { color:var(--dim); margin-right:.5rem; }
.feed .type { color:var(--run); margin-right:.5rem; }
button { background:var(--card); color:var(--text); border:1px solid var(--line); border-radius:6px;
padding:.4rem .9rem; cursor:pointer; font-size:.85rem; }
button:hover { border-color:var(--dim); }
textarea { width:100%; background:var(--bg); color:var(--text); border:1px solid var(--line);
border-radius:6px; padding:.5rem; font:inherit; resize:vertical; margin-bottom:.4rem; }
#pausebtn.paused { color:var(--bad); border-color:var(--bad); }
.muted { color:var(--dim); }
</style>
</head>
<body>
<h1>🏠 FarmHouse Holdings
<a href="/office" style="color:var(--run); font-size:.8rem; text-decoration:none;">🕹 office view</a>
<span id="health" class="pill">…</span>
<button id="pausebtn" onclick="togglePause()">…</button>
</h1>
<div id="companies"></div>
<script>
let paused = false;
async function j(url, opts) { const r = await fetch(url, opts); return r.json(); }
function pill(s) { return `<span class="pill ${s}">${s}</span>`; }
async function togglePause() {
await j(paused ? '/resume' : '/pause', {method:'POST'});
refresh();
}
async function companyState(slug, action) {
await fetch(`/companies/${slug}/${action}`, {method:'POST'});
refresh();
}
async function answerItem(slug, id) {
const answer = window.prompt('Answer / unblock instruction for this task:');
if (!answer || !answer.trim()) return;
const r = await fetch(`/companies/${slug}/work-items/${id}/answer`, {method:'POST',
headers:{'Content-Type':'application/json'}, body: JSON.stringify({answer: answer.trim()})});
if (!r.ok) alert(`answer failed: ${(await r.json()).detail ?? r.status}`);
refresh();
}
async function sendDirective(ev, slug) {
ev.preventDefault();
const el = document.getElementById('dir-' + slug);
const message = el.value.trim();
if (!message) return false;
await j(`/companies/${slug}/directives`, {method:'POST',
headers:{'Content-Type':'application/json'}, body: JSON.stringify({message})});
el.value = '';
refresh();
return false;
}
async function refresh() {
// don't clobber a directive mid-typing
if (document.activeElement && document.activeElement.tagName === 'TEXTAREA') return;
try {
const h = await j('/health');
paused = h.paused;
document.getElementById('health').textContent = h.status;
document.getElementById('health').className = 'pill ' + (h.status === 'ok' ? 'done' : 'blocked');
const b = document.getElementById('pausebtn');
b.textContent = paused ? '▶ RESUME (paused)' : '⏸ PAUSE ALL';
b.className = paused ? 'paused' : '';
const companies = await j('/companies');
let html = '';
for (const c of companies) {
if (c.lifecycle_state === 'archived') continue;
const [detail, items, events, files] = await Promise.all([
j(`/companies/${c.slug}`), j(`/companies/${c.slug}/work-items`),
j(`/companies/${c.slug}/events?limit=40`),
j(`/companies/${c.slug}/files`).catch(() => [])]);
const emps = detail.employees.map(e =>
`<tr><td>${e.name}</td><td class="muted">${e.title}</td><td>${pill(e.status)}</td></tr>`).join('');
const rows = items.filter(w => w.type !== 'objective').map(w => {
const answer = w.status === 'blocked'
? ` <button onclick="answerItem('${c.slug}','${w.id}')">answer</button>` : '';
const log = w.hermes_task_id
? ` <a href="/companies/${c.slug}/work-items/${w.id}/log" target="_blank" class="muted">log</a>` : '';
return `<tr><td>${w.title}${log}</td><td class="muted">${w.owner_name ?? ''}</td><td>${pill(w.status)}${answer}</td></tr>`;
}).join('');
const objs = items.filter(w => w.type === 'objective').map(w =>
`<tr><td>${w.title}</td><td>${pill(w.status)}</td></tr>`).join('');
const feed = events.map(e => {
const t = new Date(e.created_at).toLocaleTimeString();
const p = e.payload ?? {};
const txt = p.summary ?? p.reason ?? p.title ?? p.question ?? p.name ?? '';
return `<div class="ev"><span class="t">${t}</span><span class="type">${e.event_type}</span>${String(txt).slice(0,120)}</div>`;
}).join('');
const CEO_EVENTS = ['milestone.update', 'status.update', 'directive.received', 'clarification.requested'];
const ceoFeed = events.filter(e => CEO_EVENTS.includes(e.event_type)).map(e => {
const t = new Date(e.created_at).toLocaleTimeString();
const p = e.payload ?? {};
const txt = p.summary ?? p.message ?? p.question ?? '';
const who = e.event_type === 'directive.received' ? '🧑‍💼 you' : '👔 CEO';
return `<div class="ev"><span class="t">${t}</span><span class="type">${who}</span>${String(txt).slice(0,300)}</div>`;
}).join('');
const cpBtn = c.lifecycle_state === 'paused'
? `<button onclick="companyState('${c.slug}','resume')">▶ resume</button>`
: `<button onclick="companyState('${c.slug}','pause')">⏸ pause</button>`;
html += `<div class="card"><h1>${c.name} <span class="pill ${c.lifecycle_state}">${c.lifecycle_state}</span> ${cpBtn}</h1>
<div class="grid">
<div>
<h2>Employees</h2><table>${emps}</table>
<h2>Objectives</h2><table>${objs || '<tr><td class="muted">none</td></tr>'}</table>
<h2>Work items</h2><table>${rows || '<tr><td class="muted">none</td></tr>'}</table>
</div>
<div>
<h2>Shareholder line</h2>
<form onsubmit="return sendDirective(event, '${c.slug}')">
<textarea id="dir-${c.slug}" rows="2" placeholder="Directive to ${c.name} leadership…"></textarea>
<button type="submit">Send directive</button>
</form>
<h2>CEO updates</h2><div class="feed">${ceoFeed || '<div class="muted">no updates yet</div>'}</div>
<h2>Workspace (demos)</h2><div class="feed">${
(Array.isArray(files) ? files : []).filter(f => !f.dir).map(f =>
`<div class="ev"><a href="/companies/${c.slug}/files/${encodeURIComponent(f.name)}" target="_blank">${f.name}</a></div>`).join('')
|| '<div class="muted">no files yet</div>'}</div>
<h2>Event feed</h2><div class="feed">${feed}</div>
</div>
</div></div>`;
}
document.getElementById('companies').innerHTML = html || '<p class="muted">No companies yet.</p>';
} catch (e) {
document.getElementById('health').textContent = 'engine unreachable';
document.getElementById('health').className = 'pill blocked';
}
}
refresh();
setInterval(refresh, 5000);
</script>
</body>
</html>