generated from coulomb/repo-seed
- ref-cell.js: REF column component — click=copy deeplink, dblclick=open
- field-help.js: field registry + fieldRow helper with help-tip decoration;
FK fields (task_id, workstream_id, repo_id) render as async-linked cells
with entity-title bubble-help on hover
- GET /token-events/{id} endpoint + get-by-id tests
- GET /repos/by-id/{repo_id} UUID lookup endpoint
- Landing pages: /token-events/[id], /workstreams/[id], /repos/[slug], /tasks/[id]
- token-cost.md: REF + Name columns on all three tables; parallel fetch of
workstreams/tasks for title resolution
- reference.md: entity detail page URL scheme documented
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
264 lines
9.2 KiB
JavaScript
264 lines
9.2 KiB
JavaScript
// 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) → <tr> 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`<tbody>${fields.map(([k,v]) => fieldRow(k,v))}</tbody>`;
|
|
|
|
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;
|
|
}
|