@versdotsh/reef 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +45 -0
- package/.github/workflows/test.yml +1 -26
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/services/board/index.ts +0 -155
- package/services/board/routes.ts +0 -335
- package/services/board/store.ts +0 -329
- package/services/board/tools.ts +0 -214
- package/services/feed/behaviors.ts +0 -23
- package/services/feed/index.ts +0 -117
- package/services/feed/routes.ts +0 -224
- package/services/feed/store.ts +0 -194
- package/services/feed/tools.ts +0 -83
- package/services/ui/auth.ts +0 -61
- package/services/ui/index.ts +0 -16
- package/services/ui/routes.ts +0 -160
- package/services/ui/static/app.js +0 -369
- package/services/ui/static/index.html +0 -42
- package/services/ui/static/style.css +0 -157
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
id-token: write
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- uses: oven-sh/setup-bun@v2
|
|
20
|
+
with:
|
|
21
|
+
bun-version: latest
|
|
22
|
+
|
|
23
|
+
- run: bun install
|
|
24
|
+
|
|
25
|
+
- run: bun test
|
|
26
|
+
|
|
27
|
+
publish:
|
|
28
|
+
needs: test
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
|
|
34
|
+
- uses: actions/setup-node@v4
|
|
35
|
+
with:
|
|
36
|
+
node-version: "24"
|
|
37
|
+
registry-url: "https://registry.npmjs.org"
|
|
38
|
+
|
|
39
|
+
- uses: oven-sh/setup-bun@v2
|
|
40
|
+
with:
|
|
41
|
+
bun-version: latest
|
|
42
|
+
|
|
43
|
+
- run: bun install
|
|
44
|
+
|
|
45
|
+
- run: npm publish --access public
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
name: test
|
|
1
|
+
name: test
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
@@ -6,10 +6,6 @@ on:
|
|
|
6
6
|
pull_request:
|
|
7
7
|
branches: [main]
|
|
8
8
|
|
|
9
|
-
permissions:
|
|
10
|
-
id-token: write
|
|
11
|
-
contents: read
|
|
12
|
-
|
|
13
9
|
jobs:
|
|
14
10
|
test:
|
|
15
11
|
runs-on: ubuntu-latest
|
|
@@ -24,24 +20,3 @@ jobs:
|
|
|
24
20
|
- run: bun install
|
|
25
21
|
|
|
26
22
|
- run: bun test
|
|
27
|
-
|
|
28
|
-
publish:
|
|
29
|
-
needs: test
|
|
30
|
-
runs-on: ubuntu-latest
|
|
31
|
-
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
|
32
|
-
|
|
33
|
-
steps:
|
|
34
|
-
- uses: actions/checkout@v4
|
|
35
|
-
|
|
36
|
-
- uses: actions/setup-node@v4
|
|
37
|
-
with:
|
|
38
|
-
node-version: "24"
|
|
39
|
-
registry-url: "https://registry.npmjs.org"
|
|
40
|
-
|
|
41
|
-
- uses: oven-sh/setup-bun@v2
|
|
42
|
-
with:
|
|
43
|
-
bun-version: latest
|
|
44
|
-
|
|
45
|
-
- run: bun install
|
|
46
|
-
|
|
47
|
-
- run: npm publish --access public
|
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.6
|
|
4
|
+
|
|
5
|
+
- Remove board, feed, and ui from `services/` — they were shipping as defaults but belong in `examples/services/`
|
|
6
|
+
- `services/` now only contains core infrastructure: agent, docs, installer, services
|
|
7
|
+
|
|
3
8
|
## 0.1.5
|
|
4
9
|
|
|
5
10
|
- Move updater service to `examples/services/` (not core infrastructure)
|
|
11
|
+
- Split CI: tests run on every push to main, publish only triggers on `v*` tags
|
|
12
|
+
- Trusted publishing via OIDC — no npm tokens in CI
|
|
6
13
|
|
|
7
14
|
## 0.1.4
|
|
8
15
|
|
package/package.json
CHANGED
package/services/board/index.ts
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Board service module — shared task tracking for agent fleets.
|
|
3
|
-
*
|
|
4
|
-
* Emits server-side events:
|
|
5
|
-
* board:task_created — { task }
|
|
6
|
-
* board:task_updated — { task, changes }
|
|
7
|
-
* board:task_deleted — { taskId }
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { ServiceModule, ServiceContext, FleetClient } from "../src/core/types.js";
|
|
11
|
-
import type { ServiceEventBus } from "../src/core/events.js";
|
|
12
|
-
import { BoardStore } from "./store.js";
|
|
13
|
-
import { createRoutes } from "./routes.js";
|
|
14
|
-
import { registerTools } from "./tools.js";
|
|
15
|
-
|
|
16
|
-
const store = new BoardStore();
|
|
17
|
-
|
|
18
|
-
// Late-bound reference — filled by init(), used by routes
|
|
19
|
-
let events: ServiceEventBus | null = null;
|
|
20
|
-
export function getEvents(): ServiceEventBus | null { return events; }
|
|
21
|
-
|
|
22
|
-
const board: ServiceModule = {
|
|
23
|
-
name: "board",
|
|
24
|
-
description: "Shared task tracking",
|
|
25
|
-
routes: createRoutes(store, () => events),
|
|
26
|
-
store,
|
|
27
|
-
registerTools,
|
|
28
|
-
|
|
29
|
-
init(ctx: ServiceContext) {
|
|
30
|
-
events = ctx.events;
|
|
31
|
-
},
|
|
32
|
-
|
|
33
|
-
routeDocs: {
|
|
34
|
-
"POST /tasks": {
|
|
35
|
-
summary: "Create a new task",
|
|
36
|
-
body: {
|
|
37
|
-
title: { type: "string", required: true, description: "Task title" },
|
|
38
|
-
description: { type: "string", description: "Detailed description" },
|
|
39
|
-
status: { type: "string", description: "Initial status: open | in_progress | in_review | blocked | done. Default: open" },
|
|
40
|
-
assignee: { type: "string", description: "Agent or user to assign to" },
|
|
41
|
-
tags: { type: "string[]", description: "Tags for categorization" },
|
|
42
|
-
dependencies: { type: "string[]", description: "IDs of tasks this depends on" },
|
|
43
|
-
createdBy: { type: "string", required: true, description: "Who created this task" },
|
|
44
|
-
},
|
|
45
|
-
response: "The created task with generated ID, timestamps, empty notes/artifacts, and score 0",
|
|
46
|
-
},
|
|
47
|
-
"GET /tasks": {
|
|
48
|
-
summary: "List tasks with optional filters",
|
|
49
|
-
query: {
|
|
50
|
-
status: { type: "string", description: "Filter by status: open | in_progress | in_review | blocked | done" },
|
|
51
|
-
assignee: { type: "string", description: "Filter by assignee" },
|
|
52
|
-
tag: { type: "string", description: "Filter by tag" },
|
|
53
|
-
},
|
|
54
|
-
response: "{ tasks: Task[], count: number }",
|
|
55
|
-
},
|
|
56
|
-
"GET /tasks/:id": {
|
|
57
|
-
summary: "Get a single task by ID",
|
|
58
|
-
params: { id: { type: "string", required: true, description: "Task ID (ULID)" } },
|
|
59
|
-
response: "The full task object including notes and artifacts",
|
|
60
|
-
},
|
|
61
|
-
"PATCH /tasks/:id": {
|
|
62
|
-
summary: "Update a task",
|
|
63
|
-
params: { id: { type: "string", required: true, description: "Task ID" } },
|
|
64
|
-
body: {
|
|
65
|
-
title: { type: "string", description: "New title" },
|
|
66
|
-
description: { type: "string", description: "New description" },
|
|
67
|
-
status: { type: "string", description: "New status" },
|
|
68
|
-
assignee: { type: "string | null", description: "New assignee, or null to unassign" },
|
|
69
|
-
tags: { type: "string[]", description: "Replace tags" },
|
|
70
|
-
},
|
|
71
|
-
response: "The updated task object",
|
|
72
|
-
},
|
|
73
|
-
"DELETE /tasks/:id": {
|
|
74
|
-
summary: "Delete a task",
|
|
75
|
-
params: { id: { type: "string", required: true, description: "Task ID" } },
|
|
76
|
-
response: "{ ok: true }",
|
|
77
|
-
},
|
|
78
|
-
"POST /tasks/:id/bump": {
|
|
79
|
-
summary: "Bump a task's priority score by 1",
|
|
80
|
-
detail: "Use to signal importance or upvote. Score is displayed in the dashboard.",
|
|
81
|
-
params: { id: { type: "string", required: true, description: "Task ID" } },
|
|
82
|
-
response: "The updated task with incremented score",
|
|
83
|
-
},
|
|
84
|
-
"POST /tasks/:id/notes": {
|
|
85
|
-
summary: "Add a note to a task",
|
|
86
|
-
params: { id: { type: "string", required: true, description: "Task ID" } },
|
|
87
|
-
body: {
|
|
88
|
-
author: { type: "string", required: true, description: "Who wrote the note" },
|
|
89
|
-
content: { type: "string", required: true, description: "Note content" },
|
|
90
|
-
type: { type: "string", required: true, description: "finding | blocker | question | update" },
|
|
91
|
-
},
|
|
92
|
-
response: "The created note with generated ID and timestamp",
|
|
93
|
-
},
|
|
94
|
-
"GET /tasks/:id/notes": {
|
|
95
|
-
summary: "Get all notes for a task",
|
|
96
|
-
params: { id: { type: "string", required: true, description: "Task ID" } },
|
|
97
|
-
response: "{ notes: Note[] }",
|
|
98
|
-
},
|
|
99
|
-
"POST /tasks/:id/artifacts": {
|
|
100
|
-
summary: "Attach artifacts to a task",
|
|
101
|
-
params: { id: { type: "string", required: true, description: "Task ID" } },
|
|
102
|
-
body: {
|
|
103
|
-
artifacts: { type: "Artifact[]", required: true, description: "Array of { type, url, label, addedBy? }. Type: branch | report | deploy | diff | file | url" },
|
|
104
|
-
},
|
|
105
|
-
response: "The updated task with new artifacts appended",
|
|
106
|
-
},
|
|
107
|
-
"POST /tasks/:id/review": {
|
|
108
|
-
summary: "Submit a task for review",
|
|
109
|
-
detail: "Sets status to in_review, adds a summary note, and optionally attaches artifacts.",
|
|
110
|
-
params: { id: { type: "string", required: true, description: "Task ID" } },
|
|
111
|
-
body: {
|
|
112
|
-
summary: { type: "string", required: true, description: "Review summary describing what was done" },
|
|
113
|
-
reviewedBy: { type: "string", required: true, description: "Who is submitting for review" },
|
|
114
|
-
artifacts: { type: "Artifact[]", description: "Artifacts to attach" },
|
|
115
|
-
},
|
|
116
|
-
response: "The updated task in in_review status",
|
|
117
|
-
},
|
|
118
|
-
"POST /tasks/:id/approve": {
|
|
119
|
-
summary: "Approve a task in review",
|
|
120
|
-
params: { id: { type: "string", required: true, description: "Task ID" } },
|
|
121
|
-
response: "The task moved to done status",
|
|
122
|
-
},
|
|
123
|
-
"POST /tasks/:id/reject": {
|
|
124
|
-
summary: "Reject a task in review, sending it back to in_progress",
|
|
125
|
-
params: { id: { type: "string", required: true, description: "Task ID" } },
|
|
126
|
-
body: {
|
|
127
|
-
reason: { type: "string", description: "Reason for rejection" },
|
|
128
|
-
},
|
|
129
|
-
response: "The task moved back to in_progress status",
|
|
130
|
-
},
|
|
131
|
-
"GET /review": {
|
|
132
|
-
summary: "List all tasks currently in review",
|
|
133
|
-
response: "{ tasks: Task[], count: number }",
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
widget: {
|
|
138
|
-
async getLines(client: FleetClient) {
|
|
139
|
-
try {
|
|
140
|
-
const res = await client.api<{ tasks: { status: string }[]; count: number }>(
|
|
141
|
-
"GET",
|
|
142
|
-
"/board/tasks",
|
|
143
|
-
);
|
|
144
|
-
const open = res.tasks.filter((t) => t.status === "open").length;
|
|
145
|
-
const inProgress = res.tasks.filter((t) => t.status === "in_progress").length;
|
|
146
|
-
const blocked = res.tasks.filter((t) => t.status === "blocked").length;
|
|
147
|
-
return [`Board: ${open} open, ${inProgress} in-progress, ${blocked} blocked`];
|
|
148
|
-
} catch {
|
|
149
|
-
return [];
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
export default board;
|
package/services/board/routes.ts
DELETED
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Board HTTP routes.
|
|
3
|
-
*
|
|
4
|
-
* Receives a store instance and a lazy event bus getter.
|
|
5
|
-
* Emits board:task_created, board:task_updated, board:task_deleted.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { Hono } from "hono";
|
|
9
|
-
import type { BoardStore, TaskFilters, TaskStatus, AddArtifactInput } from "./store.js";
|
|
10
|
-
import { NotFoundError, ValidationError } from "./store.js";
|
|
11
|
-
import type { ServiceEventBus } from "../src/core/events.js";
|
|
12
|
-
|
|
13
|
-
export function createRoutes(
|
|
14
|
-
store: BoardStore,
|
|
15
|
-
getEvents: () => ServiceEventBus | null = () => null,
|
|
16
|
-
): Hono {
|
|
17
|
-
const routes = new Hono();
|
|
18
|
-
|
|
19
|
-
// Create a task
|
|
20
|
-
routes.post("/tasks", async (c) => {
|
|
21
|
-
try {
|
|
22
|
-
const body = await c.req.json();
|
|
23
|
-
const task = store.createTask(body);
|
|
24
|
-
getEvents()?.fire("board:task_created", { task });
|
|
25
|
-
return c.json(task, 201);
|
|
26
|
-
} catch (e) {
|
|
27
|
-
if (e instanceof ValidationError) return c.json({ error: e.message }, 400);
|
|
28
|
-
throw e;
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// List tasks
|
|
33
|
-
routes.get("/tasks", (c) => {
|
|
34
|
-
const filters: TaskFilters = {};
|
|
35
|
-
const status = c.req.query("status");
|
|
36
|
-
const assignee = c.req.query("assignee");
|
|
37
|
-
const tag = c.req.query("tag");
|
|
38
|
-
|
|
39
|
-
if (status) filters.status = status as TaskStatus;
|
|
40
|
-
if (assignee) filters.assignee = assignee;
|
|
41
|
-
if (tag) filters.tag = tag;
|
|
42
|
-
|
|
43
|
-
const tasks = store.listTasks(filters);
|
|
44
|
-
return c.json({ tasks, count: tasks.length });
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
// Get a single task
|
|
48
|
-
routes.get("/tasks/:id", (c) => {
|
|
49
|
-
const task = store.getTask(c.req.param("id"));
|
|
50
|
-
if (!task) return c.json({ error: "task not found" }, 404);
|
|
51
|
-
return c.json(task);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// Update a task
|
|
55
|
-
routes.patch("/tasks/:id", async (c) => {
|
|
56
|
-
try {
|
|
57
|
-
const body = await c.req.json();
|
|
58
|
-
const task = store.updateTask(c.req.param("id"), body);
|
|
59
|
-
getEvents()?.fire("board:task_updated", { task, changes: body });
|
|
60
|
-
return c.json(task);
|
|
61
|
-
} catch (e) {
|
|
62
|
-
if (e instanceof NotFoundError) return c.json({ error: e.message }, 404);
|
|
63
|
-
if (e instanceof ValidationError) return c.json({ error: e.message }, 400);
|
|
64
|
-
throw e;
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
// Delete a task
|
|
69
|
-
routes.delete("/tasks/:id", (c) => {
|
|
70
|
-
const deleted = store.deleteTask(c.req.param("id"));
|
|
71
|
-
if (!deleted) return c.json({ error: "task not found" }, 404);
|
|
72
|
-
getEvents()?.fire("board:task_deleted", { taskId: c.req.param("id") });
|
|
73
|
-
return c.json({ deleted: true });
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
// Bump a task's score
|
|
77
|
-
routes.post("/tasks/:id/bump", (c) => {
|
|
78
|
-
try {
|
|
79
|
-
const task = store.bumpTask(c.req.param("id"));
|
|
80
|
-
return c.json(task);
|
|
81
|
-
} catch (e) {
|
|
82
|
-
if (e instanceof NotFoundError) return c.json({ error: e.message }, 404);
|
|
83
|
-
throw e;
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// Add a note to a task
|
|
88
|
-
routes.post("/tasks/:id/notes", async (c) => {
|
|
89
|
-
try {
|
|
90
|
-
const body = await c.req.json();
|
|
91
|
-
const note = store.addNote(c.req.param("id"), body);
|
|
92
|
-
return c.json(note, 201);
|
|
93
|
-
} catch (e) {
|
|
94
|
-
if (e instanceof NotFoundError) return c.json({ error: e.message }, 404);
|
|
95
|
-
if (e instanceof ValidationError) return c.json({ error: e.message }, 400);
|
|
96
|
-
throw e;
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// Get notes for a task
|
|
101
|
-
routes.get("/tasks/:id/notes", (c) => {
|
|
102
|
-
try {
|
|
103
|
-
const notes = store.getNotes(c.req.param("id"));
|
|
104
|
-
return c.json({ notes, count: notes.length });
|
|
105
|
-
} catch (e) {
|
|
106
|
-
if (e instanceof NotFoundError) return c.json({ error: e.message }, 404);
|
|
107
|
-
throw e;
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// Add artifacts to a task
|
|
112
|
-
routes.post("/tasks/:id/artifacts", async (c) => {
|
|
113
|
-
try {
|
|
114
|
-
const body = await c.req.json();
|
|
115
|
-
store.addArtifacts(c.req.param("id"), body.artifacts);
|
|
116
|
-
const task = store.getTask(c.req.param("id"));
|
|
117
|
-
return c.json(task, 201);
|
|
118
|
-
} catch (e) {
|
|
119
|
-
if (e instanceof NotFoundError) return c.json({ error: e.message }, 404);
|
|
120
|
-
if (e instanceof ValidationError) return c.json({ error: e.message }, 400);
|
|
121
|
-
throw e;
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
// Submit a task for review
|
|
126
|
-
routes.post("/tasks/:id/review", async (c) => {
|
|
127
|
-
try {
|
|
128
|
-
const body = await c.req.json();
|
|
129
|
-
const id = c.req.param("id");
|
|
130
|
-
|
|
131
|
-
if (!body.summary?.trim()) {
|
|
132
|
-
return c.json({ error: "summary is required" }, 400);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
store.updateTask(id, { status: "in_review" });
|
|
136
|
-
|
|
137
|
-
const author = body.reviewedBy?.trim() || "unknown";
|
|
138
|
-
store.addNote(id, { author, content: body.summary.trim(), type: "update" });
|
|
139
|
-
|
|
140
|
-
if (body.artifacts?.length) {
|
|
141
|
-
const artifacts = body.artifacts.map((a: AddArtifactInput) => ({
|
|
142
|
-
...a,
|
|
143
|
-
addedBy: a.addedBy || author,
|
|
144
|
-
}));
|
|
145
|
-
store.addArtifacts(id, artifacts);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const task = store.getTask(id);
|
|
149
|
-
getEvents()?.fire("board:task_updated", { task, changes: { status: "in_review" } });
|
|
150
|
-
return c.json(task);
|
|
151
|
-
} catch (e) {
|
|
152
|
-
if (e instanceof NotFoundError) return c.json({ error: e.message }, 404);
|
|
153
|
-
if (e instanceof ValidationError) return c.json({ error: e.message }, 400);
|
|
154
|
-
throw e;
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// Approve a reviewed task
|
|
159
|
-
routes.post("/tasks/:id/approve", async (c) => {
|
|
160
|
-
try {
|
|
161
|
-
const body = await c.req.json();
|
|
162
|
-
const id = c.req.param("id");
|
|
163
|
-
const approvedBy = body.approvedBy?.trim() || "unknown";
|
|
164
|
-
const comment = body.comment?.trim() || "";
|
|
165
|
-
|
|
166
|
-
store.updateTask(id, { status: "done" });
|
|
167
|
-
store.addNote(id, {
|
|
168
|
-
author: approvedBy,
|
|
169
|
-
content: comment ? `Approved by ${approvedBy}: ${comment}` : `Approved by ${approvedBy}`,
|
|
170
|
-
type: "update",
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
const task = store.getTask(id);
|
|
174
|
-
getEvents()?.fire("board:task_updated", { task, changes: { status: "done" } });
|
|
175
|
-
return c.json(task);
|
|
176
|
-
} catch (e) {
|
|
177
|
-
if (e instanceof NotFoundError) return c.json({ error: e.message }, 404);
|
|
178
|
-
if (e instanceof ValidationError) return c.json({ error: e.message }, 400);
|
|
179
|
-
throw e;
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
// Reject a reviewed task
|
|
184
|
-
routes.post("/tasks/:id/reject", async (c) => {
|
|
185
|
-
try {
|
|
186
|
-
const body = await c.req.json();
|
|
187
|
-
const id = c.req.param("id");
|
|
188
|
-
|
|
189
|
-
if (!body.reason?.trim()) return c.json({ error: "reason is required" }, 400);
|
|
190
|
-
|
|
191
|
-
const rejectedBy = body.rejectedBy?.trim() || "unknown";
|
|
192
|
-
store.updateTask(id, { status: "open" });
|
|
193
|
-
store.addNote(id, {
|
|
194
|
-
author: rejectedBy,
|
|
195
|
-
content: `Rejected by ${rejectedBy}: ${body.reason.trim()}`,
|
|
196
|
-
type: "update",
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
const task = store.getTask(id);
|
|
200
|
-
getEvents()?.fire("board:task_updated", { task, changes: { status: "open" } });
|
|
201
|
-
return c.json(task);
|
|
202
|
-
} catch (e) {
|
|
203
|
-
if (e instanceof NotFoundError) return c.json({ error: e.message }, 404);
|
|
204
|
-
if (e instanceof ValidationError) return c.json({ error: e.message }, 400);
|
|
205
|
-
throw e;
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// List tasks in review
|
|
210
|
-
routes.get("/review", (c) => {
|
|
211
|
-
const tasks = store.listTasks({ status: "in_review" });
|
|
212
|
-
tasks.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
213
|
-
return c.json({ tasks, count: tasks.length });
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
// ─── UI Panel ───
|
|
217
|
-
|
|
218
|
-
routes.get("/_panel", (c) => {
|
|
219
|
-
return c.html(`
|
|
220
|
-
<style>
|
|
221
|
-
.panel-board { padding: 8px; }
|
|
222
|
-
.panel-board .status-group { margin-bottom: 12px; }
|
|
223
|
-
.panel-board .status-label {
|
|
224
|
-
font-size: 10px; text-transform: uppercase; letter-spacing: 1px;
|
|
225
|
-
padding: 4px 8px; color: var(--text-dim, #666); display: flex; align-items: center; gap: 8px;
|
|
226
|
-
}
|
|
227
|
-
.panel-board .status-label .count {
|
|
228
|
-
background: var(--bg-card, #1a1a1a); padding: 1px 6px; border-radius: 3px; font-size: 10px;
|
|
229
|
-
}
|
|
230
|
-
.panel-board .task-card {
|
|
231
|
-
background: var(--bg-card, #1a1a1a); border: 1px solid var(--border, #2a2a2a);
|
|
232
|
-
border-radius: 4px; padding: 10px 12px; margin: 4px 0; cursor: pointer;
|
|
233
|
-
transition: border-color 0.15s;
|
|
234
|
-
}
|
|
235
|
-
.panel-board .task-card:hover { border-color: #444; }
|
|
236
|
-
.panel-board .task-card .title { color: var(--text-bright, #eee); font-weight: 500; margin-bottom: 4px; }
|
|
237
|
-
.panel-board .task-card .meta {
|
|
238
|
-
font-size: 11px; color: var(--text-dim, #666); display: flex; gap: 12px; flex-wrap: wrap;
|
|
239
|
-
}
|
|
240
|
-
.panel-board .task-card .tag {
|
|
241
|
-
background: #222; padding: 1px 6px; border-radius: 3px; font-size: 10px; color: var(--blue, #5af);
|
|
242
|
-
}
|
|
243
|
-
.panel-board .task-card .assignee { color: var(--purple, #a7f); }
|
|
244
|
-
.panel-board .task-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; }
|
|
245
|
-
.panel-board .bump-btn {
|
|
246
|
-
background: none; border: 1px solid var(--border, #2a2a2a); border-radius: 3px;
|
|
247
|
-
color: var(--text-dim, #666); cursor: pointer; font-size: 11px; padding: 2px 6px;
|
|
248
|
-
font-family: inherit; transition: all 0.15s;
|
|
249
|
-
}
|
|
250
|
-
.panel-board .bump-btn:hover { border-color: var(--accent, #4f9); color: var(--accent, #4f9); }
|
|
251
|
-
.panel-board .score { font-weight: 700; color: var(--yellow, #fd0); }
|
|
252
|
-
.panel-board .score.dim { color: var(--text-dim, #666); font-weight: 400; }
|
|
253
|
-
.panel-board .notes { margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--border, #2a2a2a); font-size: 11px; display: none; }
|
|
254
|
-
.panel-board .task-card.expanded .notes { display: block; }
|
|
255
|
-
.panel-board .note { padding: 2px 0; color: var(--text-dim, #666); }
|
|
256
|
-
.panel-board .note-author { color: var(--purple, #a7f); }
|
|
257
|
-
.panel-board .note-type { color: var(--orange, #f93); font-size: 10px; }
|
|
258
|
-
.panel-board .status-open { border-left: 3px solid var(--blue, #5af); }
|
|
259
|
-
.panel-board .status-in_progress { border-left: 3px solid var(--yellow, #fd0); }
|
|
260
|
-
.panel-board .status-in_review { border-left: 3px solid var(--orange, #f93); }
|
|
261
|
-
.panel-board .status-blocked { border-left: 3px solid var(--red, #f55); }
|
|
262
|
-
.panel-board .status-done { border-left: 3px solid var(--accent, #4f9); opacity: 0.6; }
|
|
263
|
-
.panel-board .empty { color: var(--text-dim, #666); font-style: italic; padding: 20px; text-align: center; }
|
|
264
|
-
</style>
|
|
265
|
-
|
|
266
|
-
<div class="panel-board" id="board-root">
|
|
267
|
-
<div class="empty">Loading board…</div>
|
|
268
|
-
</div>
|
|
269
|
-
|
|
270
|
-
<script>
|
|
271
|
-
(function() {
|
|
272
|
-
const root = document.getElementById('board-root');
|
|
273
|
-
const API = typeof PANEL_API !== 'undefined' ? PANEL_API : '/ui/api';
|
|
274
|
-
const ORDER = ['open', 'in_progress', 'in_review', 'blocked', 'done'];
|
|
275
|
-
|
|
276
|
-
function esc(s) { const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }
|
|
277
|
-
function ago(iso) {
|
|
278
|
-
const ms = Date.now() - new Date(iso).getTime();
|
|
279
|
-
if (ms < 60000) return Math.floor(ms/1000) + 's ago';
|
|
280
|
-
if (ms < 3600000) return Math.floor(ms/60000) + 'm ago';
|
|
281
|
-
if (ms < 86400000) return Math.floor(ms/3600000) + 'h ago';
|
|
282
|
-
return Math.floor(ms/86400000) + 'd ago';
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
async function load() {
|
|
286
|
-
try {
|
|
287
|
-
const res = await fetch(API + '/board/tasks');
|
|
288
|
-
if (!res.ok) throw new Error(res.status);
|
|
289
|
-
const data = await res.json();
|
|
290
|
-
render(data.tasks || []);
|
|
291
|
-
} catch (e) {
|
|
292
|
-
root.innerHTML = '<div class="empty">Board unavailable: ' + esc(e.message) + '</div>';
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function render(tasks) {
|
|
297
|
-
const grouped = {};
|
|
298
|
-
ORDER.forEach(s => grouped[s] = []);
|
|
299
|
-
tasks.forEach(t => (grouped[t.status] || grouped.open).push(t));
|
|
300
|
-
|
|
301
|
-
let html = '';
|
|
302
|
-
for (const status of ORDER) {
|
|
303
|
-
const items = grouped[status];
|
|
304
|
-
if (!items.length) continue;
|
|
305
|
-
html += '<div class="status-group"><div class="status-label">'
|
|
306
|
-
+ status.replace(/_/g, ' ') + ' <span class="count">' + items.length + '</span></div>';
|
|
307
|
-
for (const t of items) {
|
|
308
|
-
const tags = (t.tags||[]).map(tag => '<span class="tag">' + esc(tag) + '</span>').join('');
|
|
309
|
-
const assignee = t.assignee ? '<span class="assignee">@' + esc(t.assignee) + '</span>' : '';
|
|
310
|
-
const notes = (t.notes||[]).map(n =>
|
|
311
|
-
'<div class="note"><span class="note-author">@' + esc(n.author) + '</span> <span class="note-type">' + esc(n.type) + '</span> ' + esc(n.content) + '</div>'
|
|
312
|
-
).join('');
|
|
313
|
-
const score = t.score || 0;
|
|
314
|
-
html += '<div class="task-card status-' + status + '" onclick="this.classList.toggle(\\'expanded\\')">'
|
|
315
|
-
+ '<div class="task-top"><div class="title">' + esc(t.title) + '</div>'
|
|
316
|
-
+ '<button class="bump-btn" onclick="event.stopPropagation();fetch(\\'' + API + '/board/tasks/' + t.id + '/bump\\',{method:\\'POST\\'}).then(()=>window._boardLoad())"><span class="score' + (score ? '' : ' dim') + '">' + score + '</span></button></div>'
|
|
317
|
-
+ '<div class="meta">' + assignee + tags + '<span>' + ago(t.createdAt) + '</span></div>'
|
|
318
|
-
+ (notes ? '<div class="notes">' + notes + '</div>' : '')
|
|
319
|
-
+ '</div>';
|
|
320
|
-
}
|
|
321
|
-
html += '</div>';
|
|
322
|
-
}
|
|
323
|
-
root.innerHTML = html || '<div class="empty">No tasks</div>';
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
window._boardLoad = load;
|
|
327
|
-
load();
|
|
328
|
-
setInterval(load, 10000);
|
|
329
|
-
})();
|
|
330
|
-
</script>
|
|
331
|
-
`);
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
return routes;
|
|
335
|
-
}
|