Skip to content

Commit 352a238

Browse files
Merge pull request #185 from PetalNet/feat/console-work
feat(console): build Work surface
2 parents 28d0a7f + ec6dda6 commit 352a238

10 files changed

Lines changed: 667 additions & 67 deletions

File tree

apps/console/src/lib/api/client.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import { env } from "$env/dynamic/public";
1313

1414
import type {
15+
AttentionItem,
1516
ApiError,
1617
BoxUpdateItem,
1718
BoxUpdateRaw,
@@ -22,6 +23,7 @@ import type {
2223
DeliveryItem,
2324
ExecutorItem,
2425
HeartbeatItem,
26+
LeaseItem,
2527
EdgeRegistryItem,
2628
EdgeSessionItem,
2729
Me,
@@ -30,6 +32,7 @@ import type {
3032
QueryResult,
3133
StructuredQuery,
3234
SubscriptionItem,
35+
TaskItem,
3336
} from "./types";
3437

3538
export type DataMode = "mock" | "live";
@@ -131,6 +134,40 @@ export async function readHeartbeats(
131134
});
132135
return json<ReadEnvelope<HeartbeatItem>>(res);
133136
}
137+
export async function readTasks(fetchFn: typeof fetch = fetch): Promise<ReadEnvelope<TaskItem>> {
138+
let cursor: string | null = null;
139+
let first: ReadEnvelope<TaskItem> | null = null;
140+
const items: TaskItem[] = [];
141+
do {
142+
// Cursor pagination is sequential: each request depends on the previous cursor.
143+
// oxlint-disable-next-line no-await-in-loop
144+
const res: Response = await fetchFn(
145+
`${base()}/tasks?limit=250${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`,
146+
{
147+
headers: { accept: "application/json" },
148+
credentials: "include",
149+
},
150+
);
151+
// oxlint-disable-next-line no-await-in-loop
152+
const page: ReadEnvelope<TaskItem> = await json<ReadEnvelope<TaskItem>>(res);
153+
first ??= page;
154+
items.push(...page.items);
155+
cursor = page.next_cursor;
156+
} while (cursor);
157+
return {
158+
...(first as ReadEnvelope<TaskItem>),
159+
items,
160+
next_cursor: null,
161+
total: first?.total ?? items.length,
162+
};
163+
}
164+
export async function readLeases(fetchFn: typeof fetch = fetch): Promise<ReadEnvelope<LeaseItem>> {
165+
const res = await fetchFn(`${base()}/leases?limit=1000`, {
166+
headers: { accept: "application/json" },
167+
credentials: "include",
168+
});
169+
return json<ReadEnvelope<LeaseItem>>(res);
170+
}
134171

