2026-03-29 22:35:35 +02:00
|
|
|
---
|
|
|
|
|
title: Task
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
import {API} from "../components/config.js";
|
|
|
|
|
import {fieldRow} from "../components/field-help.js";
|
2026-05-19 02:16:24 +02:00
|
|
|
import {statusControl, TASK_STATUSES} from "../components/status-control.js";
|
2026-03-29 22:35:35 +02:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
const taskId = observable.params.id;
|
|
|
|
|
const raw = await fetch(`${API}/tasks/${taskId}`)
|
|
|
|
|
.then(r => r.ok ? r.json() : r.json().then(e => ({error: e.detail ?? `HTTP ${r.status}`})))
|
|
|
|
|
.catch(e => ({error: String(e)}));
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
if (raw.error) {
|
|
|
|
|
display(html`<div style="color:red;padding:1rem">⚠️ ${raw.error}</div>`);
|
|
|
|
|
} else {
|
|
|
|
|
const name = raw.title || taskId;
|
|
|
|
|
const shortName = name.length > 60 ? name.slice(0, 60) + "…" : name;
|
|
|
|
|
display(html`<h1 style="font-size:1.1rem;margin-bottom:0.25rem">Task · <em>${shortName}</em></h1>`);
|
|
|
|
|
display(html`<p style="margin-top:0"><a href="/tasks">← Tasks</a> | <a href="/token-cost">← Token Cost</a></p>`);
|
2026-05-19 02:16:24 +02:00
|
|
|
display(html`<div class="task-summary">
|
|
|
|
|
<div>
|
|
|
|
|
<span>Status</span>
|
|
|
|
|
${statusControl({
|
|
|
|
|
entity: raw,
|
|
|
|
|
type: "task",
|
|
|
|
|
statuses: TASK_STATUSES,
|
|
|
|
|
onSaved: () => setTimeout(() => location.reload(), 450),
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
<div><span>Priority</span><strong>${raw.priority ?? "—"}</strong></div>
|
|
|
|
|
<div><span>Assignee</span><strong>${raw.assignee ?? "—"}</strong></div>
|
|
|
|
|
</div>`);
|
2026-03-29 22:35:35 +02:00
|
|
|
|
|
|
|
|
const FIELD_ORDER = [
|
|
|
|
|
"id","title","status","priority","assignee",
|
|
|
|
|
"workstream_id","due_date","needs_human","intervention_note",
|
|
|
|
|
"created_at","updated_at",
|
|
|
|
|
];
|
2026-05-19 02:16:24 +02:00
|
|
|
const HIDDEN_FIELDS = ["description"];
|
|
|
|
|
|
|
|
|
|
const taskContent = (raw.description ?? "").trim();
|
|
|
|
|
if (taskContent) {
|
|
|
|
|
display(html`<section class="task-content">
|
|
|
|
|
<h2>Task Content</h2>
|
|
|
|
|
<div class="task-content-body">${taskContent}</div>
|
|
|
|
|
</section>`);
|
|
|
|
|
} else {
|
|
|
|
|
display(html`<p class="task-content-empty">No task content recorded.</p>`);
|
|
|
|
|
}
|
2026-03-29 22:35:35 +02:00
|
|
|
|
|
|
|
|
const rows = FIELD_ORDER.map(k => fieldRow(k, raw[k] ?? null));
|
|
|
|
|
for (const k of Object.keys(raw)) {
|
2026-05-19 02:16:24 +02:00
|
|
|
if (!FIELD_ORDER.includes(k) && !HIDDEN_FIELDS.includes(k)) rows.push(fieldRow(k, raw[k]));
|
2026-03-29 22:35:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
display(html`<table style="border-collapse:collapse;width:100%;max-width:640px">
|
|
|
|
|
<colgroup><col style="width:160px"><col></colgroup>
|
|
|
|
|
<tbody>${rows}</tbody>
|
|
|
|
|
</table>`);
|
|
|
|
|
}
|
|
|
|
|
```
|
2026-05-19 02:16:24 +02:00
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
.task-summary {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
margin: 1rem 0 1.25rem;
|
|
|
|
|
max-width: 860px;
|
|
|
|
|
}
|
|
|
|
|
.task-summary div {
|
|
|
|
|
background: var(--theme-background-alt);
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
padding: 0.75rem 0.9rem;
|
|
|
|
|
}
|
|
|
|
|
.task-summary span {
|
|
|
|
|
display: block;
|
|
|
|
|
color: gray;
|
|
|
|
|
font-size: 0.72rem;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
margin-bottom: 0.2rem;
|
|
|
|
|
}
|
|
|
|
|
.task-summary strong {
|
|
|
|
|
overflow-wrap: anywhere;
|
|
|
|
|
}
|
|
|
|
|
.task-content {
|
|
|
|
|
margin: 1rem 0 1.25rem;
|
|
|
|
|
max-width: 860px;
|
|
|
|
|
border: 1px solid var(--theme-foreground-faint, #e5e7eb);
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
background: var(--theme-background-alt, #f8fafc);
|
|
|
|
|
}
|
|
|
|
|
.task-content h2 {
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 0.65rem 0.9rem;
|
|
|
|
|
border-bottom: 1px solid var(--theme-foreground-faint, #e5e7eb);
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
}
|
|
|
|
|
.task-content-body {
|
|
|
|
|
padding: 0.85rem 0.9rem;
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
overflow-wrap: anywhere;
|
|
|
|
|
line-height: 1.55;
|
|
|
|
|
font-size: 0.88rem;
|
|
|
|
|
}
|
|
|
|
|
.task-content-empty {
|
|
|
|
|
margin: 1rem 0;
|
|
|
|
|
color: var(--theme-foreground-muted, #666);
|
|
|
|
|
}
|
|
|
|
|
</style>
|