taskchef 7.5.0 → 7.6.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/.codex-plugin/plugin.json +1 -1
- package/README.md +4 -0
- package/assets/codex.svg +3 -0
- package/docs/images/result-history-dashboard.jpg +0 -0
- package/package.json +1 -1
- package/src/dashboard/app.js +70 -20
- package/src/dashboard/index.html +4 -1
- package/src/dashboard/styles.css +8 -4
- package/src/dashboard/time.js +146 -0
- package/src/dashboard.js +2 -0
package/README.md
CHANGED
|
@@ -176,6 +176,10 @@ taskchef dashboard --port 3211
|
|
|
176
176
|
The loopback dashboard watches `tasks.jsonl`, groups current states, and opens
|
|
177
177
|
linked Codex tasks. List snapshots and SSE events carry only the latest-result
|
|
178
178
|
projection; opening task details fetches the full newest-first result history.
|
|
179
|
+
Task and result times are relative through 29 days (with minute detail for the
|
|
180
|
+
first six hours), then use a locale-aware calendar date. Each time is a keyboard-
|
|
181
|
+
accessible toggle for its full locale-aware date and time, and one shared
|
|
182
|
+
30-second timer keeps relative labels current without reloading the page.
|
|
179
183
|
It does not mutate TaskChef data and prints its local URL.
|
|
180
184
|
When a compatible foreground dashboard already owns port 3210,
|
|
181
185
|
`ensure_dashboard` reuses it but does not take ownership. If an unknown,
|
package/assets/codex.svg
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
|
2
|
+
<path fill="currentColor" d="M3 1.5h10A1.5 1.5 0 0 1 14.5 3v10a1.5 1.5 0 0 1-1.5 1.5H3A1.5 1.5 0 0 1 1.5 13V3A1.5 1.5 0 0 1 3 1.5Zm0 1a.5.5 0 0 0-.5.5v10a.5.5 0 0 0 .5.5h10a.5.5 0 0 0 .5-.5V3a.5.5 0 0 0-.5-.5H3Zm1.15 3.1.7-.7L7.95 8l-3.1 3.1-.7-.7L6.55 8l-2.4-2.4ZM8 10h4v1H8v-1Z"/>
|
|
3
|
+
</svg>
|
|
Binary file
|
package/package.json
CHANGED
package/src/dashboard/app.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
taskWithinDateFilter,
|
|
9
9
|
} from "./state.js";
|
|
10
10
|
import { openTaskFromControl } from "./actions.js";
|
|
11
|
+
import { RelativeTimeController, parsedTimestamp } from "./time.js";
|
|
11
12
|
|
|
12
13
|
const state = {
|
|
13
14
|
tasks: [],
|
|
@@ -18,6 +19,7 @@ const state = {
|
|
|
18
19
|
};
|
|
19
20
|
let dateRefreshTimer = null;
|
|
20
21
|
let detailRequestGeneration = 0;
|
|
22
|
+
const relativeTimes = new RelativeTimeController();
|
|
21
23
|
|
|
22
24
|
const elements = {
|
|
23
25
|
clearNotifications: document.querySelector("#clear-notifications"),
|
|
@@ -45,12 +47,42 @@ const elements = {
|
|
|
45
47
|
toastList: document.querySelector("#toast-list"),
|
|
46
48
|
};
|
|
47
49
|
|
|
48
|
-
function
|
|
49
|
-
if (!value)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
function timestampControl(value, { accessibleName, key, prefix = "" }) {
|
|
51
|
+
if (!parsedTimestamp(value)) {
|
|
52
|
+
const missing = document.createElement("span");
|
|
53
|
+
missing.className = "timestamp-missing";
|
|
54
|
+
missing.textContent = `${prefix}—`;
|
|
55
|
+
return missing;
|
|
56
|
+
}
|
|
57
|
+
const control = document.createElement("button");
|
|
58
|
+
control.type = "button";
|
|
59
|
+
control.className = "timestamp-toggle";
|
|
60
|
+
const time = document.createElement("time");
|
|
61
|
+
control.append(time);
|
|
62
|
+
relativeTimes.register(key, value, ({ exact, iso, label }) => {
|
|
63
|
+
time.dateTime = iso;
|
|
64
|
+
time.textContent = `${prefix}${label}`;
|
|
65
|
+
const action = exact ? "Show relative time" : "Show exact date and time";
|
|
66
|
+
control.title = action;
|
|
67
|
+
control.setAttribute("aria-label", `${accessibleName}: ${label}. ${action}.`);
|
|
68
|
+
}, { isActive: () => control.isConnected });
|
|
69
|
+
control.addEventListener("click", () => relativeTimes.toggle(key));
|
|
70
|
+
return control;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function codexIcon() {
|
|
74
|
+
const icon = document.createElement("span");
|
|
75
|
+
icon.className = "codex-icon";
|
|
76
|
+
icon.setAttribute("aria-hidden", "true");
|
|
77
|
+
return icon;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function configureOpenTaskControl(control, ariaLabel) {
|
|
81
|
+
control.classList.add("task-action");
|
|
82
|
+
control.setAttribute("aria-label", ariaLabel);
|
|
83
|
+
const label = document.createElement("span");
|
|
84
|
+
label.textContent = "Open task";
|
|
85
|
+
control.replaceChildren(codexIcon(), label);
|
|
54
86
|
}
|
|
55
87
|
|
|
56
88
|
function setConnection(connected) {
|
|
@@ -116,11 +148,12 @@ function detailRow(term, value) {
|
|
|
116
148
|
const dt = document.createElement("dt");
|
|
117
149
|
dt.textContent = term;
|
|
118
150
|
const dd = document.createElement("dd");
|
|
119
|
-
|
|
151
|
+
if (value instanceof Node) dd.append(value);
|
|
152
|
+
else dd.textContent = value ?? "—";
|
|
120
153
|
return [dt, dd];
|
|
121
154
|
}
|
|
122
155
|
|
|
123
|
-
function resultHistory(results) {
|
|
156
|
+
function resultHistory(taskId, results) {
|
|
124
157
|
if (results.length === 0) {
|
|
125
158
|
const empty = document.createElement("p");
|
|
126
159
|
empty.className = "result-history-empty";
|
|
@@ -135,9 +168,11 @@ function resultHistory(results) {
|
|
|
135
168
|
const status = document.createElement("span");
|
|
136
169
|
status.className = `status status-${result.status}`;
|
|
137
170
|
status.textContent = result.status.replaceAll("_", " ");
|
|
138
|
-
const
|
|
139
|
-
timestamp
|
|
140
|
-
|
|
171
|
+
const resultKey = result.turnId ?? `${result.status}:${index}`;
|
|
172
|
+
const timestamp = timestampControl(result.updatedAt, {
|
|
173
|
+
accessibleName: `Result updated time for ${result.status.replaceAll("_", " ")}`,
|
|
174
|
+
key: `detail:${taskId}:result:${resultKey}`,
|
|
175
|
+
});
|
|
141
176
|
header.append(status, timestamp);
|
|
142
177
|
const summary = document.createElement("p");
|
|
143
178
|
summary.className = "preserve-lines";
|
|
@@ -161,7 +196,7 @@ function renderDialog(task) {
|
|
|
161
196
|
state.selectedTask = detailedTask;
|
|
162
197
|
elements.dialogProject.textContent = task.project.name;
|
|
163
198
|
elements.dialogTitle.textContent = task.title;
|
|
164
|
-
elements.dialogResults.replaceChildren(...resultHistory(detailedTask.results));
|
|
199
|
+
elements.dialogResults.replaceChildren(...resultHistory(task.id, detailedTask.results));
|
|
165
200
|
elements.dialogInstruction.textContent = task.instruction;
|
|
166
201
|
elements.copyThreadId.disabled = !task.threadId;
|
|
167
202
|
elements.dialogMetadata.replaceChildren(
|
|
@@ -169,16 +204,27 @@ function renderDialog(task) {
|
|
|
169
204
|
...detailRow("Current turn ID", task.turnId),
|
|
170
205
|
...detailRow("Last result status", task.lastResult?.status?.replaceAll("_", " ")),
|
|
171
206
|
...detailRow("Last result turn ID", task.lastResult?.turnId),
|
|
172
|
-
...detailRow("Last result updated",
|
|
207
|
+
...detailRow("Last result updated", timestampControl(task.lastResult?.updatedAt, {
|
|
208
|
+
accessibleName: `Last result updated time for ${task.title}`,
|
|
209
|
+
key: `detail:${task.id}:last-result-updated`,
|
|
210
|
+
})),
|
|
173
211
|
...detailRow("Task ID", task.id),
|
|
174
212
|
...detailRow("Thread ID", task.threadId),
|
|
175
213
|
...detailRow("Project path", task.project.path),
|
|
176
|
-
...detailRow("Created",
|
|
177
|
-
|
|
214
|
+
...detailRow("Created", timestampControl(task.createdAt, {
|
|
215
|
+
accessibleName: `Created time for ${task.title}`,
|
|
216
|
+
key: `detail:${task.id}:created`,
|
|
217
|
+
})),
|
|
218
|
+
...detailRow("Updated", timestampControl(
|
|
178
219
|
task.meaningfulUpdatedAt ?? task.updatedAt ?? task.createdAt,
|
|
220
|
+
{
|
|
221
|
+
accessibleName: `Updated time for ${task.title}`,
|
|
222
|
+
key: `detail:${task.id}:updated`,
|
|
223
|
+
},
|
|
179
224
|
)),
|
|
180
225
|
...detailRow("Updated by", task.updatedBy),
|
|
181
226
|
);
|
|
227
|
+
configureOpenTaskControl(elements.openProject, `Open ${task.title} in Codex`);
|
|
182
228
|
}
|
|
183
229
|
|
|
184
230
|
async function openDialog(task) {
|
|
@@ -223,16 +269,20 @@ function taskCard(task) {
|
|
|
223
269
|
const summary = document.createElement("p");
|
|
224
270
|
summary.className = "task-summary";
|
|
225
271
|
summary.textContent = task.lastResult?.summary ?? "No semantic result reported yet.";
|
|
226
|
-
const time =
|
|
227
|
-
|
|
228
|
-
|
|
272
|
+
const time = timestampControl(
|
|
273
|
+
task.meaningfulUpdatedAt ?? task.updatedAt ?? task.createdAt,
|
|
274
|
+
{
|
|
275
|
+
accessibleName: `Updated time for ${task.title}`,
|
|
276
|
+
key: `list:${task.id}:updated`,
|
|
277
|
+
prefix: "Updated ",
|
|
278
|
+
},
|
|
279
|
+
);
|
|
229
280
|
const footer = document.createElement("div");
|
|
230
281
|
footer.className = "task-footer";
|
|
231
282
|
const openTask = document.createElement("button");
|
|
232
283
|
openTask.type = "button";
|
|
233
284
|
openTask.className = "secondary-button task-open";
|
|
234
|
-
openTask
|
|
235
|
-
openTask.setAttribute("aria-label", `Open ${task.title} in Codex`);
|
|
285
|
+
configureOpenTaskControl(openTask, `Open ${task.title} in Codex`);
|
|
236
286
|
openTask.addEventListener("click", (event) => openTaskFromControl(event, task.id, {
|
|
237
287
|
showMessage,
|
|
238
288
|
}));
|
package/src/dashboard/index.html
CHANGED
|
@@ -83,7 +83,10 @@
|
|
|
83
83
|
<button id="close-dialog" class="icon-button" type="button" aria-label="Close task details">×</button>
|
|
84
84
|
</div>
|
|
85
85
|
<div class="dialog-actions">
|
|
86
|
-
<button id="open-codex" class="primary-button" type="button"
|
|
86
|
+
<button id="open-codex" class="primary-button task-action" type="button" aria-label="Open this task in Codex">
|
|
87
|
+
<span class="codex-icon" aria-hidden="true"></span>
|
|
88
|
+
<span>Open task</span>
|
|
89
|
+
</button>
|
|
87
90
|
<button id="copy-thread-id" class="secondary-button" type="button">Copy thread ID</button>
|
|
88
91
|
</div>
|
|
89
92
|
<section>
|
package/src/dashboard/styles.css
CHANGED
|
@@ -51,7 +51,7 @@ h3 { margin-bottom: 8px; font-size: 0.84rem; letter-spacing: 0.08em; text-transf
|
|
|
51
51
|
.dashboard-icon img { display: block; width: 100%; height: 100%; object-fit: contain; }
|
|
52
52
|
|
|
53
53
|
.eyebrow { margin-bottom: 10px; color: var(--accent); font-size: 0.78rem; font-weight: 750; letter-spacing: 0.15em; text-transform: uppercase; }
|
|
54
|
-
.subtitle, .task-project, time { color: var(--muted); }
|
|
54
|
+
.subtitle, .task-project, time, .timestamp-missing { color: var(--muted); }
|
|
55
55
|
.subtitle { margin-bottom: 0; }
|
|
56
56
|
|
|
57
57
|
.connection { display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 0.9rem; }
|
|
@@ -78,9 +78,13 @@ select { min-width: 180px; padding: 9px 34px 9px 11px; border: 1px solid var(--b
|
|
|
78
78
|
.task-title:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
|
|
79
79
|
.task-project { margin: 3px 0 12px; font-size: 0.86rem; }
|
|
80
80
|
.task-summary { max-width: 78ch; margin-bottom: 14px; }
|
|
81
|
-
time { font-size: 0.78rem; }
|
|
81
|
+
time, .timestamp-missing { font-size: 0.78rem; }
|
|
82
|
+
.timestamp-toggle { padding: 2px 0; border: 0; background: none; cursor: pointer; text-align: left; }
|
|
83
|
+
.timestamp-toggle:hover time { color: var(--text); text-decoration: underline; text-underline-offset: 3px; }
|
|
82
84
|
.task-footer { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
|
83
85
|
.task-open { flex: none; }
|
|
86
|
+
.task-action { display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-height: 32px; padding: 5px 9px; font-size: 0.82rem; line-height: 1.2; white-space: nowrap; }
|
|
87
|
+
.codex-icon { display: inline-block; flex: none; width: 16px; height: 16px; background: currentColor; mask: url("/assets/codex.svg") center / contain no-repeat; }
|
|
84
88
|
|
|
85
89
|
.status { flex: none; padding: 4px 9px; border-radius: 999px; background: var(--surface-muted); color: var(--muted); font-size: 0.72rem; font-weight: 750; text-transform: capitalize; }
|
|
86
90
|
.status-completed { background: var(--accent-soft); color: var(--accent); }
|
|
@@ -142,8 +146,8 @@ pre { max-height: 280px; margin: 0; padding: 14px; overflow: auto; border-radius
|
|
|
142
146
|
.task-count { margin: 0; }
|
|
143
147
|
.task-heading { align-items: flex-start; }
|
|
144
148
|
.task-footer { align-items: flex-start; flex-direction: column; }
|
|
145
|
-
.task-open {
|
|
146
|
-
.dialog-actions { align-items:
|
|
149
|
+
.task-open { align-self: flex-start; }
|
|
150
|
+
.dialog-actions { align-items: center; flex-flow: row wrap; }
|
|
147
151
|
.metadata { grid-template-columns: 1fr; }
|
|
148
152
|
.metadata dt { padding-bottom: 0; border-bottom: 0; }
|
|
149
153
|
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
export const RELATIVE_TIME_REFRESH_MS = 30_000;
|
|
2
|
+
export const RELATIVE_DATE_LIMIT_DAYS = 30;
|
|
3
|
+
|
|
4
|
+
const MINUTE_MS = 60_000;
|
|
5
|
+
const HOUR_MS = 60 * MINUTE_MS;
|
|
6
|
+
const DAY_MS = 24 * HOUR_MS;
|
|
7
|
+
|
|
8
|
+
export function parsedTimestamp(value) {
|
|
9
|
+
if (typeof value !== "string" || value.trim() === "") return null;
|
|
10
|
+
const milliseconds = Date.parse(value);
|
|
11
|
+
if (!Number.isFinite(milliseconds)) return null;
|
|
12
|
+
return {
|
|
13
|
+
date: new Date(milliseconds),
|
|
14
|
+
iso: new Date(milliseconds).toISOString(),
|
|
15
|
+
milliseconds,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function formatExactTime(value, { locale, timeZone } = {}) {
|
|
20
|
+
const parsed = parsedTimestamp(value);
|
|
21
|
+
if (!parsed) return "—";
|
|
22
|
+
return new Intl.DateTimeFormat(locale, {
|
|
23
|
+
dateStyle: "medium",
|
|
24
|
+
timeStyle: "short",
|
|
25
|
+
timeZone,
|
|
26
|
+
}).format(parsed.date);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function hourPhrase(milliseconds, direction) {
|
|
30
|
+
const hours = Math.floor(milliseconds / HOUR_MS);
|
|
31
|
+
const minutes = Math.floor((milliseconds % HOUR_MS) / MINUTE_MS);
|
|
32
|
+
const hourText = hours === 1 ? "1 hour" : `${hours} hours`;
|
|
33
|
+
const detail = hours < 6 && minutes > 0 ? ` ${minutes} minutes` : "";
|
|
34
|
+
return direction === "future"
|
|
35
|
+
? `in ${hourText}${detail}`
|
|
36
|
+
: `${hourText}${detail} ago`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function formatRelativeTime(value, {
|
|
40
|
+
now = Date.now(),
|
|
41
|
+
locale,
|
|
42
|
+
timeZone,
|
|
43
|
+
} = {}) {
|
|
44
|
+
const parsed = parsedTimestamp(value);
|
|
45
|
+
if (!parsed) return "—";
|
|
46
|
+
const difference = now - parsed.milliseconds;
|
|
47
|
+
const future = difference < 0;
|
|
48
|
+
const elapsed = Math.abs(difference);
|
|
49
|
+
if (elapsed < MINUTE_MS) return "just now";
|
|
50
|
+
if (elapsed < 2 * MINUTE_MS) return future ? "in 1 minute" : "1 minute ago";
|
|
51
|
+
if (elapsed < HOUR_MS) {
|
|
52
|
+
const minutes = future
|
|
53
|
+
? Math.ceil(elapsed / MINUTE_MS)
|
|
54
|
+
: Math.floor(elapsed / MINUTE_MS);
|
|
55
|
+
return future ? `in ${minutes} minutes` : `${minutes} minutes ago`;
|
|
56
|
+
}
|
|
57
|
+
if (elapsed < 2 * HOUR_MS) return future ? "in 1 hour" : "1 hour ago";
|
|
58
|
+
if (elapsed < DAY_MS) return hourPhrase(elapsed, future ? "future" : "past");
|
|
59
|
+
if (elapsed < 2 * DAY_MS) return future ? "in 1 day" : "1 day ago";
|
|
60
|
+
if (elapsed < RELATIVE_DATE_LIMIT_DAYS * DAY_MS) {
|
|
61
|
+
const days = future
|
|
62
|
+
? Math.ceil(elapsed / DAY_MS)
|
|
63
|
+
: Math.floor(elapsed / DAY_MS);
|
|
64
|
+
return future ? `in ${days} days` : `${days} days ago`;
|
|
65
|
+
}
|
|
66
|
+
return new Intl.DateTimeFormat(locale, {
|
|
67
|
+
dateStyle: "medium",
|
|
68
|
+
timeZone,
|
|
69
|
+
}).format(parsed.date);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function timestampPresentation(value, {
|
|
73
|
+
exact = false,
|
|
74
|
+
now = Date.now(),
|
|
75
|
+
locale,
|
|
76
|
+
timeZone,
|
|
77
|
+
} = {}) {
|
|
78
|
+
const parsed = parsedTimestamp(value);
|
|
79
|
+
if (!parsed) return { valid: false, label: "—", iso: null };
|
|
80
|
+
return {
|
|
81
|
+
exact,
|
|
82
|
+
iso: parsed.iso,
|
|
83
|
+
label: exact
|
|
84
|
+
? formatExactTime(value, { locale, timeZone })
|
|
85
|
+
: formatRelativeTime(value, { now, locale, timeZone }),
|
|
86
|
+
valid: true,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class RelativeTimeController {
|
|
91
|
+
constructor({
|
|
92
|
+
now = () => Date.now(),
|
|
93
|
+
setIntervalFn = (...args) => globalThis.setInterval(...args),
|
|
94
|
+
clearIntervalFn = (...args) => globalThis.clearInterval(...args),
|
|
95
|
+
refreshEveryMs = RELATIVE_TIME_REFRESH_MS,
|
|
96
|
+
} = {}) {
|
|
97
|
+
this.now = now;
|
|
98
|
+
this.setIntervalFn = setIntervalFn;
|
|
99
|
+
this.clearIntervalFn = clearIntervalFn;
|
|
100
|
+
this.refreshEveryMs = refreshEveryMs;
|
|
101
|
+
this.entries = new Map();
|
|
102
|
+
this.exactKeys = new Set();
|
|
103
|
+
this.timer = null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
register(key, value, render, { isActive = () => true } = {}) {
|
|
107
|
+
const token = Symbol(key);
|
|
108
|
+
this.entries.set(token, { isActive, key, render, value });
|
|
109
|
+
this.renderEntry(this.entries.get(token));
|
|
110
|
+
if (this.timer === null) {
|
|
111
|
+
this.timer = this.setIntervalFn(() => this.refresh(), this.refreshEveryMs);
|
|
112
|
+
}
|
|
113
|
+
return () => this.entries.delete(token);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
renderEntry(entry) {
|
|
117
|
+
entry.render(timestampPresentation(entry.value, {
|
|
118
|
+
exact: this.exactKeys.has(entry.key),
|
|
119
|
+
now: this.now(),
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
toggle(key) {
|
|
124
|
+
if (this.exactKeys.has(key)) this.exactKeys.delete(key);
|
|
125
|
+
else this.exactKeys.add(key);
|
|
126
|
+
for (const entry of this.entries.values()) {
|
|
127
|
+
if (entry.key === key) this.renderEntry(entry);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
refresh() {
|
|
132
|
+
for (const [token, entry] of this.entries) {
|
|
133
|
+
if (!entry.isActive()) {
|
|
134
|
+
this.entries.delete(token);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (!this.exactKeys.has(entry.key)) this.renderEntry(entry);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
stop() {
|
|
142
|
+
if (this.timer !== null) this.clearIntervalFn(this.timer);
|
|
143
|
+
this.timer = null;
|
|
144
|
+
this.entries.clear();
|
|
145
|
+
}
|
|
146
|
+
}
|
package/src/dashboard.js
CHANGED
|
@@ -43,10 +43,12 @@ const STATIC_FILES = new Map([
|
|
|
43
43
|
["/", [path.join(STATIC_ROOT, "index.html"), "text/html; charset=utf-8"]],
|
|
44
44
|
["/actions.js", [path.join(STATIC_ROOT, "actions.js"), "text/javascript; charset=utf-8"]],
|
|
45
45
|
["/app.js", [path.join(STATIC_ROOT, "app.js"), "text/javascript; charset=utf-8"]],
|
|
46
|
+
["/time.js", [path.join(STATIC_ROOT, "time.js"), "text/javascript; charset=utf-8"]],
|
|
46
47
|
["/state.js", [path.join(STATIC_ROOT, "state.js"), "text/javascript; charset=utf-8"]],
|
|
47
48
|
["/styles.css", [path.join(STATIC_ROOT, "styles.css"), "text/css; charset=utf-8"]],
|
|
48
49
|
["/assets/taskchef-dark.svg", [path.join(ASSET_ROOT, "taskchef-dark.svg"), "image/svg+xml"]],
|
|
49
50
|
["/assets/taskchef.svg", [path.join(ASSET_ROOT, "taskchef.svg"), "image/svg+xml"]],
|
|
51
|
+
["/assets/codex.svg", [path.join(ASSET_ROOT, "codex.svg"), "image/svg+xml"]],
|
|
50
52
|
]);
|
|
51
53
|
|
|
52
54
|
export function dashboardAuthority(host, port) {
|