@tidegap/cli 0.0.0-stage → 0.19.0
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/LICENSE +201 -0
- package/README.md +136 -2
- package/dist/commands/clients.d.ts +20 -0
- package/dist/commands/clients.d.ts.map +1 -0
- package/dist/commands/clients.js +36 -0
- package/dist/commands/clients.js.map +1 -0
- package/dist/commands/env-file.d.ts +54 -0
- package/dist/commands/env-file.d.ts.map +1 -0
- package/dist/commands/env-file.js +153 -0
- package/dist/commands/env-file.js.map +1 -0
- package/dist/commands/install.d.ts +83 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +297 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/queue.d.ts +139 -0
- package/dist/commands/queue.d.ts.map +1 -0
- package/dist/commands/queue.js +491 -0
- package/dist/commands/queue.js.map +1 -0
- package/dist/commands/receipts.d.ts +15 -0
- package/dist/commands/receipts.d.ts.map +1 -0
- package/dist/commands/receipts.js +70 -0
- package/dist/commands/receipts.js.map +1 -0
- package/dist/commands/register-wake.d.ts +21 -0
- package/dist/commands/register-wake.d.ts.map +1 -0
- package/dist/commands/register-wake.js +76 -0
- package/dist/commands/register-wake.js.map +1 -0
- package/dist/commands/stats.d.ts +31 -0
- package/dist/commands/stats.d.ts.map +1 -0
- package/dist/commands/stats.js +165 -0
- package/dist/commands/stats.js.map +1 -0
- package/dist/commands/tick.d.ts +73 -0
- package/dist/commands/tick.d.ts.map +1 -0
- package/dist/commands/tick.js +294 -0
- package/dist/commands/tick.js.map +1 -0
- package/dist/commands/ui.d.ts +33 -0
- package/dist/commands/ui.d.ts.map +1 -0
- package/dist/commands/ui.js +119 -0
- package/dist/commands/ui.js.map +1 -0
- package/dist/commands/verify.d.ts +32 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +135 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/env.d.ts +57 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +105 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +366 -0
- package/dist/index.js.map +1 -0
- package/dist/platform/index.d.ts +30 -0
- package/dist/platform/index.d.ts.map +1 -0
- package/dist/platform/index.js +66 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/platform/linux.d.ts +81 -0
- package/dist/platform/linux.d.ts.map +1 -0
- package/dist/platform/linux.js +158 -0
- package/dist/platform/linux.js.map +1 -0
- package/dist/platform/macos.d.ts +69 -0
- package/dist/platform/macos.d.ts.map +1 -0
- package/dist/platform/macos.js +151 -0
- package/dist/platform/macos.js.map +1 -0
- package/dist/platform/windows.d.ts +31 -0
- package/dist/platform/windows.d.ts.map +1 -0
- package/dist/platform/windows.js +58 -0
- package/dist/platform/windows.js.map +1 -0
- package/dist/ui/api.d.ts +66 -0
- package/dist/ui/api.d.ts.map +1 -0
- package/dist/ui/api.js +613 -0
- package/dist/ui/api.js.map +1 -0
- package/dist/ui/clients.d.ts +65 -0
- package/dist/ui/clients.d.ts.map +1 -0
- package/dist/ui/clients.js +179 -0
- package/dist/ui/clients.js.map +1 -0
- package/dist/ui/discovery.d.ts +25 -0
- package/dist/ui/discovery.d.ts.map +1 -0
- package/dist/ui/discovery.js +46 -0
- package/dist/ui/discovery.js.map +1 -0
- package/dist/ui/dispatcher.d.ts +84 -0
- package/dist/ui/dispatcher.d.ts.map +1 -0
- package/dist/ui/dispatcher.js +150 -0
- package/dist/ui/dispatcher.js.map +1 -0
- package/dist/ui/events.d.ts +27 -0
- package/dist/ui/events.d.ts.map +1 -0
- package/dist/ui/events.js +85 -0
- package/dist/ui/events.js.map +1 -0
- package/dist/ui/server.d.ts +84 -0
- package/dist/ui/server.d.ts.map +1 -0
- package/dist/ui/server.js +322 -0
- package/dist/ui/server.js.map +1 -0
- package/dist/ui/summary.d.ts +47 -0
- package/dist/ui/summary.d.ts.map +1 -0
- package/dist/ui/summary.js +120 -0
- package/dist/ui/summary.js.map +1 -0
- package/package.json +53 -3
- package/ui/agenda.js +256 -0
- package/ui/api.js +114 -0
- package/ui/app.css +713 -0
- package/ui/app.js +599 -0
- package/ui/clients.js +104 -0
- package/ui/detail.js +431 -0
- package/ui/dialog.js +284 -0
- package/ui/dnd.js +131 -0
- package/ui/dom.js +255 -0
- package/ui/index.html +38 -0
- package/ui/journal.js +207 -0
- package/ui/keys.js +92 -0
- package/ui/row.js +153 -0
- package/ui/state.js +412 -0
- package/ui/timeline.js +202 -0
- package/ui/toast.js +69 -0
- package/ui/tokens.css +108 -0
- package/ui/views.js +31 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GET /api/summary` (native clients KTD5): the one answer a compact client
|
|
3
|
+
* renders. Queue counts, unread count, the next run, the next clean grid
|
|
4
|
+
* window and, with `since`, the tasks finished after that cursor. The clean
|
|
5
|
+
* window rule lives here once, so the extension badge and the Mac menu bar
|
|
6
|
+
* show the same numbers.
|
|
7
|
+
*/
|
|
8
|
+
import { taskSummary } from "../commands/queue.js";
|
|
9
|
+
const HOUR_MS = 3_600_000;
|
|
10
|
+
const CLEAN_BANDS = new Set(["very_clean", "clean"]);
|
|
11
|
+
/** Most finished tasks one summary lists. */
|
|
12
|
+
export const SUMMARY_FINISHED_MAX = 20;
|
|
13
|
+
export const ACTIVE_STATUSES = ["queued", "scheduled", "running", "submitted"];
|
|
14
|
+
/**
|
|
15
|
+
* The first run of consecutive clean or very clean hours that ends after
|
|
16
|
+
* `now`. Without one, the cleanest hour that ends after `now`, flagged
|
|
17
|
+
* `fallback`. `null` when the forecast has no hour after `now`.
|
|
18
|
+
*/
|
|
19
|
+
export function nextCleanWindow(points, now) {
|
|
20
|
+
const t = now.getTime();
|
|
21
|
+
const hours = points
|
|
22
|
+
.map((p) => ({ ...p, start: Date.parse(p.t) }))
|
|
23
|
+
.filter((p) => Number.isFinite(p.start))
|
|
24
|
+
.sort((a, b) => a.start - b.start);
|
|
25
|
+
const window = (run, fallback) => {
|
|
26
|
+
const start = run[0].start;
|
|
27
|
+
const end = run[run.length - 1].start + HOUR_MS;
|
|
28
|
+
return {
|
|
29
|
+
start: new Date(start).toISOString(),
|
|
30
|
+
end: new Date(end).toISOString(),
|
|
31
|
+
band: fallback ? run[0].band : run.every((h) => h.band === "very_clean") ? "very_clean" : "clean",
|
|
32
|
+
g: Math.min(...run.map((h) => h.g)),
|
|
33
|
+
now: start <= t && t < end,
|
|
34
|
+
fallback,
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
// Runs of consecutive clean hours; the first one that ends after now wins,
|
|
38
|
+
// with the hours of it that already passed (so "now" shows the whole run).
|
|
39
|
+
let run = [];
|
|
40
|
+
for (const h of hours) {
|
|
41
|
+
const prev = run[run.length - 1];
|
|
42
|
+
if (CLEAN_BANDS.has(h.band) && (!prev || h.start === prev.start + HOUR_MS)) {
|
|
43
|
+
run.push(h);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
if (run.length > 0 && run[run.length - 1].start + HOUR_MS > t)
|
|
47
|
+
return window(run, false);
|
|
48
|
+
run = CLEAN_BANDS.has(h.band) ? [h] : [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (run.length > 0 && run[run.length - 1].start + HOUR_MS > t)
|
|
52
|
+
return window(run, false);
|
|
53
|
+
const ahead = hours.filter((h) => h.start + HOUR_MS > t);
|
|
54
|
+
if (ahead.length === 0)
|
|
55
|
+
return null;
|
|
56
|
+
const cleanest = ahead.reduce((best, h) => (h.g < best.g ? h : best));
|
|
57
|
+
return window([cleanest], true);
|
|
58
|
+
}
|
|
59
|
+
function brief(r) {
|
|
60
|
+
return { task_id: r.taskId, title: String(taskSummary(r).display_title) };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* How many of `times` (ascending) one summary lists: at most
|
|
64
|
+
* SUMMARY_FINISHED_MAX, cut so that tasks sharing a `completed_at` are never
|
|
65
|
+
* split across two polls (the next `since` is that instant, and `>` would
|
|
66
|
+
* skip the rest). Only when more than the cap share the first instant does
|
|
67
|
+
* the cut grow past the cap to take all of them.
|
|
68
|
+
*/
|
|
69
|
+
function capCut(times) {
|
|
70
|
+
if (times.length <= SUMMARY_FINISHED_MAX)
|
|
71
|
+
return times.length;
|
|
72
|
+
let cut = SUMMARY_FINISHED_MAX;
|
|
73
|
+
while (cut > 0 && times[cut] === times[cut - 1])
|
|
74
|
+
cut--;
|
|
75
|
+
if (cut > 0)
|
|
76
|
+
return cut;
|
|
77
|
+
cut = SUMMARY_FINISHED_MAX;
|
|
78
|
+
while (cut < times.length && times[cut] === times[cut - 1])
|
|
79
|
+
cut++;
|
|
80
|
+
return cut;
|
|
81
|
+
}
|
|
82
|
+
/** Assemble the summary body (KTD5). */
|
|
83
|
+
export function buildSummary(input) {
|
|
84
|
+
const { store, now } = input;
|
|
85
|
+
const generatedAt = now.toISOString();
|
|
86
|
+
const counts = Object.fromEntries(ACTIVE_STATUSES.map((s) => [s, store.list({ status: s }).length]));
|
|
87
|
+
const active = ACTIVE_STATUSES.reduce((n, s) => n + counts[s], 0);
|
|
88
|
+
const upcoming = [...store.list({ status: "scheduled" }), ...store.list({ status: "queued" })]
|
|
89
|
+
.filter((r) => r.scheduledFor && Number.isFinite(Date.parse(r.scheduledFor)))
|
|
90
|
+
.sort((a, b) => Date.parse(a.scheduledFor) - Date.parse(b.scheduledFor));
|
|
91
|
+
const next = upcoming[0];
|
|
92
|
+
const body = {
|
|
93
|
+
generated_at: generatedAt,
|
|
94
|
+
region: input.region,
|
|
95
|
+
counts,
|
|
96
|
+
active,
|
|
97
|
+
unread: store.list({ unread: true }).length,
|
|
98
|
+
next_run: next ? { ...brief(next), scheduled_for: next.scheduledFor } : null,
|
|
99
|
+
clean_window: input.points ? nextCleanWindow(input.points, now) : null,
|
|
100
|
+
forecast_ok: input.points !== null,
|
|
101
|
+
};
|
|
102
|
+
if (input.since === undefined)
|
|
103
|
+
return { ...body, cursor: generatedAt };
|
|
104
|
+
const since = input.since.getTime();
|
|
105
|
+
// Over the cap, take the oldest after `since`: the cursor stops at the
|
|
106
|
+
// newest of those, so the next poll lists the rest and none is skipped.
|
|
107
|
+
const after = [...store.list({ status: "completed" }), ...store.list({ status: "failed" })]
|
|
108
|
+
.filter((r) => r.completedAt && Date.parse(r.completedAt) > since)
|
|
109
|
+
.sort((a, b) => Date.parse(a.completedAt) - Date.parse(b.completedAt));
|
|
110
|
+
const finished = after.slice(0, capCut(after.map((r) => Date.parse(r.completedAt)))).reverse();
|
|
111
|
+
return {
|
|
112
|
+
...body,
|
|
113
|
+
finished: finished.map((r) => ({ ...brief(r), status: r.status, completed_at: r.completedAt })),
|
|
114
|
+
// The server's own clock: clients never compare their clock with ours.
|
|
115
|
+
// With nothing new, keep the caller's cursor so a task stamped just
|
|
116
|
+
// before `generated_at` but written after it is still reported.
|
|
117
|
+
cursor: finished[0]?.completedAt ?? input.since.toISOString(),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=summary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"summary.js","sourceRoot":"","sources":["../../src/ui/summary.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tidegap/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.19.0",
|
|
4
|
+
"description": "The tidegap command: runs queued Tidegap LLM tasks at the cleanest grid hour before their deadline.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "Vitalii Borovyk",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20"
|
|
10
|
+
},
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"bin": {
|
|
13
|
+
"tidegap": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"ui",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"commander": "^12.1.0",
|
|
23
|
+
"@tidegap/core": "0.19.0"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@anthropic-ai/sdk": ">=0.32.0",
|
|
27
|
+
"openai": ">=4.70.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependenciesMeta": {
|
|
30
|
+
"@anthropic-ai/sdk": {
|
|
31
|
+
"optional": true
|
|
32
|
+
},
|
|
33
|
+
"openai": {
|
|
34
|
+
"optional": true
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"typescript": "^5.6.3",
|
|
39
|
+
"vitest": "^4.1.11",
|
|
40
|
+
"@types/node": "^22.20.5"
|
|
41
|
+
},
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/tidegap/tidegap.git",
|
|
45
|
+
"directory": "packages/cli"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsc -p tsconfig.json && chmod +x dist/index.js",
|
|
52
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
53
|
+
"test": "vitest run",
|
|
54
|
+
"clean": "rm -rf dist"
|
|
55
|
+
}
|
|
6
56
|
}
|
package/ui/agenda.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// Agenda: the default view. Pending tasks grouped by local day (Today,
|
|
2
|
+
// Tomorrow, weekday + date, Later), with overdue tasks and unread failures
|
|
3
|
+
// in "Needs attention" on top, and projected rows for upcoming occurrences
|
|
4
|
+
// of repeating series. Day headers are drop targets (dnd.js).
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
by,
|
|
8
|
+
bandChip,
|
|
9
|
+
dayLabel,
|
|
10
|
+
dayStart,
|
|
11
|
+
daysFromToday,
|
|
12
|
+
h,
|
|
13
|
+
hm,
|
|
14
|
+
nextDayStart,
|
|
15
|
+
parse,
|
|
16
|
+
relDay,
|
|
17
|
+
when,
|
|
18
|
+
} from "./dom.js";
|
|
19
|
+
import { projectedRow, shortDay, statusTag, taskRow } from "./row.js";
|
|
20
|
+
import { finishedTasks, pendingTasks, state } from "./state.js";
|
|
21
|
+
import { emptyState, errorCard, skeletonRows } from "./views.js";
|
|
22
|
+
|
|
23
|
+
const PROJECT_DAYS = 14;
|
|
24
|
+
const LATER = "later";
|
|
25
|
+
|
|
26
|
+
/** Ids the user sent "Run now" from this page; they read "runs on the next tick". */
|
|
27
|
+
export const ranNow = new Set();
|
|
28
|
+
|
|
29
|
+
/** Seconds between dispatcher ticks, from /api/status. */
|
|
30
|
+
function tickInterval() {
|
|
31
|
+
return state.status?.dispatcher?.interval_s ?? 300;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function dispatcherRunning() {
|
|
35
|
+
return state.status?.dispatcher?.state === "running";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** When a task runs, in ms: its scheduled hour, else its deadline. */
|
|
39
|
+
export function runAt(t) {
|
|
40
|
+
const at = parse(t.scheduled_for);
|
|
41
|
+
return Number.isFinite(at) ? at : parse(t.deadline);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Pending, due, but no tick picked it up in two intervals (or none runs). */
|
|
45
|
+
function isOverdue(t, now = Date.now()) {
|
|
46
|
+
if (t.status !== "queued" && t.status !== "scheduled") return false;
|
|
47
|
+
if (t._op || ranNow.has(t.task_id)) return false;
|
|
48
|
+
const at = runAt(t);
|
|
49
|
+
if (!Number.isFinite(at) || at > now) return false;
|
|
50
|
+
return !dispatcherRunning() || now - at > 2 * tickInterval() * 1000;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isDue(t, now) {
|
|
54
|
+
const at = runAt(t);
|
|
55
|
+
return (t.status === "queued" || t.status === "scheduled") && Number.isFinite(at) && at <= now;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Region whose forecast colours the day headers: the most common pending one. */
|
|
59
|
+
function primaryRegion(tasks) {
|
|
60
|
+
const counts = new Map();
|
|
61
|
+
for (const t of tasks) counts.set(t.region, (counts.get(t.region) || 0) + 1);
|
|
62
|
+
let best = state.status?.default_region;
|
|
63
|
+
let n = 0;
|
|
64
|
+
for (const [r, c] of counts) if (c > n) [best, n] = [r, c];
|
|
65
|
+
return best;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Cleanest forecast hour in [from, to) for `region`, or null. */
|
|
69
|
+
export function cleanest(region, from, to) {
|
|
70
|
+
const f = state.forecasts.get(region);
|
|
71
|
+
if (!f || f.points.length === 0) return null;
|
|
72
|
+
let best = null;
|
|
73
|
+
for (const p of f.points) {
|
|
74
|
+
const t = Date.parse(p.t);
|
|
75
|
+
if (t + 3_600_000 <= from || t >= to) continue;
|
|
76
|
+
if (!best || p.g < best.g) best = { ...p, ms: t };
|
|
77
|
+
}
|
|
78
|
+
return best;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function dayHeader(key, label, sub, count, region, now) {
|
|
82
|
+
const isDay = key !== LATER;
|
|
83
|
+
const start = isDay ? Number(key) : NaN;
|
|
84
|
+
const attrs = { class: "day-head" };
|
|
85
|
+
if (isDay) {
|
|
86
|
+
attrs["data-drop-day"] = String(start);
|
|
87
|
+
const rel = relDay(start, now);
|
|
88
|
+
// Weekday names already are the date ("Sat 17 Oct"); only Today and
|
|
89
|
+
// Tomorrow need it added.
|
|
90
|
+
attrs["data-drop-label"] = rel === dayLabel(start) ? rel : `${rel} · ${dayLabel(start)}`;
|
|
91
|
+
}
|
|
92
|
+
const head = h("div", attrs,
|
|
93
|
+
h("h2", { class: "day-title" }, label, sub ? h("span", { class: "day-sub" }, ` · ${sub}`) : null),
|
|
94
|
+
count ? h("span", { class: "day-count", "aria-label": `${count} tasks` }, String(count)) : null);
|
|
95
|
+
if (isDay) {
|
|
96
|
+
const best = cleanest(region, Math.max(start, now), nextDayStart(start));
|
|
97
|
+
const forecastLoaded = state.forecasts.has(region);
|
|
98
|
+
head.append(
|
|
99
|
+
best
|
|
100
|
+
? h("span", { class: "day-clean" }, "cleanest ", h("span", { class: "num" }, hm(best.ms)), " ", bandChip(best.band))
|
|
101
|
+
: forecastLoaded
|
|
102
|
+
? h("span", { class: "day-clean muted" }, "forecast not available yet")
|
|
103
|
+
: null,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
head.append(h("span", { class: "drop-hint", "aria-hidden": "true" }, isDay ? `Move to ${relDay(start, now)}` : ""));
|
|
107
|
+
return head;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function agendaMeta(t, now) {
|
|
111
|
+
const dl = parse(t.deadline);
|
|
112
|
+
const parts = [];
|
|
113
|
+
if (t.status === "running" || t.status === "submitted") parts.push(statusTag(t));
|
|
114
|
+
if (ranNow.has(t.task_id) && isDue(t, now)) parts.push(h("span", { class: "tag tag-accent" }, "runs on the next tick"));
|
|
115
|
+
else if (isOverdue(t, now)) parts.push(h("span", { class: "tag tag-warn" }, `overdue since ${when(runAt(t), now)}`));
|
|
116
|
+
else if (isDue(t, now)) parts.push(h("span", { class: "tag tag-accent" }, "due · runs on the next tick"));
|
|
117
|
+
if (Number.isFinite(dl)) parts.push(by(dl, now));
|
|
118
|
+
if (t.origin_host) parts.push(t.origin_host);
|
|
119
|
+
return parts;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function timeLabel(t, key, now) {
|
|
123
|
+
if (isDue(t, now) || t.status === "running") return "now";
|
|
124
|
+
if (t.status === "submitted") return "batch";
|
|
125
|
+
const at = runAt(t);
|
|
126
|
+
if (key === LATER) return shortDay(at);
|
|
127
|
+
return hm(at);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function renderAgenda(root, { selectedId } = {}) {
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
const header = h("div", { class: "view-head" }, h("h1", { class: "view-title" }, "Agenda"));
|
|
133
|
+
root.append(header);
|
|
134
|
+
|
|
135
|
+
if (!state.loaded) {
|
|
136
|
+
root.append(state.error ? errorCard("Could not load tasks.") : skeletonGroups());
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (state.error) root.append(errorCard("Could not refresh tasks. Showing the last data."));
|
|
140
|
+
|
|
141
|
+
const pend = pendingTasks();
|
|
142
|
+
const attention = [];
|
|
143
|
+
const upcoming = [];
|
|
144
|
+
for (const t of pend) (isOverdue(t, now) ? attention : upcoming).push(t);
|
|
145
|
+
const failed = finishedTasks()
|
|
146
|
+
.filter((t) => t.status === "failed" && !t.read_at)
|
|
147
|
+
.sort((a, b) => parse(b.completed_at) - parse(a.completed_at));
|
|
148
|
+
|
|
149
|
+
const nextRun = upcoming.map(runAt).filter((x) => Number.isFinite(x) && x > now).sort((a, b) => a - b)[0];
|
|
150
|
+
header.append(
|
|
151
|
+
h("p", { class: "view-sub" },
|
|
152
|
+
upcoming.length === 0
|
|
153
|
+
? "Nothing scheduled"
|
|
154
|
+
: `${upcoming.length} scheduled${nextRun ? ` · next run ${when(nextRun, now)}` : ""}`),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (attention.length || failed.length) {
|
|
158
|
+
const section = h("section", { class: "group group-attention", "aria-label": "Needs attention" },
|
|
159
|
+
h("div", { class: "day-head day-head-attention" },
|
|
160
|
+
h("h2", { class: "day-title" }, "Needs attention"),
|
|
161
|
+
h("span", { class: "day-count" }, String(attention.length + failed.length))));
|
|
162
|
+
const list = h("ul", { class: "rows" });
|
|
163
|
+
for (const t of attention.sort((a, b) => runAt(a) - runAt(b))) {
|
|
164
|
+
list.append(taskRow(t, {
|
|
165
|
+
ctx: "attention",
|
|
166
|
+
time: "▲ " + hm(runAt(t)),
|
|
167
|
+
meta: agendaMeta(t, now),
|
|
168
|
+
selected: t.task_id === selectedId,
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
for (const t of failed) {
|
|
172
|
+
list.append(taskRow(t, {
|
|
173
|
+
ctx: "attention",
|
|
174
|
+
time: "✕ failed",
|
|
175
|
+
band: null,
|
|
176
|
+
meta: [`failed ${when(parse(t.completed_at), now)}`, t.origin_host].filter(Boolean),
|
|
177
|
+
selected: t.task_id === selectedId,
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
section.append(list);
|
|
181
|
+
root.append(section);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Group upcoming tasks and projected series occurrences by local day.
|
|
185
|
+
const groups = new Map();
|
|
186
|
+
const put = (key, item) => {
|
|
187
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
188
|
+
groups.get(key).push(item);
|
|
189
|
+
};
|
|
190
|
+
const keyOf = (ms) => {
|
|
191
|
+
const at = Number.isFinite(ms) ? Math.max(ms, now) : now;
|
|
192
|
+
return daysFromToday(at, now) > 6 ? LATER : String(dayStart(at));
|
|
193
|
+
};
|
|
194
|
+
for (const t of upcoming) put(keyOf(runAt(t)), { at: Math.max(runAt(t) || now, 0), task: t });
|
|
195
|
+
for (const s of state.series.values()) {
|
|
196
|
+
if (s.ended || !s.next_deadlines) continue;
|
|
197
|
+
const head = s.occurrences?.[s.occurrences.length - 1];
|
|
198
|
+
if (!head || !pend.some((t) => t.series_id === s.series_id)) continue;
|
|
199
|
+
for (const iso of s.next_deadlines) {
|
|
200
|
+
const ms = parse(iso);
|
|
201
|
+
if (!Number.isFinite(ms) || ms - now > PROJECT_DAYS * 86_400_000) break;
|
|
202
|
+
put(keyOf(ms - 3_600_000), { at: ms - 3_600_000, projected: { ...s, head }, deadline: ms });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (upcoming.length === 0) {
|
|
207
|
+
const journalCount = finishedTasks().length;
|
|
208
|
+
root.append(
|
|
209
|
+
emptyState({
|
|
210
|
+
title: "Nothing scheduled.",
|
|
211
|
+
body: [
|
|
212
|
+
"Defer work from Claude Code with ",
|
|
213
|
+
h("code", {}, "/tidegap:defer <task> --by 6h"),
|
|
214
|
+
", or from any MCP client with ",
|
|
215
|
+
h("code", {}, "schedule_task"),
|
|
216
|
+
". It shows up here with the hour tidegap picked.",
|
|
217
|
+
],
|
|
218
|
+
link: journalCount ? { href: "#/journal", text: `${journalCount} finished in the Journal →` } : null,
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
if (groups.size === 0) return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const region = primaryRegion(pend);
|
|
225
|
+
const keys = [...groups.keys()].sort((a, b) => (a === LATER ? 1 : b === LATER ? -1 : Number(a) - Number(b)));
|
|
226
|
+
for (const key of keys) {
|
|
227
|
+
const items = groups.get(key).sort((a, b) => a.at - b.at);
|
|
228
|
+
const start = key === LATER ? NaN : Number(key);
|
|
229
|
+
const label = key === LATER ? "Later" : relDay(start, now);
|
|
230
|
+
const sub = key === LATER ? "" : daysFromToday(start, now) < 2 ? dayLabel(start) : "";
|
|
231
|
+
const section = h("section", { class: "group", "aria-label": key === LATER ? "Later" : `${label} ${dayLabel(start)}` });
|
|
232
|
+
section.append(dayHeader(key, key === LATER ? label : daysFromToday(start, now) < 2 ? label : dayLabel(start), sub, items.filter((i) => i.task).length, region, now));
|
|
233
|
+
const list = h("ul", { class: "rows" });
|
|
234
|
+
for (const item of items) {
|
|
235
|
+
if (item.task) {
|
|
236
|
+
list.append(taskRow(item.task, {
|
|
237
|
+
time: timeLabel(item.task, key, now),
|
|
238
|
+
meta: agendaMeta(item.task, now),
|
|
239
|
+
selected: item.task.task_id === selectedId,
|
|
240
|
+
}));
|
|
241
|
+
} else {
|
|
242
|
+
list.append(projectedRow(item.projected, item.deadline, key === LATER));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
section.append(list);
|
|
246
|
+
root.append(section);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function skeletonGroups() {
|
|
251
|
+
const wrap = h("div", { class: "skeleton-wrap", "aria-busy": "true", "aria-label": "Loading tasks" });
|
|
252
|
+
for (let g = 0; g < 3; g++) {
|
|
253
|
+
wrap.append(h("div", { class: "day-head skeleton-head" }, h("span", { class: "sk sk-title" })), skeletonRows(2));
|
|
254
|
+
}
|
|
255
|
+
return wrap;
|
|
256
|
+
}
|
package/ui/api.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// API client. The token lives in this module's memory only: it is read once
|
|
2
|
+
// from the URL fragment (never sent to the server by the browser) and every
|
|
3
|
+
// request carries it in the Authorization header. No cookie, no storage.
|
|
4
|
+
|
|
5
|
+
let token = null;
|
|
6
|
+
|
|
7
|
+
export class SessionExpired extends Error {
|
|
8
|
+
constructor() {
|
|
9
|
+
super("This link expired. Run tidegap ui again.");
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A non-2xx answer. `message` is the server's one-line error. */
|
|
14
|
+
export class ApiError extends Error {
|
|
15
|
+
constructor(status, message) {
|
|
16
|
+
super(message || `HTTP ${status}`);
|
|
17
|
+
this.status = status;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function setToken(t) {
|
|
22
|
+
token = t;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function hasToken() {
|
|
26
|
+
return !!token;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function headers(json) {
|
|
30
|
+
const out = { authorization: `Bearer ${token}` };
|
|
31
|
+
if (json) out["content-type"] = "application/json";
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Raw fetch with the bearer header; 401 becomes SessionExpired. */
|
|
36
|
+
export async function request(path, init = {}) {
|
|
37
|
+
if (!token) throw new SessionExpired();
|
|
38
|
+
const res = await fetch(path, { ...init, headers: { ...headers(init.json), ...(init.headers || {}) } });
|
|
39
|
+
if (res.status === 401) throw new SessionExpired();
|
|
40
|
+
return res;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function errorOf(res) {
|
|
44
|
+
let msg = "";
|
|
45
|
+
try {
|
|
46
|
+
const body = await res.json();
|
|
47
|
+
if (body && typeof body.error === "string") msg = body.error;
|
|
48
|
+
} catch {
|
|
49
|
+
// no JSON body
|
|
50
|
+
}
|
|
51
|
+
return new ApiError(res.status, msg);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function get(path) {
|
|
55
|
+
const res = await request(path);
|
|
56
|
+
if (!res.ok) throw await errorOf(res);
|
|
57
|
+
return res.json();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* POST a write. `body` undefined sends no body and no content type (the
|
|
62
|
+
* body-less routes refuse one); an object is sent as JSON.
|
|
63
|
+
*/
|
|
64
|
+
async function post(path, body) {
|
|
65
|
+
const init = { method: "POST" };
|
|
66
|
+
if (body !== undefined) {
|
|
67
|
+
init.json = true;
|
|
68
|
+
init.body = JSON.stringify(body);
|
|
69
|
+
}
|
|
70
|
+
const res = await request(path, init);
|
|
71
|
+
if (!res.ok) throw await errorOf(res);
|
|
72
|
+
if (res.status === 204) return null;
|
|
73
|
+
return res.json();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Fire-and-forget POST that survives page unload (pending cancels on
|
|
78
|
+
* pagehide). keepalive keeps the request alive after the page is gone.
|
|
79
|
+
*/
|
|
80
|
+
export function postKeepalive(path, body) {
|
|
81
|
+
if (!token) return;
|
|
82
|
+
try {
|
|
83
|
+
fetch(path, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
keepalive: true,
|
|
86
|
+
headers: headers(body !== undefined),
|
|
87
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
88
|
+
});
|
|
89
|
+
} catch {
|
|
90
|
+
// the page is going away; nothing to report to
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const enc = encodeURIComponent;
|
|
95
|
+
|
|
96
|
+
export const tasksPath = (id, action) => `/api/tasks/${enc(id)}${action ? `/${action}` : ""}`;
|
|
97
|
+
|
|
98
|
+
export const api = {
|
|
99
|
+
list: (query) => get(`/api/tasks${query ? `?${query}` : ""}`),
|
|
100
|
+
task: (id) => get(tasksPath(id)),
|
|
101
|
+
series: (id) => get(`/api/series/${enc(id)}`),
|
|
102
|
+
status: () => get("/api/status"),
|
|
103
|
+
forecast: (region, hours) => get(`/api/forecast?region=${enc(region)}&hours=${hours}`),
|
|
104
|
+
cancel: (id, series = false) => post(tasksPath(id, "cancel"), series ? { series: true } : {}),
|
|
105
|
+
reschedule: (id, target) => post(tasksPath(id, "reschedule"), target),
|
|
106
|
+
runNow: (id) => post(tasksPath(id, "run-now")),
|
|
107
|
+
reply: (id, text) => post(tasksPath(id, "reply"), { text }),
|
|
108
|
+
read: (id) => post(tasksPath(id, "read")),
|
|
109
|
+
unread: (id) => post(tasksPath(id, "unread")),
|
|
110
|
+
startDispatcher: () => post("/api/dispatcher/start"),
|
|
111
|
+
clients: () => get("/api/clients"),
|
|
112
|
+
addClient: (client) => post("/api/clients", client),
|
|
113
|
+
revokeClient: (id) => post(`/api/clients/${enc(id)}/revoke`),
|
|
114
|
+
};
|