@versdotsh/reef 0.1.4 → 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.
@@ -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 & publish
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,5 +1,16 @@
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
+
8
+ ## 0.1.5
9
+
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
13
+
3
14
  ## 0.1.4
4
15
 
5
16
  - Add updater service — auto-update reef from npm
@@ -18,7 +18,7 @@ import { Hono } from "hono";
18
18
  import { execSync, spawn } from "node:child_process";
19
19
  import { readFileSync } from "node:fs";
20
20
  import { join } from "node:path";
21
- import type { ServiceModule, ServiceContext } from "../../src/core/types.js";
21
+ import type { ServiceModule, ServiceContext } from "../src/core/types.js";
22
22
 
23
23
  interface UpdateRecord {
24
24
  from: string;
@@ -1,5 +1,5 @@
1
1
  import { describe, test, expect, afterAll } from "bun:test";
2
- import { createTestHarness, type TestHarness } from "../../src/core/testing.js";
2
+ import { createTestHarness, type TestHarness } from "../../../src/core/testing.js";
3
3
  import updater from "./index.js";
4
4
 
5
5
  let t: TestHarness;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@versdotsh/reef",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Self-improving fleet infrastructure — the minimum kernel agents need to build their own tools",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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;
@@ -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
- }