diff --git a/engine/main.py b/engine/main.py index 919585c..36bee6e 100644 --- a/engine/main.py +++ b/engine/main.py @@ -77,6 +77,11 @@ async def dashboard() -> FileResponse: return FileResponse(Path(__file__).parent / "static" / "index.html") +@app.get("/office") +async def office() -> FileResponse: + return FileResponse(Path(__file__).parent / "static" / "office.html") + + @app.get("/health") async def health() -> dict: row = await db.fetch_one("SELECT count(*) AS companies FROM company") @@ -295,6 +300,46 @@ async def shareholder_directive(slug: str, body: DirectiveCreate) -> dict: return result +class AnswerCreate(BaseModel): + answer: str = Field(min_length=1, max_length=4000) + + +@app.post("/companies/{slug}/work-items/{item_id}/answer") +async def answer_blocked_item(slug: str, item_id: UUID, body: AnswerCreate) -> dict: + """Shareholder answers a blocked/clarification item; work resumes.""" + item = await db.fetch_one( + "SELECT wi.* FROM work_item wi JOIN company c ON c.id = wi.company_id " + "WHERE wi.id = %s AND c.slug = %s", item_id, slug) + if not item: + raise HTTPException(404, "no such work item") + if item["status"] != "blocked": + raise HTTPException(409, f"work item is {item['status']}, not blocked") + + new_desc = f"{item['description']}\n\n## Shareholder answer\n{body.answer}" + if item["hermes_task_id"]: + # rerun the existing kanban task with the answer appended + try: + await hermes.update_task(item["hermes_task_id"], status="ready", body=new_desc) + except HermesError as e: + raise HTTPException(502, f"hermes update failed: {e}") + new_status = "in_progress" # poll loop picks the rerun up + else: + new_status = "ready" # never dispatched (e.g. dispatch.failed); dispatch fresh + + async with db.pool.connection() as conn: + async with conn.transaction(): + await conn.execute( + "UPDATE work_item SET description=%s, status=%s, updated_at=now() WHERE id=%s", + (new_desc, new_status, item_id)) + await conn.execute( + "UPDATE employee SET status='working' WHERE id=%s", (item["owner_id"],)) + await db.append_event( + conn, actor="shareholder:philip", event_type="clarification.answered", + company_id=item["company_id"], work_item_id=item_id, + source="api", payload={"answer": body.answer[:500]}) + return {"status": new_status} + + @app.get("/companies/{slug}/work-items") async def list_work_items(slug: str) -> list[dict]: company = await db.fetch_one("SELECT id FROM company WHERE slug = %s", slug) diff --git a/engine/static/index.html b/engine/static/index.html index 4944067..3ced710 100644 --- a/engine/static/index.html +++ b/engine/static/index.html @@ -37,6 +37,7 @@

๐Ÿ  FarmHouse Holdings + ๐Ÿ•น office view โ€ฆ

@@ -53,6 +54,15 @@ async function togglePause() { 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); @@ -86,8 +96,11 @@ async function refresh() { j(`/companies/${c.slug}/events?limit=40`)]); const emps = detail.employees.map(e => `${e.name}${e.title}${pill(e.status)}`).join(''); - const rows = items.filter(w => w.type !== 'objective').map(w => - `${w.title}${w.owner_name ?? ''}${pill(w.status)}`).join(''); + const rows = items.filter(w => w.type !== 'objective').map(w => { + const answer = w.status === 'blocked' + ? ` ` : ''; + return `${w.title}${w.owner_name ?? ''}${pill(w.status)}${answer}`; + }).join(''); const objs = items.filter(w => w.type === 'objective').map(w => `${w.title}${pill(w.status)}`).join(''); const feed = events.map(e => { diff --git a/engine/static/office.html b/engine/static/office.html new file mode 100644 index 0000000..bc28c6f --- /dev/null +++ b/engine/static/office.html @@ -0,0 +1,93 @@ + + + + + +FarmHouse Office + + + +

๐Ÿ  FarmHouse Office โ† dashboard

+ +
โ€ฆ
+ + +