--- Build: not run | Tests: not run --- Build: pass | Tests: pass — Tests 861 passed (861)
20 KiB
Dashboard API Mapping — Paperclip UI ↔ Clawdie Controlplane
ARCHIVED: Paperclip dashboard track is halted. Kept for historical reference.
See doc/AGENTIC-HARNESS-PIVOT.md for the current direction.
Date: 2026-04-09 Status: Design document — proxy layer reference for Phase D Scope: MVP 4-panel dashboard only. Dashboard summary, heartbeat runs, and write mutations deferred.
1. Purpose
The Paperclip UI expects its own API contract. We serve it unchanged via a read-only git submodule (or standalone checkout) and bridge the gap with a proxy layer in src/dashboard-proxy.ts. This document defines that translation contract.
The proxy mounts Paperclip-compatible routes under /api/paperclip/* (or rewrites the UI's API base path at build time). It translates inbound Paperclip requests into Clawdie controlplane API calls or direct DB queries, then reshapes responses into what the Paperclip UI expects.
2. Concept Mapping
Clawdie is single-tenant. One instance = one "company" in Paperclip terms.
| Paperclip Concept | Clawdie Equivalent | Notes |
|---|---|---|
Company |
Entire Clawdie instance | Hardcoded company ID "clawdie" in proxy. |
Company.id |
"clawdie" |
Static. All GET /api/companies/clawdie/... routes resolve to this. |
Agent |
agents table row |
4 agents: orchestrator, sysadmin, db_admin, git_admin. |
Agent.role (ceo, engineer, ...) |
agents.role (orchestrator, sysadmin_agent, ...) |
Role enum differs — see mapping table below. |
Agent.status (active, paused, ...) |
agents.heartbeat_enabled + budget state |
Synthesized — see field table. |
Issue |
tasks table row |
Status enum differs — see mapping table below. |
Issue.identifier (PAP-42) |
tasks.id (API-{base36}) |
Pass through as-is. |
Approval |
approvals table row |
Fields differ — see field table. |
ActivityEvent |
agent_activity table row |
Field names differ — see field table. |
HeartbeatRun |
No equivalent | Stub only for MVP. |
OrgNode |
No equivalent | Flat agent list for MVP. |
Project / Goal / Label |
No equivalent | Stub only. |
Agent Role Mapping
Paperclip AgentRole |
Clawdie AgentRole |
Direction |
|---|---|---|
ceo |
orchestrator |
Paperclip → Clawdie: map ceo → orchestrator |
devops |
sysadmin_agent |
Paperclip → Clawdie: map devops → sysadmin_agent |
| — | db_admin_agent |
Clawdie has no Paperclip role equivalent; map to general |
| — | git_admin_agent |
Clawdie has no Paperclip role equivalent; map to general |
For the reverse (Clawdie → Paperclip response):
Clawdie AgentRole |
Paperclip AgentRole in response |
|---|---|
orchestrator |
ceo |
sysadmin_agent |
devops |
db_admin_agent |
engineer |
git_admin_agent |
engineer |
Task Status Mapping
Clawdie status |
Paperclip IssueStatus |
|---|---|
pending |
todo |
in_progress |
in_progress |
| (any other) | backlog |
Paperclip also has in_review, done, blocked, cancelled. For now, any Clawdie status not in the table above maps to backlog.
Priority Mapping
Clawdie priority |
Paperclip IssuePriority |
|---|---|
critical |
critical |
high |
high |
medium |
medium |
low |
low |
| (any other) | medium |
3. Endpoint Translation
The proxy intercepts 6 Paperclip API calls that the 4 dashboard panels make. All other Paperclip endpoints return 501 (Not Implemented) — the UI should degrade gracefully.
3.1 Agent List Panel
Paperclip calls:
GET /api/companies/{companyId}/agents
GET /api/companies/{companyId}/dashboard (counts for badges)
Proxy maps to:
GET /api/controlplane/state
Response translation:
Paperclip expects: Agent[] (array of full agent objects)
Clawdie returns: { agents: [{id, role, heartbeat_enabled}], budget: {...} }
Field-by-field mapping for each agent:
Paperclip Agent field |
Source | Strategy |
|---|---|---|
id |
agents.id |
Direct. |
companyId |
— | Hardcode "clawdie". |
name |
agents.id |
Use agent ID as display name. |
urlKey |
agents.id |
Same as name — slugified agent ID. |
role |
agents.role |
Map via role table above. |
title |
agents.role |
Human-readable label: "Orchestrator", "Sysadmin Agent", etc. |
icon |
— | Stub null. |
status |
agents.heartbeat_enabled + budget |
Synthesized: heartbeat_enabled && budget.remaining > 0 → "active", heartbeat_enabled && budget.hard_limit_exceeded → "paused", !heartbeat_enabled → "idle". |
reportsTo |
— | All agents report to orchestrator: return orchestrator ID. Orchestrator returns null. |
capabilities |
— | Stub null. |
adapterType |
agents.adapter |
Map "pi-local" → "process", "codex" → "codex_local". |
adapterConfig |
— | Stub {}. |
runtimeConfig |
— | Stub {}. |
budgetMonthlyCents |
budget allocation % × daily_tokens × 30 |
Synthesized from budget data. Approximate — Paperclip tracks monthly cents, we track daily tokens. Use (allocation_pct / 100) * daily_tokens * 30 as a rough proxy. |
spentMonthlyCents |
budget.spent_today × 30 |
Synthesized — rough monthly projection. |
pauseReason |
— | null when active, "budget" when hard_limit_exceeded, "manual" when heartbeat disabled. |
pausedAt |
— | Stub null. |
permissions |
— | Stub { canCreateAgents: false }. |
lastHeartbeatAt |
Latest agent_activity.created_at for this agent |
Requires new query. See gap analysis. Fallback: null. |
metadata |
— | Stub null. |
createdAt |
agents.created_at |
Direct. |
updatedAt |
— | Stub same as createdAt. |
3.2 Task Queue Panel
Paperclip calls:
GET /api/companies/{companyId}/issues?status=...
GET /api/issues/{id} (detail view)
Proxy maps to:
GET /api/controlplane/tasks
GET /api/controlplane/tasks?role={agentId} (filtered)
Response translation:
Paperclip expects: Issue[] (array of full issue objects)
Clawdie returns: { tasks: [{task_id, title, description, assigned_to, priority, status, created_at}] }
Field-by-field mapping:
Paperclip Issue field |
Source | Strategy |
|---|---|---|
id |
tasks.id |
Direct. |
companyId |
— | Hardcode "clawdie". |
projectId |
— | Stub null. |
projectWorkspaceId |
— | Stub null. |
goalId |
— | Stub null. |
parentId |
— | Stub null (no subtask support). |
title |
tasks.title |
Direct. |
description |
tasks.description |
Direct. |
status |
tasks.status |
Map via status table above. |
priority |
tasks.priority |
Map via priority table above. |
assigneeAgentId |
tasks.assigned_to |
Direct. |
assigneeUserId |
— | Stub null. |
checkoutRunId |
— | Stub null. |
executionRunId |
— | Stub null. |
issueNumber |
— | Stub null. |
identifier |
tasks.id |
Pass through — our API-xxx format serves as identifier. |
originKind |
— | Stub "manual". |
requestDepth |
— | Stub 0. |
billingCode |
— | Stub null. |
startedAt |
tasks.created_at |
Use created_at as approximation. |
completedAt |
— | Stub null (no completion tracking). |
cancelledAt |
— | Stub null. |
hiddenAt |
— | Stub null. |
labelIds |
— | Stub []. |
blockedBy |
— | Stub []. |
blocks |
— | Stub []. |
createdAt |
tasks.created_at |
Direct. |
updatedAt |
— | Stub same as createdAt (no updated_at column). |
Query parameter translation:
| Paperclip query param | Proxy behavior |
|---|---|
status=backlog,todo |
Map to WHERE status IN ('pending') — we don't distinguish backlog vs todo. |
status=in_progress |
Map to WHERE status = 'in_progress'. |
assigneeAgentId=X |
Map to role=X on our GET /tasks?role=X endpoint. |
limit=N |
Forward as-is (add LIMIT to query). |
q=search |
No support — ignore for MVP. |
3.3 Approvals Pane
Paperclip calls:
GET /api/companies/{companyId}/approvals?status=pending
GET /api/approvals/{id}
Proxy maps to:
GET /api/controlplane/approvals
GET /api/controlplane/approvals?agent_id={agentId}
Response translation:
Paperclip expects a flat Approval[] with a status field. Clawdie returns { pending: [...], approved: [...] } split by operator_approved boolean.
Paperclip Approval field |
Source | Strategy |
|---|---|---|
id |
approvals.id |
Direct. |
companyId |
— | Hardcode "clawdie". |
type |
— | Synthesized from context: if estimated_tokens is set → "budget_override_required", otherwise "request_board_approval". |
requestedByAgentId |
approvals.agent_id |
Direct. |
requestedByUserId |
— | Stub null. |
status |
approvals.operator_approved |
operator_approved = true → "approved", operator_approved = false → "pending". |
payload |
approvals.operation + approvals.estimated_tokens |
Construct { operation, estimated_tokens }. |
decisionNote |
— | Stub null. |
decidedByUserId |
— | Stub null. |
decidedAt |
— | Stub null. No approved_at column exists. |
createdAt |
approvals.created_at |
Direct. |
updatedAt |
— | Stub same as createdAt. |
The proxy flattens the two Clawdie arrays into one Paperclip Approval[], setting status per row. When the Paperclip UI filters by ?status=pending, the proxy returns only the pending array.
3.4 Activity Log Panel
Paperclip calls:
GET /api/companies/{companyId}/activity?entityType=X&entityId=Y&agentId=Z
Proxy maps to:
Direct DB query on agent_activity (no API endpoint exists yet)
GAP: Clawdie has no GET /activity endpoint. The proxy must query the DB directly. See gap analysis in section 5.
Field-by-field mapping:
Paperclip ActivityEvent field |
Source | Strategy |
|---|---|---|
id |
agent_activity.id |
Direct. |
companyId |
— | Hardcode "clawdie". |
actorType |
agent_activity.agent_id |
Always "agent" (we have no user-initiated activity). |
actorId |
agent_activity.agent_id |
Direct. |
action |
agent_activity.event_type |
Map: "task_completed" → "issue.completed", "approval_request" → "approval.requested", "error" → "agent.error". |
entityType |
agent_activity.event_type |
Map: "task_completed" → "issue", "approval_request" → "approval", "error" → "agent". |
entityId |
agent_activity.payload->>'task_id' |
Extract from JSONB payload. Fallback: agent_id. |
agentId |
agent_activity.agent_id |
Direct. |
runId |
— | Stub null (no heartbeat run tracking). |
details |
agent_activity.payload |
Direct — pass the full JSONB payload. |
createdAt |
agent_activity.created_at |
Direct. |
Query parameter translation:
| Paperclip query param | Proxy behavior |
|---|---|
entityType=issue |
Filter WHERE event_type = 'task_completed'. |
entityType=approval |
Filter WHERE event_type = 'approval_request'. |
entityType=agent |
Filter WHERE event_type = 'error'. |
agentId=X |
Filter WHERE agent_id = 'X'. |
entityId=Y |
Filter WHERE payload @> '{"task_id": "Y"}' (JSONB containment). |
4. Stub Strategy
Fields the proxy returns as fixed values because Clawdie has no equivalent concept.
| Stub value | Applied to | Rationale |
|---|---|---|
null |
projectId, goalId, parentId, labelIds, blockedBy, blocks, issueNumber, checkoutRunId, executionRunId, billingCode, icon, metadata, capabilities |
Paperclip concepts that don't exist in Clawdie's simpler model. UI should show empty/hidden sections. |
"manual" |
originKind |
All tasks in Clawdie are operator-created. |
0 |
requestDepth |
No nesting. |
false |
permissions.canCreateAgents |
Agent creation is a setup-time operation, not runtime. |
{} |
adapterConfig, runtimeConfig |
Empty but valid objects so the UI doesn't crash on property access. |
Same as createdAt |
updatedAt (on all entities) |
No updated_at column exists. UI shows identical create/update times. |
agent.created_at |
lastHeartbeatAt (fallback) |
If no activity found, use agent creation time so the UI shows something rather than "never". |
501 Endpoints
These Paperclip endpoints are called by the UI but return 501 Not Implemented for MVP. The UI should degrade gracefully (hide the section or show "not available").
POST /api/companies/{id}/agents— agent creationPATCH /api/agents/{id}— agent editingPOST /api/agents/{id}/pause,POST /api/agents/{id}/resume— agent controlPOST /api/companies/{id}/issues— issue creationPATCH /api/issues/{id}— issue editingPOST /api/approvals/{id}/approve,POST /api/approvals/{id}/reject— approval decisionsGET /api/companies/{id}/heartbeat-runs— heartbeat run historyGET /api/companies/{id}/org— org chartGET /api/companies/{id}/dashboard— aggregate summary- All
/api/projects/*,/api/goals/*,/api/routines/*,/api/secrets/*endpoints
5. Gap Analysis
API endpoints and DB changes needed before the proxy layer can serve all 4 panels.
Critical (blocking)
| Gap | Impact | Fix |
|---|---|---|
No GET /activity endpoint |
Activity panel cannot function. Proxy must query DB directly — breaks if proxy is separated from DB. | Add GET /api/controlplane/activity to controlplane-api.ts with query params: ?agent_id=&event_type=&limit=&offset=. Query: SELECT * FROM agent_activity WHERE ... ORDER BY created_at DESC LIMIT $n. |
No updated_at on any table |
All entities return stale updatedAt values. |
Add updated_at TIMESTAMPTZ DEFAULT NOW() to agents, tasks, approvals tables. Update on every mutation. |
No approved_at on approvals |
Cannot show when an approval was decided. | Add approved_at TIMESTAMPTZ to approvals table. Set when operator_approved flips to true. |
Important (degrades experience)
| Gap | Impact | Fix |
|---|---|---|
No lastHeartbeatAt query |
Agent panel shows "never" for last heartbeat. | Add query: SELECT agent_id, MAX(created_at) as last_heartbeat FROM agent_activity GROUP BY agent_id. Expose via GET /api/controlplane/state or a new endpoint. |
| No task status transitions | Tasks stay pending forever in the UI — no way to mark in_progress or done. |
Add PATCH /api/controlplane/tasks/:id with `{ status: "in_progress" |
| No approval mutations | Approvals can only be viewed, never approved/rejected. | Add POST /api/controlplane/approvals/:id/approve and POST /api/controlplane/approvals/:id/reject. |
GET /tasks?role=X is misnamed |
Proxy must know that role param actually filters by assigned_to (agent ID, not role). |
No code change needed — just document this in the proxy. Long-term: rename param to assigned_to. |
context excluded from TaskResponse |
Task detail view loses rich context stored in JSONB. | Add context field to formatTask() in controlplane-api.ts. |
deadline excluded from TaskResponse |
Deadline stored but never returned. | Add deadline field to formatTask(). |
Nice-to-have (deferred)
| Gap | Impact | Fix |
|---|---|---|
| No heartbeat run tracking | Cannot show execution history per agent. | New heartbeat_runs table + write path in heartbeat loop. Phase 2. |
| No org chart data | Flat agent list only. | Synthesize from reportsTo pattern (all → orchestrator). No DB change needed. |
| No project/goal/label taxonomy | Tasks have no categorization. | New tables if needed. Phase 2. |
| Agent auth is a no-op | Any non-op: Bearer token passes. |
Implement proper API key verification against api_key_hash. |
| No rate limiting | Proxy and API are unprotected from abuse. | Add rate limiting middleware. Phase 2. |
CONTROLPLANE_SHARED_SECRET unused |
Dead config var. | Remove from config.ts or repurpose. |
6. Proxy Architecture
Route Mounting
The proxy lives in src/dashboard-proxy.ts and mounts in controlplane-api.ts alongside existing routes:
/api/auth/* → Better Auth handler
/api/controlplane/* → Existing controlplane API
/api/paperclip/* → Dashboard proxy (NEW)
/dashboard/* → Static SPA files
The Paperclip UI's API client uses base path /api. At build time, either:
- Rewrite the base path to
/api/paperclipvia Vite env variable (VITE_API_BASE_URL), or - Mount the proxy at
/apiand move existing controlplane routes to a different prefix (disruptive).
Recommendation: Option 1. Set VITE_API_BASE_URL=/api/paperclip in the Paperclip UI's .env before building.
Auth Flow
The proxy reuses the same auth middleware as the controlplane API:
local_trustedmode: no auth required on any proxy route.authenticatedmode: Better Auth session cookie required.- Agent bearer tokens (
op:scheme) also accepted for machine access.
The proxy injects companyId: "clawdie" automatically — the UI's path parameter is accepted but ignored.
Data Flow
Paperclip UI
│
│ GET /api/paperclip/companies/clawdie/issues
▼
dashboard-proxy.ts
│
│ Translate params, call internal DB/API
▼
controlplane-db.ts (direct query, bypasses HTTP)
│
│ Raw rows
▼
dashboard-proxy.ts
│
│ Reshape rows → Paperclip Issue[] format
▼
Paperclip UI
The proxy queries the DB directly (same Pool instance) rather than making HTTP calls to its own API. This avoids double-serialization and keeps latency minimal.
7. Implementation Order
Build order for src/dashboard-proxy.ts:
- Skeleton — mount
/api/paperclip/*routes, 501 catch-all, auth middleware reuse. - Agent list —
GET /companies/:id/agents→ queryagents+agent_budgets→ reshape to PaperclipAgent[]. Most complex due to field synthesis. - Task queue —
GET /companies/:id/issues→ querytasks→ reshape to PaperclipIssue[]. Simple field mapping. - Approvals —
GET /companies/:id/approvals→ queryapprovals→ reshape to flat PaperclipApproval[]. Trivial. - Activity log —
GET /companies/:id/activity→ queryagent_activity→ reshape. Requires the newGET /activityendpoint or direct DB query. - Error handling — graceful 501 for unimplemented endpoints, proper 404 for missing entities, 401/403 for auth failures.
Each step can be tested independently with curl against the proxy routes.
8. Suggested DB Migration (Pre-Proxy)
Run before building the proxy, ideally in the next FreeBSD agent session:
-- Add updated_at to tasks
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW();
-- Add updated_at to approvals
ALTER TABLE approvals ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW();
ALTER TABLE approvals ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ;
-- Add updated_at to agents
ALTER TABLE agents ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW();
-- Index for activity queries (used by proxy)
CREATE INDEX IF NOT EXISTS idx_agent_activity_agent_created
ON agent_activity (agent_id, created_at DESC);
-- Index for task status filtering
CREATE INDEX IF NOT EXISTS idx_tasks_assigned_status
ON tasks (assigned_to, status);
9. References
doc/DASHBOARD-PHASE-D-HANDOFF.md— Phase D integration plandoc/DASHBOARD-FEASIBILITY.md— Phase A/B/C results (all green)doc/CONTROLPLANE-MESSAGE-CONTRACT.md— Clawdie API shapesdoc/CONTROLPLANE-ARCHITECTURE.md— Service architecturesrc/controlplane-api.ts— Existing API routessrc/controlplane-db.ts— DB schema and queries- Paperclip upstream:
ui/src/api/*.ts(UI client),server/src/routes/*.ts(server routes),packages/shared/src/types/*.ts(type definitions)