@chrysb/alphaclaw 0.6.0 → 0.6.2-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 +1 -1
- package/lib/public/css/cron.css +535 -0
- package/lib/public/css/theme.css +72 -0
- package/lib/public/js/app.js +45 -10
- package/lib/public/js/components/action-button.js +26 -20
- package/lib/public/js/components/agents-tab/agent-detail-panel.js +98 -17
- package/lib/public/js/components/agents-tab/agent-tools/index.js +105 -0
- package/lib/public/js/components/agents-tab/agent-tools/tool-catalog.js +289 -0
- package/lib/public/js/components/agents-tab/agent-tools/use-agent-tools.js +128 -0
- package/lib/public/js/components/agents-tab/index.js +4 -0
- package/lib/public/js/components/cron-tab/cron-calendar-helpers.js +385 -0
- package/lib/public/js/components/cron-tab/cron-calendar.js +441 -0
- package/lib/public/js/components/cron-tab/cron-helpers.js +326 -0
- package/lib/public/js/components/cron-tab/cron-job-detail.js +425 -0
- package/lib/public/js/components/cron-tab/cron-job-list.js +305 -0
- package/lib/public/js/components/cron-tab/cron-job-usage.js +70 -0
- package/lib/public/js/components/cron-tab/cron-overview.js +599 -0
- package/lib/public/js/components/cron-tab/cron-runs-trend-card.js +277 -0
- package/lib/public/js/components/cron-tab/index.js +100 -0
- package/lib/public/js/components/cron-tab/use-cron-tab.js +366 -0
- package/lib/public/js/components/doctor/summary-cards.js +5 -11
- package/lib/public/js/components/google/gmail-setup-wizard.js +30 -30
- package/lib/public/js/components/google/index.js +1 -1
- package/lib/public/js/components/icons.js +13 -0
- package/lib/public/js/components/pill-tabs.js +33 -0
- package/lib/public/js/components/pop-actions.js +58 -0
- package/lib/public/js/components/routes/agents-route.js +4 -0
- package/lib/public/js/components/routes/cron-route.js +9 -0
- package/lib/public/js/components/routes/index.js +1 -0
- package/lib/public/js/components/segmented-control.js +15 -9
- package/lib/public/js/components/summary-stat-card.js +17 -0
- package/lib/public/js/components/tooltip.js +50 -4
- package/lib/public/js/components/watchdog-tab.js +46 -1
- package/lib/public/js/lib/api.js +94 -0
- package/lib/public/js/lib/app-navigation.js +2 -0
- package/lib/public/js/lib/storage-keys.js +1 -0
- package/lib/public/setup.html +1 -0
- package/lib/server/agents/agents.js +15 -0
- package/lib/server/constants.js +1 -0
- package/lib/server/cost-utils.js +312 -0
- package/lib/server/cron-service.js +461 -0
- package/lib/server/db/usage/index.js +100 -1
- package/lib/server/db/usage/pricing.js +1 -83
- package/lib/server/db/usage/sessions.js +4 -1
- package/lib/server/db/usage/shared.js +2 -1
- package/lib/server/db/usage/summary.js +5 -1
- package/lib/server/gmail-watch.js +0 -1
- package/lib/server/onboarding/index.js +39 -5
- package/lib/server/onboarding/openclaw.js +25 -19
- package/lib/server/onboarding/validation.js +28 -0
- package/lib/server/routes/cron.js +148 -0
- package/lib/server.js +13 -0
- package/package.json +1 -1
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import htm from "https://esm.sh/htm";
|
|
4
|
+
import { ActionButton } from "../action-button.js";
|
|
5
|
+
import { Tooltip } from "../tooltip.js";
|
|
6
|
+
import { formatCost, formatTokenCount } from "./cron-helpers.js";
|
|
7
|
+
import { formatCronScheduleLabel } from "./cron-helpers.js";
|
|
8
|
+
import { readUiSettings, updateUiSettings } from "../../lib/ui-settings.js";
|
|
9
|
+
import {
|
|
10
|
+
buildTokenTierByJobId,
|
|
11
|
+
classifyRepeatingJobs,
|
|
12
|
+
expandJobsToRollingSlots,
|
|
13
|
+
getUpcomingSlots,
|
|
14
|
+
mapRunStatusesToSlots,
|
|
15
|
+
} from "./cron-calendar-helpers.js";
|
|
16
|
+
|
|
17
|
+
const html = htm.bind(h);
|
|
18
|
+
|
|
19
|
+
const formatHourLabel = (hourOfDay) => {
|
|
20
|
+
const dateValue = new Date();
|
|
21
|
+
dateValue.setHours(hourOfDay, 0, 0, 0);
|
|
22
|
+
return dateValue.toLocaleTimeString([], {
|
|
23
|
+
hour: "numeric",
|
|
24
|
+
minute: "2-digit",
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const buildCellKey = (dayKey, hourOfDay) => `${String(dayKey || "")}:${hourOfDay}`;
|
|
29
|
+
const toLocalDayKey = (valueMs) => {
|
|
30
|
+
const dateValue = new Date(valueMs);
|
|
31
|
+
const year = dateValue.getFullYear();
|
|
32
|
+
const month = String(dateValue.getMonth() + 1).padStart(2, "0");
|
|
33
|
+
const day = String(dateValue.getDate()).padStart(2, "0");
|
|
34
|
+
return `${year}-${month}-${day}`;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const slotStateClassName = ({
|
|
38
|
+
isPast = false,
|
|
39
|
+
mappedStatus = "",
|
|
40
|
+
tokenTier = "low",
|
|
41
|
+
} = {}) => {
|
|
42
|
+
const tierClassNameByKey = {
|
|
43
|
+
unknown: "cron-calendar-slot-tier-unknown",
|
|
44
|
+
low: "cron-calendar-slot-tier-low",
|
|
45
|
+
medium: "cron-calendar-slot-tier-medium",
|
|
46
|
+
high: "cron-calendar-slot-tier-high",
|
|
47
|
+
"very-high": "cron-calendar-slot-tier-very-high",
|
|
48
|
+
disabled: "cron-calendar-slot-tier-disabled",
|
|
49
|
+
};
|
|
50
|
+
const tierClassName = tierClassNameByKey[tokenTier] || tierClassNameByKey.low;
|
|
51
|
+
if (!isPast) return `${tierClassName} cron-calendar-slot-upcoming`;
|
|
52
|
+
if (mappedStatus === "ok") return `${tierClassName} cron-calendar-slot-ok`;
|
|
53
|
+
if (mappedStatus === "error") return `${tierClassName} cron-calendar-slot-error`;
|
|
54
|
+
if (mappedStatus === "skipped") return `${tierClassName} cron-calendar-slot-skipped`;
|
|
55
|
+
return `${tierClassName} cron-calendar-slot-past`;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const renderLegend = () => html`
|
|
59
|
+
<div class="cron-calendar-legend">
|
|
60
|
+
<span class="cron-calendar-legend-label">Token intensity</span>
|
|
61
|
+
<span class="cron-calendar-legend-pill cron-calendar-slot-tier-unknown">No usage</span>
|
|
62
|
+
<span class="cron-calendar-legend-pill cron-calendar-slot-tier-low">Low</span>
|
|
63
|
+
<span class="cron-calendar-legend-pill cron-calendar-slot-tier-medium">Medium</span>
|
|
64
|
+
<span class="cron-calendar-legend-pill cron-calendar-slot-tier-high">High</span>
|
|
65
|
+
<span class="cron-calendar-legend-pill cron-calendar-slot-tier-very-high">Very high</span>
|
|
66
|
+
</div>
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
const kNowRefreshMs = 60 * 1000;
|
|
70
|
+
const kCalendarExpandedUiSettingKey = "cronCalendarExpanded";
|
|
71
|
+
|
|
72
|
+
const formatUpcomingTime = (timestampMs) => {
|
|
73
|
+
const dateValue = new Date(timestampMs);
|
|
74
|
+
return dateValue.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
const buildJobTooltipText = ({
|
|
79
|
+
jobName = "",
|
|
80
|
+
job = null,
|
|
81
|
+
usage = {},
|
|
82
|
+
latestRun = null,
|
|
83
|
+
scheduledAtMs = 0,
|
|
84
|
+
scheduledStatus = "",
|
|
85
|
+
} = {}) => {
|
|
86
|
+
const runCount = Number(usage?.runCount || 0);
|
|
87
|
+
const totalTokens = Number(usage?.totalTokens || 0);
|
|
88
|
+
const totalCost = Number(usage?.totalCost || 0);
|
|
89
|
+
const avgTokensPerRun = runCount > 0
|
|
90
|
+
? Number(usage?.avgTokensPerRun || Math.round(totalTokens / runCount))
|
|
91
|
+
: 0;
|
|
92
|
+
const avgCostPerRun = runCount > 0 ? totalCost / runCount : 0;
|
|
93
|
+
|
|
94
|
+
const lines = [
|
|
95
|
+
String(jobName || "Job"),
|
|
96
|
+
`Avg tokens/run: ${runCount > 0 ? formatTokenCount(avgTokensPerRun) : "—"}`,
|
|
97
|
+
`Avg cost/run: ${runCount > 0 ? formatCost(avgCostPerRun) : "—"}`,
|
|
98
|
+
`Total cost: ${formatCost(totalCost)}`,
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
if (runCount <= 0) {
|
|
102
|
+
lines.push("Runs: none yet");
|
|
103
|
+
} else {
|
|
104
|
+
lines.push(`Runs: ${formatTokenCount(runCount)}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (latestRun?.status) {
|
|
108
|
+
lines.push(
|
|
109
|
+
`Latest run: ${latestRun.status} (${new Date(Number(latestRun.ts || 0)).toLocaleString()})`,
|
|
110
|
+
);
|
|
111
|
+
} else {
|
|
112
|
+
lines.push("Latest run: none");
|
|
113
|
+
}
|
|
114
|
+
if (Number(job?.state?.runningAtMs || 0) > 0) {
|
|
115
|
+
lines.push(`Current run: active (${new Date(Number(job.state.runningAtMs)).toLocaleString()})`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (scheduledAtMs > 0) {
|
|
119
|
+
const slotLabel = new Date(scheduledAtMs).toLocaleString();
|
|
120
|
+
const slotState = scheduledStatus || (scheduledAtMs <= Date.now() ? "past" : "upcoming");
|
|
121
|
+
lines.push(`Slot: ${slotState} (${slotLabel})`);
|
|
122
|
+
}
|
|
123
|
+
return lines.join("\n");
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const CronCalendar = ({
|
|
127
|
+
jobs = [],
|
|
128
|
+
usageByJobId = {},
|
|
129
|
+
runsByJobId = {},
|
|
130
|
+
onSelectJob = () => {},
|
|
131
|
+
}) => {
|
|
132
|
+
const [expanded, setExpanded] = useState(() => {
|
|
133
|
+
const settings = readUiSettings();
|
|
134
|
+
return settings[kCalendarExpandedUiSettingKey] === true;
|
|
135
|
+
});
|
|
136
|
+
const toggleExpanded = useCallback(() => {
|
|
137
|
+
setExpanded((previous) => {
|
|
138
|
+
const next = !previous;
|
|
139
|
+
updateUiSettings((settings) => ({ ...settings, [kCalendarExpandedUiSettingKey]: next }));
|
|
140
|
+
return next;
|
|
141
|
+
});
|
|
142
|
+
}, []);
|
|
143
|
+
|
|
144
|
+
const [nowMs, setNowMs] = useState(() => Date.now());
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
const intervalId = window.setInterval(() => {
|
|
147
|
+
setNowMs(Date.now());
|
|
148
|
+
}, kNowRefreshMs);
|
|
149
|
+
return () => {
|
|
150
|
+
window.clearInterval(intervalId);
|
|
151
|
+
};
|
|
152
|
+
}, []);
|
|
153
|
+
const todayDayKey = toLocalDayKey(nowMs);
|
|
154
|
+
const { repeatingJobs, scheduledJobs } = useMemo(
|
|
155
|
+
() => classifyRepeatingJobs(jobs),
|
|
156
|
+
[jobs],
|
|
157
|
+
);
|
|
158
|
+
const timeline = useMemo(
|
|
159
|
+
() => expandJobsToRollingSlots({ jobs: scheduledJobs, nowMs }),
|
|
160
|
+
[scheduledJobs, nowMs],
|
|
161
|
+
);
|
|
162
|
+
const statusBySlotKey = useMemo(
|
|
163
|
+
() => mapRunStatusesToSlots({ slots: timeline.slots, bulkRunsByJobId: runsByJobId, nowMs }),
|
|
164
|
+
[timeline.slots, runsByJobId, nowMs],
|
|
165
|
+
);
|
|
166
|
+
const tokenTierByJobId = useMemo(
|
|
167
|
+
() => buildTokenTierByJobId({ jobs, usageByJobId }),
|
|
168
|
+
[jobs, usageByJobId],
|
|
169
|
+
);
|
|
170
|
+
const jobById = useMemo(
|
|
171
|
+
() =>
|
|
172
|
+
jobs.reduce((accumulator, job) => {
|
|
173
|
+
const jobId = String(job?.id || "");
|
|
174
|
+
if (jobId) accumulator[jobId] = job;
|
|
175
|
+
return accumulator;
|
|
176
|
+
}, {}),
|
|
177
|
+
[jobs],
|
|
178
|
+
);
|
|
179
|
+
const latestRunByJobId = useMemo(
|
|
180
|
+
() =>
|
|
181
|
+
Object.entries(runsByJobId || {}).reduce((accumulator, [jobId, runResult]) => {
|
|
182
|
+
const entries = Array.isArray(runResult?.entries) ? runResult.entries : [];
|
|
183
|
+
const latest = entries
|
|
184
|
+
.filter((entry) => Number(entry?.ts || 0) > 0)
|
|
185
|
+
.sort((left, right) => Number(right?.ts || 0) - Number(left?.ts || 0))[0];
|
|
186
|
+
accumulator[jobId] = latest || null;
|
|
187
|
+
return accumulator;
|
|
188
|
+
}, {}),
|
|
189
|
+
[runsByJobId],
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const upcomingSlots = useMemo(
|
|
193
|
+
() => getUpcomingSlots({ slots: timeline.slots, nowMs }),
|
|
194
|
+
[timeline.slots, nowMs],
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const hourRows = useMemo(() => {
|
|
198
|
+
const uniqueHours = new Set(timeline.slots.map((slot) => slot.hourOfDay));
|
|
199
|
+
return [...uniqueHours].sort((left, right) => left - right);
|
|
200
|
+
}, [timeline.slots]);
|
|
201
|
+
|
|
202
|
+
const slotsByCellKey = useMemo(
|
|
203
|
+
() =>
|
|
204
|
+
timeline.slots.reduce((accumulator, slot) => {
|
|
205
|
+
const cellKey = buildCellKey(slot.dayKey, slot.hourOfDay);
|
|
206
|
+
const currentValue = accumulator[cellKey] || [];
|
|
207
|
+
currentValue.push(slot);
|
|
208
|
+
accumulator[cellKey] = currentValue;
|
|
209
|
+
return accumulator;
|
|
210
|
+
}, {}),
|
|
211
|
+
[timeline.slots],
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const totalUpcoming = useMemo(
|
|
215
|
+
() => timeline.slots.filter((slot) => slot.scheduledAtMs > nowMs).length,
|
|
216
|
+
[timeline.slots, nowMs],
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const renderCompactStrip = () => {
|
|
220
|
+
return html`
|
|
221
|
+
${upcomingSlots.length === 0
|
|
222
|
+
? html`<div class="text-xs text-gray-500 py-1">No upcoming jobs in the next 24 hours.</div>`
|
|
223
|
+
: html`
|
|
224
|
+
<div class="cron-calendar-compact-strip">
|
|
225
|
+
${upcomingSlots.map((slot) => {
|
|
226
|
+
const usage = usageByJobId[slot.jobId] || {};
|
|
227
|
+
const tooltipText = buildJobTooltipText({
|
|
228
|
+
jobName: slot.jobName,
|
|
229
|
+
job: jobById[slot.jobId] || null,
|
|
230
|
+
usage,
|
|
231
|
+
latestRun: latestRunByJobId[slot.jobId],
|
|
232
|
+
scheduledAtMs: slot.scheduledAtMs,
|
|
233
|
+
});
|
|
234
|
+
return html`
|
|
235
|
+
<${Tooltip}
|
|
236
|
+
text=${tooltipText}
|
|
237
|
+
widthClass="w-72"
|
|
238
|
+
tooltipClassName="whitespace-pre-line"
|
|
239
|
+
triggerClassName="inline-flex max-w-full"
|
|
240
|
+
>
|
|
241
|
+
<div
|
|
242
|
+
key=${slot.key}
|
|
243
|
+
class="cron-calendar-compact-chip cron-calendar-slot-tier-unknown cron-calendar-slot-upcoming"
|
|
244
|
+
role="button"
|
|
245
|
+
tabindex="0"
|
|
246
|
+
onClick=${() => onSelectJob(slot.jobId)}
|
|
247
|
+
onKeyDown=${(event) => {
|
|
248
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
249
|
+
event.preventDefault();
|
|
250
|
+
onSelectJob(slot.jobId);
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
<span class="cron-calendar-compact-time">${formatUpcomingTime(slot.scheduledAtMs)}</span>
|
|
254
|
+
<span class="truncate">${slot.jobName}</span>
|
|
255
|
+
</div>
|
|
256
|
+
</${Tooltip}>
|
|
257
|
+
`;
|
|
258
|
+
})}
|
|
259
|
+
${Math.max(0, totalUpcoming - upcomingSlots.length) > 0
|
|
260
|
+
? html`
|
|
261
|
+
<button
|
|
262
|
+
class="text-[11px] text-gray-500 hover:text-gray-300 self-center transition-colors"
|
|
263
|
+
onClick=${toggleExpanded}
|
|
264
|
+
>+${Math.max(0, totalUpcoming - upcomingSlots.length)} more this week</button>
|
|
265
|
+
`
|
|
266
|
+
: null}
|
|
267
|
+
</div>
|
|
268
|
+
`}
|
|
269
|
+
`;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const renderFullGrid = () => html`
|
|
273
|
+
<div class="space-y-3">
|
|
274
|
+
<div class="flex justify-end">
|
|
275
|
+
<${renderLegend} />
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
${hourRows.length === 0
|
|
279
|
+
? html`<div class="text-sm text-gray-500">No scheduled jobs in this rolling window.</div>`
|
|
280
|
+
: html`
|
|
281
|
+
<div class="cron-calendar-grid-wrap">
|
|
282
|
+
<div class="cron-calendar-grid-header">
|
|
283
|
+
<div class="cron-calendar-hour-cell"></div>
|
|
284
|
+
${timeline.days.map(
|
|
285
|
+
(day) => html`
|
|
286
|
+
<div
|
|
287
|
+
key=${day.dayKey}
|
|
288
|
+
class=${`cron-calendar-day-header ${day.dayKey === todayDayKey ? "is-today" : ""}`}
|
|
289
|
+
>
|
|
290
|
+
${day.label}
|
|
291
|
+
</div>
|
|
292
|
+
`,
|
|
293
|
+
)}
|
|
294
|
+
</div>
|
|
295
|
+
<div class="cron-calendar-grid-body">
|
|
296
|
+
${hourRows.map((hourOfDay) => html`
|
|
297
|
+
<div key=${hourOfDay} class="cron-calendar-grid-row">
|
|
298
|
+
<div class="cron-calendar-hour-cell">${formatHourLabel(hourOfDay)}</div>
|
|
299
|
+
${timeline.days.map((day) => {
|
|
300
|
+
const cellKey = buildCellKey(day.dayKey, hourOfDay);
|
|
301
|
+
const cellSlots = slotsByCellKey[cellKey] || [];
|
|
302
|
+
const visibleSlots = cellSlots.slice(0, 3);
|
|
303
|
+
const overflowCount = Math.max(0, cellSlots.length - visibleSlots.length);
|
|
304
|
+
return html`
|
|
305
|
+
<div
|
|
306
|
+
key=${cellKey}
|
|
307
|
+
class=${`cron-calendar-grid-cell ${day.dayKey === todayDayKey ? "is-today" : ""}`}
|
|
308
|
+
>
|
|
309
|
+
${visibleSlots.map((slot) => {
|
|
310
|
+
const status = statusBySlotKey[slot.key] || "";
|
|
311
|
+
const isPast = slot.scheduledAtMs <= nowMs;
|
|
312
|
+
const tokenTier = tokenTierByJobId[slot.jobId] || "unknown";
|
|
313
|
+
const usage = usageByJobId[slot.jobId] || {};
|
|
314
|
+
const tooltipText = buildJobTooltipText({
|
|
315
|
+
jobName: slot.jobName,
|
|
316
|
+
job: jobById[slot.jobId] || null,
|
|
317
|
+
usage,
|
|
318
|
+
latestRun: latestRunByJobId[slot.jobId],
|
|
319
|
+
scheduledAtMs: slot.scheduledAtMs,
|
|
320
|
+
scheduledStatus: status,
|
|
321
|
+
});
|
|
322
|
+
return html`
|
|
323
|
+
<${Tooltip}
|
|
324
|
+
text=${tooltipText}
|
|
325
|
+
widthClass="w-72"
|
|
326
|
+
tooltipClassName="whitespace-pre-line"
|
|
327
|
+
triggerClassName="inline-flex w-full"
|
|
328
|
+
>
|
|
329
|
+
<div
|
|
330
|
+
key=${slot.key}
|
|
331
|
+
class=${`cron-calendar-slot-chip ${slotStateClassName({
|
|
332
|
+
isPast,
|
|
333
|
+
mappedStatus: status,
|
|
334
|
+
tokenTier,
|
|
335
|
+
})}`}
|
|
336
|
+
role="button"
|
|
337
|
+
tabindex="0"
|
|
338
|
+
onClick=${() => onSelectJob(slot.jobId)}
|
|
339
|
+
onKeyDown=${(event) => {
|
|
340
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
341
|
+
event.preventDefault();
|
|
342
|
+
onSelectJob(slot.jobId);
|
|
343
|
+
}}
|
|
344
|
+
>
|
|
345
|
+
<span class="truncate">${slot.jobName}</span>
|
|
346
|
+
</div>
|
|
347
|
+
</${Tooltip}>
|
|
348
|
+
`;
|
|
349
|
+
})}
|
|
350
|
+
${overflowCount > 0
|
|
351
|
+
? html`<div class="cron-calendar-slot-overflow">+${overflowCount} more</div>`
|
|
352
|
+
: null}
|
|
353
|
+
</div>
|
|
354
|
+
`;
|
|
355
|
+
})}
|
|
356
|
+
</div>
|
|
357
|
+
`)}
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
`}
|
|
361
|
+
|
|
362
|
+
${repeatingJobs.length > 0
|
|
363
|
+
? html`
|
|
364
|
+
<div class="cron-calendar-repeating-strip">
|
|
365
|
+
<div class="text-xs text-gray-500">Repeating</div>
|
|
366
|
+
<div class="cron-calendar-repeating-list">
|
|
367
|
+
${repeatingJobs.map((job) => {
|
|
368
|
+
const jobId = String(job?.id || "");
|
|
369
|
+
const usage = usageByJobId[jobId] || {};
|
|
370
|
+
const avgTokensPerRun = Number(usage?.avgTokensPerRun || 0);
|
|
371
|
+
const tooltipText = buildJobTooltipText({
|
|
372
|
+
jobName: job.name || job.id,
|
|
373
|
+
job,
|
|
374
|
+
usage,
|
|
375
|
+
latestRun: latestRunByJobId[jobId],
|
|
376
|
+
});
|
|
377
|
+
return html`
|
|
378
|
+
<${Tooltip}
|
|
379
|
+
text=${tooltipText}
|
|
380
|
+
widthClass="w-72"
|
|
381
|
+
tooltipClassName="whitespace-pre-line"
|
|
382
|
+
triggerClassName="inline-flex max-w-full"
|
|
383
|
+
>
|
|
384
|
+
<div
|
|
385
|
+
class=${`cron-calendar-repeating-pill ${slotStateClassName({
|
|
386
|
+
isPast: false,
|
|
387
|
+
mappedStatus: "",
|
|
388
|
+
tokenTier: tokenTierByJobId[jobId] || "unknown",
|
|
389
|
+
})}`}
|
|
390
|
+
role="button"
|
|
391
|
+
tabindex="0"
|
|
392
|
+
onClick=${() => onSelectJob(jobId)}
|
|
393
|
+
onKeyDown=${(event) => {
|
|
394
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
395
|
+
event.preventDefault();
|
|
396
|
+
onSelectJob(jobId);
|
|
397
|
+
}}
|
|
398
|
+
>
|
|
399
|
+
<span class="truncate">${job.name || job.id}</span>
|
|
400
|
+
<span class="text-[10px] opacity-80">
|
|
401
|
+
${formatCronScheduleLabel(job.schedule, {
|
|
402
|
+
includeTimeZoneWhenDifferent: true,
|
|
403
|
+
})}
|
|
404
|
+
${avgTokensPerRun > 0
|
|
405
|
+
? ` | avg ${formatTokenCount(avgTokensPerRun)} tk`
|
|
406
|
+
: ""}
|
|
407
|
+
</span>
|
|
408
|
+
</div>
|
|
409
|
+
</${Tooltip}>
|
|
410
|
+
`;
|
|
411
|
+
})}
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
`
|
|
415
|
+
: null}
|
|
416
|
+
</div>
|
|
417
|
+
`;
|
|
418
|
+
|
|
419
|
+
return html`
|
|
420
|
+
<section class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
421
|
+
<div class="flex items-center justify-between gap-2">
|
|
422
|
+
<div class="flex items-center gap-2">
|
|
423
|
+
<h3 class="card-label cron-calendar-title">
|
|
424
|
+
${expanded ? "Rolling 7-Day Schedule" : "Upcoming (next 24h)"}
|
|
425
|
+
</h3>
|
|
426
|
+
${!expanded && repeatingJobs.length > 0
|
|
427
|
+
? html`<span class="text-[11px] text-gray-500">+ ${repeatingJobs.length} repeating</span>`
|
|
428
|
+
: null}
|
|
429
|
+
</div>
|
|
430
|
+
<${ActionButton}
|
|
431
|
+
onClick=${toggleExpanded}
|
|
432
|
+
tone="neutral"
|
|
433
|
+
size="sm"
|
|
434
|
+
idleLabel=${expanded ? "Collapse" : "Show full week"}
|
|
435
|
+
/>
|
|
436
|
+
</div>
|
|
437
|
+
|
|
438
|
+
${expanded ? renderFullGrid() : renderCompactStrip()}
|
|
439
|
+
</section>
|
|
440
|
+
`;
|
|
441
|
+
};
|