@chrysb/alphaclaw 0.6.2-beta.5 → 0.7.0-beta.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/lib/public/css/agents.css +37 -13
- package/lib/public/css/cron.css +124 -41
- package/lib/public/css/shell.css +61 -2
- package/lib/public/css/theme.css +2 -1
- package/lib/public/js/app.js +41 -33
- package/lib/public/js/components/agents-tab/agent-detail-panel.js +61 -49
- package/lib/public/js/components/agents-tab/agent-overview/index.js +9 -0
- package/lib/public/js/components/agents-tab/agent-overview/tools-card.js +54 -0
- package/lib/public/js/components/cron-tab/cron-calendar.js +297 -203
- package/lib/public/js/components/cron-tab/cron-helpers.js +48 -0
- package/lib/public/js/components/cron-tab/cron-insights-panel.js +294 -0
- package/lib/public/js/components/cron-tab/cron-job-detail.js +38 -363
- package/lib/public/js/components/cron-tab/cron-job-settings-card.js +233 -0
- package/lib/public/js/components/cron-tab/cron-overview.js +40 -19
- package/lib/public/js/components/cron-tab/cron-prompt-editor.js +173 -0
- package/lib/public/js/components/cron-tab/cron-run-history-panel.js +74 -62
- package/lib/public/js/components/cron-tab/cron-runs-trend-card.js +24 -24
- package/lib/public/js/components/cron-tab/index.js +170 -78
- package/lib/public/js/components/envars.js +187 -46
- package/lib/public/js/components/file-viewer/editor-surface.js +5 -1
- package/lib/public/js/components/file-viewer/use-editor-line-number-sync.js +36 -0
- package/lib/public/js/components/file-viewer/use-file-viewer.js +7 -23
- package/lib/public/js/components/file-viewer/utils.js +1 -5
- package/lib/public/js/components/models-tab/index.js +137 -133
- package/lib/public/js/components/models-tab/provider-auth-card.js +8 -1
- package/lib/public/js/components/models-tab/use-models.js +35 -8
- package/lib/public/js/components/onboarding/welcome-pairing-step.js +88 -59
- package/lib/public/js/components/pane-shell.js +27 -0
- package/lib/public/js/components/routes/envars-route.js +1 -3
- package/lib/public/js/components/routes/models-route.js +1 -3
- package/lib/public/js/lib/app-navigation.js +1 -1
- package/lib/server/cost-utils.js +2 -2
- package/package.json +1 -1
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useState,
|
|
7
|
+
} from "https://esm.sh/preact/hooks";
|
|
3
8
|
import htm from "https://esm.sh/htm";
|
|
4
|
-
import {
|
|
9
|
+
import { SegmentedControl } from "../segmented-control.js";
|
|
5
10
|
import { Tooltip } from "../tooltip.js";
|
|
6
|
-
import {
|
|
7
|
-
|
|
11
|
+
import {
|
|
12
|
+
formatCost,
|
|
13
|
+
formatCronScheduleLabel,
|
|
14
|
+
formatTokenCount,
|
|
15
|
+
getCronRunEstimatedCost,
|
|
16
|
+
getCronRunTotalTokens,
|
|
17
|
+
} from "./cron-helpers.js";
|
|
8
18
|
import { readUiSettings, updateUiSettings } from "../../lib/ui-settings.js";
|
|
9
19
|
import {
|
|
10
20
|
classifyRepeatingJobs,
|
|
@@ -24,7 +34,8 @@ const formatHourLabel = (hourOfDay) => {
|
|
|
24
34
|
});
|
|
25
35
|
};
|
|
26
36
|
|
|
27
|
-
const buildCellKey = (dayKey, hourOfDay) =>
|
|
37
|
+
const buildCellKey = (dayKey, hourOfDay) =>
|
|
38
|
+
`${String(dayKey || "")}:${hourOfDay}`;
|
|
28
39
|
const toLocalDayKey = (valueMs) => {
|
|
29
40
|
const dateValue = new Date(valueMs);
|
|
30
41
|
const year = dateValue.getFullYear();
|
|
@@ -49,101 +60,107 @@ const slotStateClassName = ({
|
|
|
49
60
|
const tierClassName = tierClassNameByKey[tokenTier] || tierClassNameByKey.low;
|
|
50
61
|
if (!isPast) return `${tierClassName} cron-calendar-slot-upcoming`;
|
|
51
62
|
if (mappedStatus === "ok") return `${tierClassName} cron-calendar-slot-ok`;
|
|
52
|
-
if (mappedStatus === "error")
|
|
53
|
-
|
|
63
|
+
if (mappedStatus === "error")
|
|
64
|
+
return `${tierClassName} cron-calendar-slot-error`;
|
|
65
|
+
if (mappedStatus === "skipped")
|
|
66
|
+
return `${tierClassName} cron-calendar-slot-skipped`;
|
|
54
67
|
return `${tierClassName} cron-calendar-slot-past`;
|
|
55
68
|
};
|
|
56
69
|
|
|
57
70
|
const renderLegend = () => html`
|
|
58
71
|
<div class="cron-calendar-legend">
|
|
59
72
|
<span class="cron-calendar-legend-label">Token intensity</span>
|
|
60
|
-
<span class="cron-calendar-legend-pill cron-calendar-slot-tier-unknown"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<span class="cron-calendar-legend-pill cron-calendar-slot-tier-
|
|
64
|
-
|
|
73
|
+
<span class="cron-calendar-legend-pill cron-calendar-slot-tier-unknown"
|
|
74
|
+
>No usage</span
|
|
75
|
+
>
|
|
76
|
+
<span class="cron-calendar-legend-pill cron-calendar-slot-tier-low"
|
|
77
|
+
>Low</span
|
|
78
|
+
>
|
|
79
|
+
<span class="cron-calendar-legend-pill cron-calendar-slot-tier-medium"
|
|
80
|
+
>Medium</span
|
|
81
|
+
>
|
|
82
|
+
<span class="cron-calendar-legend-pill cron-calendar-slot-tier-high"
|
|
83
|
+
>High</span
|
|
84
|
+
>
|
|
85
|
+
<span class="cron-calendar-legend-pill cron-calendar-slot-tier-very-high"
|
|
86
|
+
>Very high</span
|
|
87
|
+
>
|
|
65
88
|
</div>
|
|
66
89
|
`;
|
|
67
90
|
|
|
68
91
|
const kNowRefreshMs = 60 * 1000;
|
|
69
92
|
const kCalendarExpandedUiSettingKey = "cronCalendarExpanded";
|
|
93
|
+
const kCalendarViewUiSettingKey = "cronCalendarView";
|
|
94
|
+
const kCalendarViewUpcoming = "upcoming";
|
|
95
|
+
const kCalendarViewCalendar = "calendar";
|
|
96
|
+
const kCalendarViewOptions = [
|
|
97
|
+
{ label: "Up next", value: kCalendarViewUpcoming },
|
|
98
|
+
{ label: "Calendar", value: kCalendarViewCalendar },
|
|
99
|
+
];
|
|
70
100
|
const kRunWindow7dMs = 7 * 24 * 60 * 60 * 1000;
|
|
71
101
|
const kSlotRunToleranceMs = 45 * 60 * 1000;
|
|
72
102
|
const kUnknownTier = "unknown";
|
|
73
103
|
|
|
74
104
|
const formatUpcomingTime = (timestampMs) => {
|
|
75
105
|
const dateValue = new Date(timestampMs);
|
|
76
|
-
return dateValue.toLocaleTimeString([], {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const usage = entry?.usage || {};
|
|
81
|
-
const candidates = [
|
|
82
|
-
usage?.total_tokens,
|
|
83
|
-
usage?.totalTokens,
|
|
84
|
-
entry?.total_tokens,
|
|
85
|
-
entry?.totalTokens,
|
|
86
|
-
];
|
|
87
|
-
for (const candidate of candidates) {
|
|
88
|
-
const numericValue = Number(candidate);
|
|
89
|
-
if (Number.isFinite(numericValue) && numericValue >= 0) return numericValue;
|
|
90
|
-
}
|
|
91
|
-
return 0;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const getRunEstimatedCost = (entry = {}) => {
|
|
95
|
-
const usage = entry?.usage || {};
|
|
96
|
-
const candidates = [
|
|
97
|
-
entry?.estimatedCost,
|
|
98
|
-
entry?.estimated_cost,
|
|
99
|
-
usage?.estimatedCost,
|
|
100
|
-
usage?.estimated_cost,
|
|
101
|
-
usage?.totalCost,
|
|
102
|
-
usage?.total_cost,
|
|
103
|
-
usage?.costUsd,
|
|
104
|
-
usage?.cost,
|
|
105
|
-
];
|
|
106
|
-
for (const candidate of candidates) {
|
|
107
|
-
const numericValue = Number(candidate);
|
|
108
|
-
if (Number.isFinite(numericValue) && numericValue >= 0) return numericValue;
|
|
109
|
-
}
|
|
110
|
-
return null;
|
|
106
|
+
return dateValue.toLocaleTimeString([], {
|
|
107
|
+
hour: "numeric",
|
|
108
|
+
minute: "2-digit",
|
|
109
|
+
});
|
|
111
110
|
};
|
|
112
111
|
|
|
113
|
-
const buildRunSummaryByJobId = ({
|
|
112
|
+
const buildRunSummaryByJobId = ({
|
|
113
|
+
runsByJobId = {},
|
|
114
|
+
nowMs = Date.now(),
|
|
115
|
+
} = {}) => {
|
|
114
116
|
const cutoffMs = Number(nowMs || Date.now()) - kRunWindow7dMs;
|
|
115
|
-
return Object.entries(runsByJobId || {}).reduce(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
totalCost,
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
117
|
+
return Object.entries(runsByJobId || {}).reduce(
|
|
118
|
+
(accumulator, [jobId, runResult]) => {
|
|
119
|
+
const entries = Array.isArray(runResult?.entries)
|
|
120
|
+
? runResult.entries
|
|
121
|
+
: [];
|
|
122
|
+
const recentEntries = entries.filter((entry) => {
|
|
123
|
+
const timestampMs = Number(entry?.ts || 0);
|
|
124
|
+
return (
|
|
125
|
+
Number.isFinite(timestampMs) &&
|
|
126
|
+
timestampMs >= cutoffMs &&
|
|
127
|
+
timestampMs <= nowMs
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
const runCount = recentEntries.length;
|
|
131
|
+
const totalTokens = recentEntries.reduce(
|
|
132
|
+
(sum, entry) => sum + Number(getCronRunTotalTokens(entry) || 0),
|
|
133
|
+
0,
|
|
134
|
+
);
|
|
135
|
+
const totalCost = recentEntries.reduce((sum, entry) => {
|
|
136
|
+
const cost = getCronRunEstimatedCost(entry);
|
|
137
|
+
return sum + Number(cost == null ? 0 : cost);
|
|
138
|
+
}, 0);
|
|
139
|
+
accumulator[String(jobId || "")] = {
|
|
140
|
+
runCount,
|
|
141
|
+
totalTokens,
|
|
142
|
+
totalCost,
|
|
143
|
+
avgTokensPerRun: runCount > 0 ? Math.round(totalTokens / runCount) : 0,
|
|
144
|
+
avgCostPerRun: runCount > 0 ? totalCost / runCount : 0,
|
|
145
|
+
};
|
|
146
|
+
return accumulator;
|
|
147
|
+
},
|
|
148
|
+
{},
|
|
149
|
+
);
|
|
139
150
|
};
|
|
140
151
|
|
|
141
|
-
const mapRunsToSlots = ({
|
|
152
|
+
const mapRunsToSlots = ({
|
|
153
|
+
slots = [],
|
|
154
|
+
runsByJobId = {},
|
|
155
|
+
nowMs = Date.now(),
|
|
156
|
+
} = {}) => {
|
|
142
157
|
const runsBySlotKey = {};
|
|
143
158
|
const consumedRunTimestampsByJobId = {};
|
|
144
159
|
const runEntriesByJobId = Object.entries(runsByJobId || {}).reduce(
|
|
145
160
|
(accumulator, [jobId, runResult]) => {
|
|
146
|
-
const entries = Array.isArray(runResult?.entries)
|
|
161
|
+
const entries = Array.isArray(runResult?.entries)
|
|
162
|
+
? runResult.entries
|
|
163
|
+
: [];
|
|
147
164
|
const normalizedEntries = entries
|
|
148
165
|
.map((entry) => ({ ...entry, ts: Number(entry?.ts || 0) }))
|
|
149
166
|
.filter((entry) => Number.isFinite(entry.ts) && entry.ts > 0)
|
|
@@ -205,14 +222,14 @@ const classifyTokenTier = ({
|
|
|
205
222
|
} = {}) => {
|
|
206
223
|
if (!enabled) return "disabled";
|
|
207
224
|
const safeValue = Number(tokenValue || 0);
|
|
208
|
-
if (!Number.isFinite(safeValue) || safeValue <= 0 || !thresholds)
|
|
225
|
+
if (!Number.isFinite(safeValue) || safeValue <= 0 || !thresholds)
|
|
226
|
+
return kUnknownTier;
|
|
209
227
|
if (safeValue <= thresholds.q1) return "low";
|
|
210
228
|
if (safeValue <= thresholds.q2) return "medium";
|
|
211
229
|
if (safeValue <= thresholds.p90) return "high";
|
|
212
230
|
return "very-high";
|
|
213
231
|
};
|
|
214
232
|
|
|
215
|
-
|
|
216
233
|
const buildJobTooltipText = ({
|
|
217
234
|
jobName = "",
|
|
218
235
|
job = null,
|
|
@@ -223,18 +240,25 @@ const buildJobTooltipText = ({
|
|
|
223
240
|
scheduledStatus = "",
|
|
224
241
|
nowMs = Date.now(),
|
|
225
242
|
} = {}) => {
|
|
226
|
-
const isPastSlot =
|
|
243
|
+
const isPastSlot =
|
|
244
|
+
Number(scheduledAtMs || 0) > 0 && Number(scheduledAtMs || 0) <= nowMs;
|
|
227
245
|
const runCount7d = Number(runSummary7d?.runCount || 0);
|
|
228
246
|
const avgTokensPerRun7d = Number(runSummary7d?.avgTokensPerRun || 0);
|
|
229
247
|
const avgCostPerRun7d = Number(runSummary7d?.avgCostPerRun || 0);
|
|
230
|
-
const slotRunTokens =
|
|
231
|
-
const slotRunCost =
|
|
232
|
-
const slotRunStatus = String(slotRun?.status || "")
|
|
248
|
+
const slotRunTokens = getCronRunTotalTokens(slotRun || {});
|
|
249
|
+
const slotRunCost = getCronRunEstimatedCost(slotRun || {});
|
|
250
|
+
const slotRunStatus = String(slotRun?.status || "")
|
|
251
|
+
.trim()
|
|
252
|
+
.toLowerCase();
|
|
233
253
|
|
|
234
254
|
const lines = [String(jobName || "Job")];
|
|
235
255
|
if (isPastSlot) {
|
|
236
|
-
lines.push(
|
|
237
|
-
|
|
256
|
+
lines.push(
|
|
257
|
+
`Run tokens: ${slotRun ? formatTokenCount(slotRunTokens) : "—"}`,
|
|
258
|
+
);
|
|
259
|
+
lines.push(
|
|
260
|
+
`Run cost: ${slotRunCost == null ? "—" : formatCost(slotRunCost)}`,
|
|
261
|
+
);
|
|
238
262
|
lines.push(`Run status: ${slotRunStatus || scheduledStatus || "unknown"}`);
|
|
239
263
|
if (slotRun?.ts) {
|
|
240
264
|
lines.push(
|
|
@@ -248,7 +272,9 @@ const buildJobTooltipText = ({
|
|
|
248
272
|
lines.push(
|
|
249
273
|
`Avg cost/run (last 7d): ${runCount7d > 0 ? formatCost(avgCostPerRun7d) : "—"}`,
|
|
250
274
|
);
|
|
251
|
-
lines.push(
|
|
275
|
+
lines.push(
|
|
276
|
+
`Runs (last 7d): ${runCount7d > 0 ? formatTokenCount(runCount7d) : "none"}`,
|
|
277
|
+
);
|
|
252
278
|
}
|
|
253
279
|
|
|
254
280
|
if (!isPastSlot && latestRun?.status) {
|
|
@@ -259,12 +285,15 @@ const buildJobTooltipText = ({
|
|
|
259
285
|
lines.push("Latest run: none");
|
|
260
286
|
}
|
|
261
287
|
if (Number(job?.state?.runningAtMs || 0) > 0) {
|
|
262
|
-
lines.push(
|
|
288
|
+
lines.push(
|
|
289
|
+
`Current run: active (${new Date(Number(job.state.runningAtMs)).toLocaleString()})`,
|
|
290
|
+
);
|
|
263
291
|
}
|
|
264
292
|
|
|
265
293
|
if (scheduledAtMs > 0) {
|
|
266
294
|
const slotLabel = new Date(scheduledAtMs).toLocaleString();
|
|
267
|
-
const slotState =
|
|
295
|
+
const slotState =
|
|
296
|
+
scheduledStatus || (scheduledAtMs <= Date.now() ? "past" : "upcoming");
|
|
268
297
|
lines.push(`Slot: ${slotState} (${slotLabel})`);
|
|
269
298
|
}
|
|
270
299
|
return lines.join("\n");
|
|
@@ -275,16 +304,29 @@ export const CronCalendar = ({
|
|
|
275
304
|
runsByJobId = {},
|
|
276
305
|
onSelectJob = () => {},
|
|
277
306
|
}) => {
|
|
278
|
-
const [
|
|
307
|
+
const [calendarView, setCalendarView] = useState(() => {
|
|
279
308
|
const settings = readUiSettings();
|
|
280
|
-
|
|
309
|
+
const savedView = String(
|
|
310
|
+
settings?.[kCalendarViewUiSettingKey] || "",
|
|
311
|
+
).trim();
|
|
312
|
+
if (savedView === kCalendarViewCalendar) return kCalendarViewCalendar;
|
|
313
|
+
if (savedView === kCalendarViewUpcoming) return kCalendarViewUpcoming;
|
|
314
|
+
return settings[kCalendarExpandedUiSettingKey] === true
|
|
315
|
+
? kCalendarViewCalendar
|
|
316
|
+
: kCalendarViewUpcoming;
|
|
281
317
|
});
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
318
|
+
const isCalendarView = calendarView === kCalendarViewCalendar;
|
|
319
|
+
const onChangeCalendarView = useCallback((nextValue) => {
|
|
320
|
+
const nextView =
|
|
321
|
+
nextValue === kCalendarViewCalendar
|
|
322
|
+
? kCalendarViewCalendar
|
|
323
|
+
: kCalendarViewUpcoming;
|
|
324
|
+
setCalendarView(nextView);
|
|
325
|
+
updateUiSettings((settings) => ({
|
|
326
|
+
...settings,
|
|
327
|
+
[kCalendarViewUiSettingKey]: nextView,
|
|
328
|
+
[kCalendarExpandedUiSettingKey]: nextView === kCalendarViewCalendar,
|
|
329
|
+
}));
|
|
288
330
|
}, []);
|
|
289
331
|
|
|
290
332
|
const [nowMs, setNowMs] = useState(() => Date.now());
|
|
@@ -306,7 +348,12 @@ export const CronCalendar = ({
|
|
|
306
348
|
[scheduledJobs, nowMs],
|
|
307
349
|
);
|
|
308
350
|
const statusBySlotKey = useMemo(
|
|
309
|
-
() =>
|
|
351
|
+
() =>
|
|
352
|
+
mapRunStatusesToSlots({
|
|
353
|
+
slots: timeline.slots,
|
|
354
|
+
bulkRunsByJobId: runsByJobId,
|
|
355
|
+
nowMs,
|
|
356
|
+
}),
|
|
310
357
|
[timeline.slots, runsByJobId, nowMs],
|
|
311
358
|
);
|
|
312
359
|
const jobById = useMemo(
|
|
@@ -320,14 +367,21 @@ export const CronCalendar = ({
|
|
|
320
367
|
);
|
|
321
368
|
const latestRunByJobId = useMemo(
|
|
322
369
|
() =>
|
|
323
|
-
Object.entries(runsByJobId || {}).reduce(
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
370
|
+
Object.entries(runsByJobId || {}).reduce(
|
|
371
|
+
(accumulator, [jobId, runResult]) => {
|
|
372
|
+
const entries = Array.isArray(runResult?.entries)
|
|
373
|
+
? runResult.entries
|
|
374
|
+
: [];
|
|
375
|
+
const latest = entries
|
|
376
|
+
.filter((entry) => Number(entry?.ts || 0) > 0)
|
|
377
|
+
.sort(
|
|
378
|
+
(left, right) => Number(right?.ts || 0) - Number(left?.ts || 0),
|
|
379
|
+
)[0];
|
|
380
|
+
accumulator[jobId] = latest || null;
|
|
381
|
+
return accumulator;
|
|
382
|
+
},
|
|
383
|
+
{},
|
|
384
|
+
),
|
|
331
385
|
[runsByJobId],
|
|
332
386
|
);
|
|
333
387
|
const runSummary7dByJobId = useMemo(
|
|
@@ -345,51 +399,72 @@ export const CronCalendar = ({
|
|
|
345
399
|
if (!job || job.enabled === false) return;
|
|
346
400
|
const isPastSlot = Number(slot?.scheduledAtMs || 0) <= nowMs;
|
|
347
401
|
if (isPastSlot) {
|
|
348
|
-
const slotRunTokens =
|
|
402
|
+
const slotRunTokens = getCronRunTotalTokens(runBySlotKey[slot.key] || {});
|
|
349
403
|
if (slotRunTokens > 0) values.push(slotRunTokens);
|
|
350
404
|
return;
|
|
351
405
|
}
|
|
352
|
-
const projectedAvgTokens = Number(
|
|
406
|
+
const projectedAvgTokens = Number(
|
|
407
|
+
runSummary7dByJobId[slot.jobId]?.avgTokensPerRun || 0,
|
|
408
|
+
);
|
|
353
409
|
if (projectedAvgTokens > 0) values.push(projectedAvgTokens);
|
|
354
410
|
});
|
|
355
411
|
repeatingJobs.forEach((job) => {
|
|
356
412
|
const jobId = String(job?.id || "");
|
|
357
|
-
const projectedAvgTokens = Number(
|
|
413
|
+
const projectedAvgTokens = Number(
|
|
414
|
+
runSummary7dByJobId[jobId]?.avgTokensPerRun || 0,
|
|
415
|
+
);
|
|
358
416
|
if (projectedAvgTokens > 0) values.push(projectedAvgTokens);
|
|
359
417
|
});
|
|
360
418
|
return buildTierThresholds(values);
|
|
361
|
-
}, [
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
419
|
+
}, [
|
|
420
|
+
jobById,
|
|
421
|
+
nowMs,
|
|
422
|
+
repeatingJobs,
|
|
423
|
+
runBySlotKey,
|
|
424
|
+
runSummary7dByJobId,
|
|
425
|
+
timeline.slots,
|
|
426
|
+
]);
|
|
427
|
+
const getSlotTokenTier = useCallback(
|
|
428
|
+
(slot = null) => {
|
|
429
|
+
const jobId = String(slot?.jobId || "");
|
|
430
|
+
const job = jobById[jobId] || null;
|
|
431
|
+
const enabled = job?.enabled !== false;
|
|
432
|
+
const isPastSlot = Number(slot?.scheduledAtMs || 0) <= nowMs;
|
|
433
|
+
if (isPastSlot) {
|
|
434
|
+
const slotRunTokens = getCronRunTotalTokens(
|
|
435
|
+
runBySlotKey[String(slot?.key || "")] || {},
|
|
436
|
+
);
|
|
437
|
+
return classifyTokenTier({
|
|
438
|
+
enabled,
|
|
439
|
+
tokenValue: slotRunTokens,
|
|
440
|
+
thresholds: slotTierThresholds,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
const projectedAvgTokens = Number(
|
|
444
|
+
runSummary7dByJobId[jobId]?.avgTokensPerRun || 0,
|
|
445
|
+
);
|
|
369
446
|
return classifyTokenTier({
|
|
370
447
|
enabled,
|
|
371
|
-
tokenValue:
|
|
448
|
+
tokenValue: projectedAvgTokens,
|
|
372
449
|
thresholds: slotTierThresholds,
|
|
373
450
|
});
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
});
|
|
389
|
-
}, [jobById, runSummary7dByJobId, slotTierThresholds]);
|
|
451
|
+
},
|
|
452
|
+
[jobById, nowMs, runBySlotKey, runSummary7dByJobId, slotTierThresholds],
|
|
453
|
+
);
|
|
454
|
+
const getJobProjectedTier = useCallback(
|
|
455
|
+
(jobId = "") => {
|
|
456
|
+
const job = jobById[jobId] || null;
|
|
457
|
+
return classifyTokenTier({
|
|
458
|
+
enabled: job?.enabled !== false,
|
|
459
|
+
tokenValue: Number(runSummary7dByJobId[jobId]?.avgTokensPerRun || 0),
|
|
460
|
+
thresholds: slotTierThresholds,
|
|
461
|
+
});
|
|
462
|
+
},
|
|
463
|
+
[jobById, runSummary7dByJobId, slotTierThresholds],
|
|
464
|
+
);
|
|
390
465
|
|
|
391
466
|
const upcomingSlots = useMemo(
|
|
392
|
-
() => getUpcomingSlots({ slots: timeline.slots, nowMs }),
|
|
467
|
+
() => getUpcomingSlots({ slots: timeline.slots, nowMs, limit: 3 }),
|
|
393
468
|
[timeline.slots, nowMs],
|
|
394
469
|
);
|
|
395
470
|
|
|
@@ -418,7 +493,9 @@ export const CronCalendar = ({
|
|
|
418
493
|
const renderCompactStrip = () => {
|
|
419
494
|
return html`
|
|
420
495
|
${upcomingSlots.length === 0
|
|
421
|
-
? html`<div class="text-xs text-gray-500 py-1">
|
|
496
|
+
? html`<div class="text-xs text-gray-500 py-1">
|
|
497
|
+
No upcoming jobs in the next 24 hours.
|
|
498
|
+
</div>`
|
|
422
499
|
: html`
|
|
423
500
|
<div class="cron-calendar-compact-strip">
|
|
424
501
|
${upcomingSlots.map((slot) => {
|
|
@@ -460,8 +537,12 @@ export const CronCalendar = ({
|
|
|
460
537
|
? html`
|
|
461
538
|
<button
|
|
462
539
|
class="text-[11px] text-gray-500 hover:text-gray-300 self-center transition-colors"
|
|
463
|
-
onClick=${
|
|
464
|
-
|
|
540
|
+
onClick=${() =>
|
|
541
|
+
onChangeCalendarView(kCalendarViewCalendar)}
|
|
542
|
+
>
|
|
543
|
+
+${Math.max(0, totalUpcoming - upcomingSlots.length)} more
|
|
544
|
+
this week
|
|
545
|
+
</button>
|
|
465
546
|
`
|
|
466
547
|
: null}
|
|
467
548
|
</div>
|
|
@@ -476,7 +557,9 @@ export const CronCalendar = ({
|
|
|
476
557
|
</div>
|
|
477
558
|
|
|
478
559
|
${hourRows.length === 0
|
|
479
|
-
? html`<div class="text-sm text-gray-500">
|
|
560
|
+
? html`<div class="text-sm text-gray-500">
|
|
561
|
+
No scheduled jobs in this rolling window.
|
|
562
|
+
</div>`
|
|
480
563
|
: html`
|
|
481
564
|
<div class="cron-calendar-grid-wrap">
|
|
482
565
|
<div class="cron-calendar-grid-header">
|
|
@@ -493,34 +576,41 @@ export const CronCalendar = ({
|
|
|
493
576
|
)}
|
|
494
577
|
</div>
|
|
495
578
|
<div class="cron-calendar-grid-body">
|
|
496
|
-
${hourRows.map(
|
|
497
|
-
|
|
498
|
-
<div class="cron-calendar-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
579
|
+
${hourRows.map(
|
|
580
|
+
(hourOfDay) => html`
|
|
581
|
+
<div key=${hourOfDay} class="cron-calendar-grid-row">
|
|
582
|
+
<div class="cron-calendar-hour-cell">
|
|
583
|
+
${formatHourLabel(hourOfDay)}
|
|
584
|
+
</div>
|
|
585
|
+
${timeline.days.map((day) => {
|
|
586
|
+
const cellKey = buildCellKey(day.dayKey, hourOfDay);
|
|
587
|
+
const cellSlots = slotsByCellKey[cellKey] || [];
|
|
588
|
+
const visibleSlots = cellSlots.slice(0, 3);
|
|
589
|
+
const overflowCount = Math.max(
|
|
590
|
+
0,
|
|
591
|
+
cellSlots.length - visibleSlots.length,
|
|
592
|
+
);
|
|
593
|
+
return html`
|
|
594
|
+
<div
|
|
595
|
+
key=${cellKey}
|
|
596
|
+
class=${`cron-calendar-grid-cell ${day.dayKey === todayDayKey ? "is-today" : ""}`}
|
|
597
|
+
>
|
|
598
|
+
${visibleSlots.map((slot) => {
|
|
599
|
+
const status = statusBySlotKey[slot.key] || "";
|
|
600
|
+
const isPast = slot.scheduledAtMs <= nowMs;
|
|
601
|
+
const tokenTier = getSlotTokenTier(slot);
|
|
602
|
+
const tooltipText = buildJobTooltipText({
|
|
603
|
+
jobName: slot.jobName,
|
|
604
|
+
job: jobById[slot.jobId] || null,
|
|
605
|
+
runSummary7d:
|
|
606
|
+
runSummary7dByJobId[slot.jobId] || {},
|
|
607
|
+
slotRun: runBySlotKey[slot.key] || null,
|
|
608
|
+
latestRun: latestRunByJobId[slot.jobId],
|
|
609
|
+
scheduledAtMs: slot.scheduledAtMs,
|
|
610
|
+
scheduledStatus: status,
|
|
611
|
+
nowMs,
|
|
612
|
+
});
|
|
613
|
+
return html`
|
|
524
614
|
<${Tooltip}
|
|
525
615
|
text=${tooltipText}
|
|
526
616
|
widthClass="w-72"
|
|
@@ -529,16 +619,22 @@ export const CronCalendar = ({
|
|
|
529
619
|
>
|
|
530
620
|
<div
|
|
531
621
|
key=${slot.key}
|
|
532
|
-
class=${`cron-calendar-slot-chip ${slotStateClassName(
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
622
|
+
class=${`cron-calendar-slot-chip ${slotStateClassName(
|
|
623
|
+
{
|
|
624
|
+
isPast,
|
|
625
|
+
mappedStatus: status,
|
|
626
|
+
tokenTier,
|
|
627
|
+
},
|
|
628
|
+
)}`}
|
|
537
629
|
role="button"
|
|
538
630
|
tabindex="0"
|
|
539
631
|
onClick=${() => onSelectJob(slot.jobId)}
|
|
540
632
|
onKeyDown=${(event) => {
|
|
541
|
-
if (
|
|
633
|
+
if (
|
|
634
|
+
event.key !== "Enter" &&
|
|
635
|
+
event.key !== " "
|
|
636
|
+
)
|
|
637
|
+
return;
|
|
542
638
|
event.preventDefault();
|
|
543
639
|
onSelectJob(slot.jobId);
|
|
544
640
|
}}
|
|
@@ -547,19 +643,21 @@ export const CronCalendar = ({
|
|
|
547
643
|
</div>
|
|
548
644
|
</${Tooltip}>
|
|
549
645
|
`;
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
646
|
+
})}
|
|
647
|
+
${overflowCount > 0
|
|
648
|
+
? html`<div class="cron-calendar-slot-overflow">
|
|
649
|
+
+${overflowCount} more
|
|
650
|
+
</div>`
|
|
651
|
+
: null}
|
|
652
|
+
</div>
|
|
653
|
+
`;
|
|
654
|
+
})}
|
|
655
|
+
</div>
|
|
656
|
+
`,
|
|
657
|
+
)}
|
|
559
658
|
</div>
|
|
560
659
|
</div>
|
|
561
660
|
`}
|
|
562
|
-
|
|
563
661
|
${repeatingJobs.length > 0
|
|
564
662
|
? html`
|
|
565
663
|
<div class="cron-calendar-repeating-strip">
|
|
@@ -586,16 +684,19 @@ export const CronCalendar = ({
|
|
|
586
684
|
triggerClassName="inline-flex max-w-full"
|
|
587
685
|
>
|
|
588
686
|
<div
|
|
589
|
-
class=${`cron-calendar-repeating-pill ${slotStateClassName(
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
687
|
+
class=${`cron-calendar-repeating-pill ${slotStateClassName(
|
|
688
|
+
{
|
|
689
|
+
isPast: false,
|
|
690
|
+
mappedStatus: "",
|
|
691
|
+
tokenTier: getJobProjectedTier(jobId),
|
|
692
|
+
},
|
|
693
|
+
)}`}
|
|
594
694
|
role="button"
|
|
595
695
|
tabindex="0"
|
|
596
696
|
onClick=${() => onSelectJob(jobId)}
|
|
597
697
|
onKeyDown=${(event) => {
|
|
598
|
-
if (event.key !== "Enter" && event.key !== " ")
|
|
698
|
+
if (event.key !== "Enter" && event.key !== " ")
|
|
699
|
+
return;
|
|
599
700
|
event.preventDefault();
|
|
600
701
|
onSelectJob(jobId);
|
|
601
702
|
}}
|
|
@@ -605,9 +706,11 @@ export const CronCalendar = ({
|
|
|
605
706
|
${formatCronScheduleLabel(job.schedule, {
|
|
606
707
|
includeTimeZoneWhenDifferent: true,
|
|
607
708
|
})}
|
|
608
|
-
${
|
|
609
|
-
|
|
610
|
-
|
|
709
|
+
${
|
|
710
|
+
avgTokensPerRun > 0
|
|
711
|
+
? ` | avg ${formatTokenCount(avgTokensPerRun)} tk`
|
|
712
|
+
: ""
|
|
713
|
+
}
|
|
611
714
|
</span>
|
|
612
715
|
</div>
|
|
613
716
|
</${Tooltip}>
|
|
@@ -622,24 +725,15 @@ export const CronCalendar = ({
|
|
|
622
725
|
|
|
623
726
|
return html`
|
|
624
727
|
<section class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
625
|
-
<div class="flex items-center
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
${!expanded && repeatingJobs.length > 0
|
|
631
|
-
? html`<span class="text-[11px] text-gray-500">+ ${repeatingJobs.length} repeating</span>`
|
|
632
|
-
: null}
|
|
633
|
-
</div>
|
|
634
|
-
<${ActionButton}
|
|
635
|
-
onClick=${toggleExpanded}
|
|
636
|
-
tone="neutral"
|
|
637
|
-
size="sm"
|
|
638
|
-
idleLabel=${expanded ? "Collapse" : "Show full week"}
|
|
728
|
+
<div class="flex items-center gap-2">
|
|
729
|
+
<${SegmentedControl}
|
|
730
|
+
options=${kCalendarViewOptions}
|
|
731
|
+
value=${calendarView}
|
|
732
|
+
onChange=${onChangeCalendarView}
|
|
639
733
|
/>
|
|
640
734
|
</div>
|
|
641
735
|
|
|
642
|
-
${
|
|
736
|
+
${isCalendarView ? renderFullGrid() : renderCompactStrip()}
|
|
643
737
|
</section>
|
|
644
738
|
`;
|
|
645
739
|
};
|