135172
/** POST /api/v1/query — scope-filtered structured statistics query, run as the caller. */
136173
export async function runQuery(
@@ -212,6 +249,15 @@ export async function readCards(fetchFn: typeof fetch = fetch): Promise<ReadEnve
212249
});
213250
return json<ReadEnvelope<CardItem>>(res);
214251
}
252+
export async function readAttention(
253+
fetchFn: typeof fetch = fetch,
254+
): Promise<ReadEnvelope<AttentionItem>> {
255+
const res = await fetchFn(`${base()}/attention?limit=1000`, {
256+
headers: { accept: "application/json" },
257+
credentials: "include",
258+
});
259+
return json<ReadEnvelope<AttentionItem>>(res);
260+
}
215261
export async function readHealth(fetchFn: typeof fetch = fetch): Promise<ConsoleHealth> {
216262
const res = await fetchFn(`${base()}/health`, { headers: { accept: "application/json" } });
217263
return json<ConsoleHealth>(res);

apps/console/src/lib/api/ops.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,33 @@ const OPS = {
8888
undo: false,
8989
humanOnly: false,
9090
},
91+
"task.claim": {
92+
op: "task.claim",
93+
verb: "Claim",
94+
lane: "operator",
95+
executor: "tracker",
96+
confirm: "none",
97+
undo: false,
98+
humanOnly: false,
99+
},
100+
"task.update": {
101+
op: "task.update",
102+
verb: "Update",
103+
lane: "operator",
104+
executor: "tracker",
105+
confirm: "none",
106+
undo: false,
107+
humanOnly: false,
108+
},
109+
"task.close": {
110+
op: "task.close",
111+
verb: "Close",
112+
lane: "operator",
113+
executor: "tracker",
114+
confirm: "none",
115+
undo: false,
116+
humanOnly: false,
117+
},
91118
"governance.action": {
92119
op: "governance.action",
93120
verb: "Pause",

apps/console/src/lib/api/types.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,46 @@ export interface HeartbeatItem extends Extra {
144144
host: string;
145145
observed_at: string;
146146
}
147+
export type TaskStatus = "inbox" | "todo" | "doing" | "blocked" | "review" | "done" | "dropped";
148+
export interface TaskItem extends Extra {
149+
id: number;
150+
kind: string;
151+
title: string;
152+
body?: string;
153+
status: TaskStatus;
154+
priority: 0 | 1 | 2 | 3;
155+
project_id?: number | null;
156+
project_title?: string | null;
157+
parent_id?: number | null;
158+
assignee?: string;
159+
claimed_by?: string;
160+
lease_expires_at?: string | null;
161+
blocked_on?: string;
162+
verification_status?: "unverified" | "verified" | "rejected";
163+
suggested_agent?: string;
164+
effort?: string;
165+
parallel_group?: string;
166+
rank?: number;
167+
up_next?: boolean;
168+
owner?: string;
169+
visibility?: "shared" | "private";
170+
handoff_context?: string;
171+
acceptance_criteria?: string;
172+
result_summary?: string;
173+
close_reason?: string;
174+
created_at: string;
175+
updated_at: string;
176+
capability?: string;
177+
}
178+
export interface LeaseItem extends Extra {
179+
schema_version: 1;
180+
task_id: number;
181+
worker: string;
182+
fence?: number;
183+
granted_at?: string;
184+
lease_expires_at: string;
185+
lease_seconds?: number;
186+
}
147187

148188
// ---- accounting (POST /query, GET /catalog, GET /dashboards) ----
149189
export type QueryColumnType = "string" | "number" | "boolean" | "timestamp" | "json";

apps/console/src/lib/components/Countdown.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
{#if reaped}
3333
<span class="cd danger">reaped</span>
3434
{:else if remainingMs != null}
35-
<span class="cd {tone}">{fmt(remainingMs)}</span>
35+
<span class="cd {tone}">{remainingMs <= 0 ? "expired · awaiting reap" : fmt(remainingMs)}</span>
3636
{/if}
3737

3838
<style>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script lang="ts">
2+
let { priority }: { priority: 0 | 1 | 2 | 3 } = $props();
3+
const filled = $derived(3 - priority);
4+
</script>
5+
<span class="priority p{priority}" aria-label={`Priority P${priority}`}><span class="pips" aria-hidden="true">{#each [0, 1, 2] as pip}<i class:filled={pip < filled}></i>{/each}</span><b>P{priority}</b></span>
6+
<style>
7+
.priority{display:inline-flex;align-items:center;gap:var(--s-1);font:500 .6875rem var(--mono);color:var(--text-3)}.pips{display:inline-flex;align-items:flex-end;gap:2px}.pips i{width:2px;height:8px;border-radius:1px;background:var(--rule-strong)}.pips i.filled{background:var(--text-3)}.p0,.p0 .pips i.filled{color:var(--danger-text);background-color:transparent}.p0 .pips i.filled{background:var(--danger-dot)}.p1,.p1 .pips i.filled{color:var(--warn-text)}.p1 .pips i.filled{background:var(--warn-dot)}
8+
</style>

apps/console/src/lib/components/SurfaceStub.svelte

Lines changed: 0 additions & 64 deletions
This file was deleted.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script lang="ts">
2+
import Icon from "./Icon.svelte";
3+
let { status = "unverified", word = true }: { status?: "unverified" | "verified" | "rejected"; word?: boolean } = $props();
4+
const icon = $derived(status === "verified" ? "circle-check" : status === "rejected" ? "circle-x" : "circle-dashed");
5+
</script>
6+
<span class="verification {status}" title={status} aria-label={`Verification: ${status}`}><Icon name={icon} size={12}/>{#if word}<span>{status}</span>{/if}</span>
7+
<style>
8+
.verification{display:inline-flex;align-items:center;gap:var(--s-1);font:500 .6875rem var(--mono);color:var(--text-3)}.verified{color:var(--good-text)}.rejected{color:var(--danger-text)}
9+
</style>

0 commit comments

Comments
 (0)