// Field-level help registry for dashboard entity detail pages. // // FIELD_HELP maps a field key to { label, description, doc? }. // label — human-readable field name (used as bold heading in help-tip) // description — one sentence explaining the field // doc — optional anchor into /docs/ pages for "Learn more" // // fieldRow(key, value) → element with a help-tip-decorated key cell and // a value cell. Falls back to a plain key cell when key is not in FIELD_HELP. // // Usage: // import {fieldRow} from "./components/field-help.js"; // const tbody = html`${fields.map(([k,v]) => fieldRow(k,v))}`; import {HelpTip} from "./help-tip.js"; // ensures custom element is registered import {API} from "./config.js"; void HelpTip; // silence unused-import linters // ── Entity link registry ──────────────────────────────────────────────────── // Maps FK field names to fetch/URL/title resolution rules. // getUrl receives (id, data) so slug-routed entities (repos) can use data.slug. const FIELD_LINKS = { task_id: { apiUrl: id => `${API}/tasks/${id}`, getUrl: (id, _d) => `/tasks/${id}`, getTitle: d => d.title, }, workstream_id: { apiUrl: id => `${API}/workstreams/${id}`, getUrl: (id, _d) => `/workstreams/${id}`, getTitle: d => d.title || d.slug, }, repo_id: { apiUrl: id => `${API}/repos/by-id/${id}`, getUrl: (_id, d) => `/repos/${d.slug}`, getTitle: d => d.name || d.slug, }, }; /** * Render an entity-reference value as a link with an async help-tip showing * the entity title. Falls back gracefully if the fetch fails. */ function _linkCell(key, id) { const rule = FIELD_LINKS[key]; const shortId = String(id).slice(0, 8) + "…"; const a = document.createElement("a"); a.textContent = shortId; a.href = "#"; a.style.fontFamily = "var(--monospace, monospace)"; a.title = String(id); // full UUID as native tooltip while async loads fetch(rule.apiUrl(id)) .then(r => r.ok ? r.json() : null) .then(data => { if (!data) return; const title = rule.getTitle(data); const url = rule.getUrl(id, data); if (url) a.href = url; if (title) { const tip = document.createElement("help-tip"); tip.setAttribute("label", title); tip.setAttribute("description", `${key.replace(/_id$/, "")} · ${id}`); a.replaceWith(tip); tip.appendChild(a); } }) .catch(() => { /* leave plain link */ }); return a; } export const FIELD_HELP = { // ── TokenEvent ───────────────────────────────────────────────────────────── id: { label: "ID", description: "Unique identifier for this token event (UUID v4).", doc: "/docs/reference#token-events", }, tokens_in: { label: "Tokens In", description: "Number of input (prompt) tokens consumed in this event.", doc: "/docs/reference#token-events", }, tokens_out: { label: "Tokens Out", description: "Number of output (completion) tokens generated in this event.", doc: "/docs/reference#token-events", }, tokens_total: { label: "Tokens Total", description: "Sum of input and output tokens — total cost proxy for this event.", doc: "/docs/reference#token-events", }, task_id: { label: "Task ID", description: "The task this token event was recorded against (if any).", doc: "/docs/tasks", }, workstream_id: { label: "Workstream ID", description: "The workstream this event belongs to; auto-resolved from task if not set directly.", doc: "/docs/workstreams", }, repo_id: { label: "Repo ID", description: "The managed repo this event is attributed to; auto-resolved from workstream.", doc: "/docs/repos", }, session_id: { label: "Session ID", description: "Opaque identifier for the Claude Code session that produced this event.", }, model: { label: "Model", description: "The Claude model used (e.g. claude-sonnet-4-6).", doc: "/docs/reference#models", }, agent: { label: "Agent", description: "The agent persona that produced this event (e.g. custodian, ralph).", doc: "/docs/reference#agents", }, ref_type: { label: "Ref Type", description: "What kind of entity the ref_id points to: task, commit, release, or session.", doc: "/docs/reference#token-events", }, ref_id: { label: "Ref ID", description: "Free-form reference ID; interpretation depends on ref_type.", doc: "/docs/reference#token-events", }, note: { label: "Note", description: "Quality tag: 'measured' = exact from status bar, 'userbased' = human-provided, 'workplan' = prorated, 'heuristic' = server fallback.", doc: "/docs/reference#token-note-taxonomy", }, created_at: { label: "Created At", description: "Timestamp when this token event was recorded (UTC).", }, // ── Workstream ────────────────────────────────────────────────────────────── slug: { label: "Slug", description: "URL-safe short identifier for this entity.", }, title: { label: "Title", description: "Human-readable name for this workstream or task.", }, status: { label: "Status", description: "Current lifecycle state: todo, in_progress, blocked, done, or cancelled.", doc: "/docs/workstream-lifecycle", }, topic_id: { label: "Topic ID", description: "The topic this workstream is grouped under.", doc: "/docs/reference#topics", }, repo_goal_id: { label: "Repo Goal ID", description: "Optional link to a repo-level strategic goal this workstream advances.", doc: "/docs/goals", }, // ── Task ─────────────────────────────────────────────────────────────────── assignee: { label: "Assignee", description: "Who is responsible for completing this task (agent name or human).", }, priority: { label: "Priority", description: "Relative urgency: high, medium, or low.", }, due_date: { label: "Due Date", description: "Target completion date (ISO 8601).", }, needs_human: { label: "Needs Human", description: "True if the task is blocked waiting for human input or approval.", doc: "/interventions", }, intervention_note: { label: "Intervention Note", description: "Why human intervention is required for this task.", }, // ── Repo ─────────────────────────────────────────────────────────────────── repo_slug: { label: "Repo Slug", description: "Short identifier for the repository (matches the git remote slug).", doc: "/docs/repos", }, event_count: { label: "Event Count", description: "Total number of token events attributed to this entity.", }, by_model: { label: "By Model", description: "Token totals broken down by Claude model.", }, by_note: { label: "By Note", description: "Token totals broken down by quality tier (measured / workplan / heuristic).", doc: "/docs/reference#token-note-taxonomy", }, }; /** * Render a single key-value row for an entity detail table. * @param {string} key — field name * @param {*} value — field value (stringified automatically) * @returns {HTMLTableRowElement} */ export function fieldRow(key, value) { const tr = document.createElement("tr"); // Key cell const tdKey = document.createElement("td"); tdKey.style.cssText = "padding:0.3rem 0.8rem 0.3rem 0; white-space:nowrap; vertical-align:top; color:var(--theme-foreground-muted,#666); font-size:0.82rem;"; const help = FIELD_HELP[key]; if (help) { const tip = document.createElement("help-tip"); tip.setAttribute("label", help.label); tip.setAttribute("description", help.description); if (help.doc) tip.setAttribute("doc", help.doc); tip.textContent = help.label; tdKey.appendChild(tip); } else { tdKey.textContent = key; } tr.appendChild(tdKey); // Value cell const tdVal = document.createElement("td"); tdVal.style.cssText = "padding:0.3rem 0; font-size:0.82rem; word-break:break-all; vertical-align:top;"; let display; if (value === null || value === undefined) { display = document.createElement("span"); display.style.color = "var(--theme-foreground-faint,#aaa)"; display.textContent = "—"; } else if (key in FIELD_LINKS) { display = _linkCell(key, value); } else if (typeof value === "object") { display = document.createElement("pre"); display.style.cssText = "margin:0; font-size:0.75rem; white-space:pre-wrap;"; display.textContent = JSON.stringify(value, null, 2); } else { display = document.createElement("span"); display.textContent = String(value); } tdVal.appendChild(display); tr.appendChild(tdVal); return tr; }