Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]
import { html, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { t } from "../../i18n/index.ts";
import type {
CronFieldErrors,
CronFieldKey,
CronJobsLastStatusFilter,
CronJobsScheduleKindFilter,
} from "../controllers/cron.ts";
import { formatRelativeTimestamp, formatMs } from "../format.ts";
import { pathForTab } from "../navigation.ts";
import { formatCronSchedule, formatNextRun } from "../presenter.ts";
import type { ChannelUiMetaEntry, CronJob, CronRunLogEntry, CronStatus } from "../types.ts";
import type {
CronDeliveryStatus,
CronJobsEnabledFilter,
CronRunScope,
CronRunsStatusValue,
CronJobsSortBy,
CronRunsStatusFilter,
CronSortDir,
} from "../types.ts";
import type { CronFormState } from "../ui-types.ts";
export type CronProps = {
basePath: string;
loading: boolean;
jobsLoadingMore: boolean;
status: CronStatus | null;
jobs: CronJob[];
jobsTotal: number;
jobsHasMore: boolean;
jobsQuery: string;
jobsEnabledFilter: CronJobsEnabledFilter;
jobsScheduleKindFilter: CronJobsScheduleKindFilter;
jobsLastStatusFilter: CronJobsLastStatusFilter;
jobsSortBy: CronJobsSortBy;
jobsSortDir: CronSortDir;
error: string | null;
busy: boolean;
form: CronFormState;
fieldErrors: CronFieldErrors;
canSubmit: boolean;
editingJobId: string | null;
channels: string[];
channelLabels?: Record<string, string>;
channelMeta?: ChannelUiMetaEntry[];
runsJobId: string | null;
runs: CronRunLogEntry[];
runsTotal: number;
runsHasMore: boolean;
runsLoadingMore: boolean;
runsScope: CronRunScope;
runsStatuses: CronRunsStatusValue[];
runsDeliveryStatuses: CronDeliveryStatus[];
runsStatusFilter: CronRunsStatusFilter;
runsQuery: string;
runsSortDir: CronSortDir;
agentSuggestions: string[];
modelSuggestions: string[];
thinkingSuggestions: string[];
timezoneSuggestions: string[];
deliveryToSuggestions: string[];
accountSuggestions: string[];
onFormChange: (patch: Partial<CronFormState>) => void;
onRefresh: () => void;
onAdd: () => void;
onEdit: (job: CronJob) => void;
onClone: (job: CronJob) => void;
onCancelEdit: () => void;
onToggle: (job: CronJob, enabled: boolean) => void;
onRun: (job: CronJob, mode?: "force" | "due") => void;
onRemove: (job: CronJob) => void;
/** Open the simplified creation wizard. */
onQuickCreate?: () => void;
onLoadRuns: (jobId: string) => void;
onLoadMoreJobs: () => void;
onJobsFiltersChange: (patch: {
cronJobsQuery?: string;
cronJobsEnabledFilter?: CronJobsEnabledFilter;
cronJobsScheduleKindFilter?: CronJobsScheduleKindFilter;
cronJobsLastStatusFilter?: CronJobsLastStatusFilter;
cronJobsSortBy?: CronJobsSortBy;
cronJobsSortDir?: CronSortDir;
}) => void | Promise<void>;
onJobsFiltersReset: () => void | Promise<void>;
onLoadMoreRuns: () => void;
onRunsFiltersChange: (patch: {
cronRunsScope?: CronRunScope;
cronRunsStatuses?: CronRunsStatusValue[];
cronRunsDeliveryStatuses?: CronDeliveryStatus[];
cronRunsStatusFilter?: CronRunsStatusFilter;
cronRunsQuery?: string;
cronRunsSortDir?: CronSortDir;
}) => void | Promise<void>;
onNavigateToChat?: (sessionKey: string) => void;
};
function getRunStatusOptions(): Array<{ value: CronRunsStatusValue; label: string }> {
return [
{ value: "ok", label: t("cron.runs.runStatusOk") },
{ value: "error", label: t("cron.runs.runStatusError") },
{ value: "skipped", label: t("cron.runs.runStatusSkipped") },
];
}
function getRunDeliveryOptions(): Array<{ value: CronDeliveryStatus; label: string }> {
return [
{ value: "delivered", label: t("cron.runs.deliveryDelivered") },
{ value: "not-delivered", label: t("cron.runs.deliveryNotDelivered") },
{ value: "unknown", label: t("cron.runs.deliveryUnknown") },
{ value: "not-requested", label: t("cron.runs.deliveryNotRequested") },
];
}
function toggleSelection<T extends string>(selected: T[], value: T, checked: boolean): T[] {
const set = new Set(selected);
if (checked) {
set.add(value);
} else {
set.delete(value);
}
return Array.from(set);
}
function summarizeSelection(selectedLabels: string[], allLabel: string) {
if (selectedLabels.length === 0) {
return allLabel;
}
if (selectedLabels.length <= 2) {
return selectedLabels.join(", ");
}
return `${selectedLabels[0]} +${selectedLabels.length - 1}`;
}
function buildChannelOptions(props: CronProps): string[] {
const options = ["last", ...props.channels.filter(Boolean)];
const current = props.form.deliveryChannel?.trim();
if (current && !options.includes(current)) {
options.push(current);
}
const seen = new Set<string>();
return options.filter((value) => {
if (seen.has(value)) {
return false;
}
seen.add(value);
return true;
});
}
function resolveChannelLabel(props: CronProps, channel: string): string {
if (channel === "last") {
return "last";
}
const meta = props.channelMeta?.find((entry) => entry.id === channel);
if (meta?.label) {
return meta.label;
}
return props.channelLabels?.[channel] ?? channel;
}
function renderRunFilterDropdown(params: {
id: string;
title: string;
summary: string;
options: Array<{ value: string; label: string }>;
selected: string[];
onToggle: (value: string, checked: boolean) => void;
onClear: () => void;
}) {
return html`
<div class="field cron-filter-dropdown" data-filter=${params.id}>
<span>${params.title}</span>
<details class="cron-filter-dropdown__details">
<summary class="btn cron-filter-dropdown__trigger">
<span>${params.summary}</span>
</summary>
<div class="cron-filter-dropdown__panel">
<div class="cron-filter-dropdown__list">
${params.options.map(
(option) => html`
<label class="cron-filter-dropdown__option">
<input
type="checkbox"
value=${option.value}
.checked=${params.selected.includes(option.value)}
@change=${(event: Event) => {
const target = event.target as HTMLInputElement;
params.onToggle(option.value, target.checked);
}}
/>
<span>${option.label}</span>
</label>
`,
)}
</div>
<div class="row">
<button class="btn" type="button" @click=${params.onClear}>
${t("cron.runs.clear")}
</button>
</div>
</div>
</details>
</div>
`;
}
function renderSuggestionList(id: string, options: string[]) {
const clean = Array.from(new Set(options.map((option) => option.trim()).filter(Boolean)));
if (clean.length === 0) {
return nothing;
}
return html`<datalist id=${id}>
${clean.map((value) => html`<option value=${value}></option> `)}
</datalist>`;
}
type BlockingField = {
key: CronFieldKey;
label: string;
message: string;
inputId: string;
};
function errorIdForField(key: CronFieldKey) {
return `cron-error-${key}`;
}
function inputIdForField(key: CronFieldKey) {
if (key === "name") {
return "cron-name";
}
if (key === "scheduleAt") {
return "cron-schedule-at";
}
if (key === "everyAmount") {
return "cron-every-amount";
}
if (key === "cronExpr") {
return "cron-cron-expr";
}
if (key === "staggerAmount") {
return "cron-stagger-amount";
}
if (key === "payloadText") {
return "cron-payload-text";
}
if (key === "payloadModel") {
return "cron-payload-model";
}
if (key === "payloadThinking") {
return "cron-payload-thinking";
}
if (key === "timeoutSeconds") {
return "cron-timeout-seconds";
}
if (key === "failureAlertAfter") {
return "cron-failure-alert-after";
}
if (key === "failureAlertCooldownSeconds") {
return "cron-failure-alert-cooldown-seconds";
}
return "cron-delivery-to";
}
function fieldLabelForKey(
key: CronFieldKey,
form: CronFormState,
deliveryMode: CronFormState["deliveryMode"],
) {
if (key === "payloadText") {
return form.payloadKind === "systemEvent"
? t("cron.form.mainTimelineMessage")
: t("cron.form.assistantTaskPrompt");
}
if (key === "deliveryTo") {
return deliveryMode === "webhook" ? t("cron.form.webhookUrl") : t("cron.form.to");
}
const labels: Record<CronFieldKey, string> = {
name: t("cron.form.fieldName"),
scheduleAt: t("cron.form.runAt"),
everyAmount: t("cron.form.every"),
cronExpr: t("cron.form.expression"),
staggerAmount: t("cron.form.staggerWindow"),
payloadText: t("cron.form.assistantTaskPrompt"),
payloadModel: t("cron.form.model"),
payloadThinking: t("cron.form.thinking"),
timeoutSeconds: t("cron.form.timeoutSeconds"),
deliveryTo: t("cron.form.to"),
failureAlertAfter: "Failure alert after",
failureAlertCooldownSeconds: "Failure alert cooldown",
};
return labels[key];
}
function collectBlockingFields(
errors: CronFieldErrors,
form: CronFormState,
deliveryMode: CronFormState["deliveryMode"],
): BlockingField[] {
const orderedKeys: CronFieldKey[] = [
"name",
"scheduleAt",
"everyAmount",
"cronExpr",
"staggerAmount",
"payloadText",
"payloadModel",
"payloadThinking",
"timeoutSeconds",
"deliveryTo",
"failureAlertAfter",
"failureAlertCooldownSeconds",
];
const fields: BlockingField[] = [];
for (const key of orderedKeys) {
const message = errors[key];
if (!message) {
continue;
}
fields.push({
key,
label: fieldLabelForKey(key, form, deliveryMode),
message,
inputId: inputIdForField(key),
});
}
return fields;
}
function focusFormField(id: string) {
const el = document.getElementById(id);
if (!(el instanceof HTMLElement)) {
return;
}
if (typeof el.scrollIntoView === "function") {
el.scrollIntoView({ block: "center", behavior: "smooth" });
}
el.focus();
}
function renderFieldLabel(text: string, required = false) {
return html`<span>
${text}
${required
? html`
<span class="cron-required-marker" aria-hidden="true">*</span>
<span class="cron-required-sr">${t("cron.form.requiredSr")}</span>
`
: nothing}
</span>`;
}
export function renderCron(props: CronProps) {
const isEditing = Boolean(props.editingJobId);
const isAgentTurn = props.form.payloadKind === "agentTurn";
const isCronSchedule = props.form.scheduleKind === "cron";
const channelOptions = buildChannelOptions(props);
const selectedJob =
props.runsJobId == null ? undefined : props.jobs.find((job) => job.id === props.runsJobId);
const selectedRunTitle =
props.runsScope === "all"
? t("cron.jobList.allJobs")
: (selectedJob?.name ?? props.runsJobId ?? t("cron.jobList.selectJob"));
const runs = props.runs.toSorted((a, b) =>
props.runsSortDir === "asc" ? a.ts - b.ts : b.ts - a.ts,
);
const runStatusOptions = getRunStatusOptions();
const runDeliveryOptions = getRunDeliveryOptions();
const selectedStatusLabels = runStatusOptions
.filter((option) => props.runsStatuses.includes(option.value))
.map((option) => option.label);
const selectedDeliveryLabels = runDeliveryOptions
.filter((option) => props.runsDeliveryStatuses.includes(option.value))
.map((option) => option.label);
const statusSummary = summarizeSelection(selectedStatusLabels, t("cron.runs.allStatuses"));
const deliverySummary = summarizeSelection(selectedDeliveryLabels, t("cron.runs.allDelivery"));
const supportsAnnounce =
props.form.sessionTarget !== "main" && props.form.payloadKind === "agentTurn";
const selectedDeliveryMode =
props.form.deliveryMode === "announce" && !supportsAnnounce ? "none" : props.form.deliveryMode;
const blockingFields = collectBlockingFields(props.fieldErrors, props.form, selectedDeliveryMode);
const blockedByValidation = !props.busy && blockingFields.length > 0;
const hasActiveJobsFilters =
props.jobsQuery.trim().length > 0 ||
props.jobsEnabledFilter !== "all" ||
props.jobsScheduleKindFilter !== "all" ||
props.jobsLastStatusFilter !== "all" ||
props.jobsSortBy !== "nextRunAtMs" ||
props.jobsSortDir !== "asc";
const submitDisabledReason =
blockedByValidation && !props.canSubmit
? blockingFields.length === 1
? t("cron.form.fixFields", { count: String(blockingFields.length) })
: t("cron.form.fixFieldsPlural", { count: String(blockingFields.length) })
: "";
return html`
<section class="card cron-summary-strip">
<div class="cron-summary-strip__left">
<div class="cron-summary-item">
<div class="cron-summary-label">${t("cron.summary.enabled")}</div>
<div class="cron-summary-value">
<span class=${`chip ${props.status?.enabled ? "chip-ok" : "chip-danger"}`}>
${props.status
? props.status.enabled
? t("cron.summary.yes")
: t("cron.summary.no")
: t("common.na")}
</span>
</div>
</div>
<div class="cron-summary-item">
<div class="cron-summary-label">${t("cron.summary.jobs")}</div>
<div class="cron-summary-value">${props.status?.jobs ?? t("common.na")}</div>
</div>
<div class="cron-summary-item cron-summary-item--wide">
<div class="cron-summary-label">${t("cron.summary.nextWake")}</div>
<div class="cron-summary-value">${formatNextRun(props.status?.nextWakeAtMs ?? null)}</div>
</div>
</div>
<div class="cron-summary-strip__actions">
${props.onQuickCreate
? html` <button class="btn btn--primary" @click=${props.onQuickCreate}>+ New</button> `
: nothing}
<button
class=${props.loading ? "btn cron-refresh-btn--loading" : "btn"}
?disabled=${props.loading}
@click=${props.onRefresh}
>
${props.loading ? t("cron.summary.refreshing") : t("cron.summary.refresh")}
</button>
${props.error ? html`<span class="muted">${props.error}</span>` : nothing}
</div>
</section>
<section class="cron-workspace">
<div class="cron-workspace-main">
<section class="card">
<div
class="row"
style="justify-content: space-between; align-items: flex-start; gap: 12px;"
>
<div>
<div class="card-title">${t("cron.jobs.title")}</div>
<div class="card-sub">${t("cron.jobs.subtitle")}</div>
</div>
<div class="muted">
${t("cron.jobs.shownOf", {
shown: String(props.jobs.length),
total: String(props.jobsTotal),
})}
</div>
</div>
<div class="filters" style="margin-top: 12px;">
<label class="field cron-filter-search">
<span>${t("cron.jobs.searchJobs")}</span>
<input
.value=${props.jobsQuery}
placeholder=${t("cron.jobs.searchPlaceholder")}
@input=${(e: Event) =>
props.onJobsFiltersChange({
cronJobsQuery: (e.target as HTMLInputElement).value,
})}
/>
</label>
<label class="field">
<span>${t("cron.jobs.enabled")}</span>
<select
.value=${props.jobsEnabledFilter}
@change=${(e: Event) =>
props.onJobsFiltersChange({
cronJobsEnabledFilter: (e.target as HTMLSelectElement)
.value as CronJobsEnabledFilter,
})}
>
<option value="all">${t("cron.jobs.all")}</option>
<option value="enabled">${t("common.enabled")}</option>
<option value="disabled">${t("common.disabled")}</option>
</select>
</label>
<label class="field">
<span>${t("cron.jobs.schedule")}</span>
<select
data-test-id="cron-jobs-schedule-filter"
.value=${props.jobsScheduleKindFilter}
@change=${(e: Event) =>
props.onJobsFiltersChange({
cronJobsScheduleKindFilter: (e.target as HTMLSelectElement)
.value as CronJobsScheduleKindFilter,
})}
>
<option value="all">${t("cron.jobs.all")}</option>
<option value="at">${t("cron.form.at")}</option>
<option value="every">${t("cron.form.every")}</option>
<option value="cron">${t("cron.form.cronOption")}</option>
</select>
</label>
<label class="field">
<span>${t("cron.jobs.lastRun")}</span>
<select
data-test-id="cron-jobs-last-status-filter"
.value=${props.jobsLastStatusFilter}
@change=${(e: Event) =>
props.onJobsFiltersChange({
cronJobsLastStatusFilter: (e.target as HTMLSelectElement)
.value as CronJobsLastStatusFilter,
})}
>
<option value="all">${t("cron.jobs.all")}</option>
<option value="ok">${t("cron.runs.runStatusOk")}</option>
<option value="error">${t("cron.runs.runStatusError")}</option>
<option value="skipped">${t("cron.runs.runStatusSkipped")}</option>
</select>
</label>
<label class="field">
<span>${t("cron.jobs.sort")}</span>
<select
.value=${props.jobsSortBy}
@change=${(e: Event) =>
props.onJobsFiltersChange({
cronJobsSortBy: (e.target as HTMLSelectElement).value as CronJobsSortBy,
})}
>
<option value="nextRunAtMs">${t("cron.jobs.nextRun")}</option>
<option value="updatedAtMs">${t("cron.jobs.recentlyUpdated")}</option>
<option value="name">${t("cron.jobs.name")}</option>
</select>
</label>
<label class="field">
<span>${t("cron.jobs.direction")}</span>
<select
.value=${props.jobsSortDir}
@change=${(e: Event) =>
props.onJobsFiltersChange({
cronJobsSortDir: (e.target as HTMLSelectElement).value as CronSortDir,
})}
>
<option value="asc">${t("cron.jobs.ascending")}</option>
<option value="desc">${t("cron.jobs.descending")}</option>
</select>
</label>
<label class="field">
<span>${t("cron.jobs.reset")}</span>
<button
class="btn"
data-test-id="cron-jobs-filters-reset"
?disabled=${!hasActiveJobsFilters}
@click=${props.onJobsFiltersReset}
>
${t("cron.jobs.reset")}
</button>
</label>
</div>
${props.jobs.length === 0
? html` <div class="muted" style="margin-top: 12px">${t("cron.jobs.noMatching")}</div> `
: html`
<div class="list" style="margin-top: 12px;">
${props.jobs.map((job) => renderJob(job, props))}
</div>
`}
${props.jobsHasMore
? html`
<div class="row" style="margin-top: 12px">
<button
class="btn"
?disabled=${props.loading || props.jobsLoadingMore}
@click=${props.onLoadMoreJobs}
>
${props.jobsLoadingMore ? t("cron.jobs.loading") : t("cron.jobs.loadMore")}
</button>
</div>
`
: nothing}
</section>
<section class="card">
<div
class="row"
style="justify-content: space-between; align-items: flex-start; gap: 12px;"
>
<div>
<div class="card-title">${t("cron.runs.title")}</div>
<div class="card-sub">
${props.runsScope === "all"
? t("cron.runs.subtitleAll")
: t("cron.runs.subtitleJob", { title: selectedRunTitle })}
</div>
</div>
<div class="muted">
${t("cron.jobs.shownOf", {
shown: String(runs.length),
total: String(props.runsTotal),
})}
</div>
</div>
<div class="cron-run-filters">
<div class="cron-run-filters__row cron-run-filters__row--primary">
<label class="field">
<span>${t("cron.runs.scope")}</span>
<select
.value=${props.runsScope}
@change=${(e: Event) =>
props.onRunsFiltersChange({
cronRunsScope: (e.target as HTMLSelectElement).value as CronRunScope,
})}
>
<option value="all">${t("cron.runs.allJobs")}</option>
<option value="job" ?disabled=${props.runsJobId == null}>
${t("cron.runs.selectedJob")}
</option>
</select>
</label>
<label class="field cron-run-filter-search">
<span>${t("cron.runs.searchRuns")}</span>
<input
.value=${props.runsQuery}
placeholder=${t("cron.runs.searchPlaceholder")}
@input=${(e: Event) =>
props.onRunsFiltersChange({
cronRunsQuery: (e.target as HTMLInputElement).value,
})}
/>
</label>
<label class="field">
<span>${t("cron.jobs.sort")}</span>
<select
.value=${props.runsSortDir}
@change=${(e: Event) =>
props.onRunsFiltersChange({
cronRunsSortDir: (e.target as HTMLSelectElement).value as CronSortDir,
})}
>
<option value="desc">${t("cron.runs.newestFirst")}</option>
<option value="asc">${t("cron.runs.oldestFirst")}</option>
</select>
</label>
</div>
<div class="cron-run-filters__row cron-run-filters__row--secondary">
${renderRunFilterDropdown({
id: "status",
title: t("cron.runs.status"),
summary: statusSummary,
options: runStatusOptions,
selected: props.runsStatuses,
onToggle: (value, checked) => {
const next = toggleSelection(
props.runsStatuses,
value as CronRunsStatusValue,
checked,
);
void props.onRunsFiltersChange({ cronRunsStatuses: next });
},
onClear: () => {
void props.onRunsFiltersChange({ cronRunsStatuses: [] });
},
})}
${renderRunFilterDropdown({
id: "delivery",
title: t("cron.runs.delivery"),
summary: deliverySummary,
options: runDeliveryOptions,
selected: props.runsDeliveryStatuses,
onToggle: (value, checked) => {
const next = toggleSelection(
props.runsDeliveryStatuses,
value as CronDeliveryStatus,
checked,
);
void props.onRunsFiltersChange({ cronRunsDeliveryStatuses: next });
},
onClear: () => {
void props.onRunsFiltersChange({ cronRunsDeliveryStatuses: [] });
},
})}
</div>
</div>
${props.runsScope === "job" && props.runsJobId == null
? html`
<div class="muted" style="margin-top: 12px">${t("cron.runs.selectJobHint")}</div>
`
: runs.length === 0
? html`
<div class="muted" style="margin-top: 12px">${t("cron.runs.noMatching")}</div>
`
: html`
<div class="list" style="margin-top: 12px;">
${runs.map((entry) => renderRun(entry, props.basePath, props.onNavigateToChat))}
</div>
`}
${(props.runsScope === "all" || props.runsJobId != null) && props.runsHasMore
? html`
<div class="row" style="margin-top: 12px">
<button
class="btn"
?disabled=${props.runsLoadingMore}
@click=${props.onLoadMoreRuns}
>
${props.runsLoadingMore ? t("cron.jobs.loading") : t("cron.runs.loadMore")}
</button>
</div>
`
: nothing}
</section>
</div>
<section class="card cron-workspace-form">
<div class="card-title">${isEditing ? t("cron.form.editJob") : t("cron.form.newJob")}</div>
<div class="card-sub">
${isEditing ? t("cron.form.updateSubtitle") : t("cron.form.createSubtitle")}
</div>
<div class="cron-form">
<div class="cron-required-legend">
<span class="cron-required-marker" aria-hidden="true">*</span> ${t(
"cron.form.required",
)}
</div>
<section class="cron-form-section">
<div class="cron-form-section__title">${t("cron.form.basics")}</div>
<div class="cron-form-section__sub">${t("cron.form.basicsSub")}</div>
<div class="form-grid cron-form-grid">
<label class="field">
${renderFieldLabel(t("cron.form.fieldName"), true)}
<input
id="cron-name"
.value=${props.form.name}
placeholder=${t("cron.form.namePlaceholder")}
aria-invalid=${props.fieldErrors.name ? "true" : "false"}
aria-describedby=${ifDefined(
props.fieldErrors.name ? errorIdForField("name") : undefined,
)}
@input=${(e: Event) =>
props.onFormChange({ name: (e.target as HTMLInputElement).value })}
/>
${renderFieldError(props.fieldErrors.name, errorIdForField("name"))}
</label>
<label class="field">
<span>${t("cron.form.description")}</span>
<input
.value=${props.form.description}
placeholder=${t("cron.form.descriptionPlaceholder")}
@input=${(e: Event) =>
props.onFormChange({ description: (e.target as HTMLInputElement).value })}
/>
</label>
<label class="field">
${renderFieldLabel(t("cron.form.agentId"))}
<input
id="cron-agent-id"
.value=${props.form.agentId}
list="cron-agent-suggestions"
?disabled=${props.form.clearAgent}
@input=${(e: Event) =>
props.onFormChange({ agentId: (e.target as HTMLInputElement).value })}
placeholder=${t("cron.form.agentPlaceholder")}
/>
<div class="cron-help">${t("cron.form.agentHelp")}</div>
</label>
<label class="field checkbox cron-checkbox cron-checkbox-inline">
<input
type="checkbox"
.checked=${props.form.enabled}
@change=${(e: Event) =>
props.onFormChange({ enabled: (e.target as HTMLInputElement).checked })}
/>
<span class="field-checkbox__label">${t("cron.summary.enabled")}</span>
</label>
</div>
</section>
<section class="cron-form-section">
<div class="cron-form-section__title">${t("cron.form.schedule")}</div>
<div class="cron-form-section__sub">${t("cron.form.scheduleSub")}</div>
<div class="form-grid cron-form-grid">
<label class="field cron-span-2">
${renderFieldLabel(t("cron.form.schedule"))}
<select
id="cron-schedule-kind"
.value=${props.form.scheduleKind}
@change=${(e: Event) =>
props.onFormChange({
scheduleKind: (e.target as HTMLSelectElement)
.value as CronFormState["scheduleKind"],
})}
>
<option value="every">${t("cron.form.every")}</option>
<option value="at">${t("cron.form.at")}</option>
<option value="cron">${t("cron.form.cronOption")}</option>
</select>
</label>
</div>
${renderScheduleFields(props)}
</section>
<section class="cron-form-section">
<div class="cron-form-section__title">${t("cron.form.execution")}</div>
<div class="cron-form-section__sub">${t("cron.form.executionSub")}</div>
<div class="form-grid cron-form-grid">
<label class="field">
${renderFieldLabel(t("cron.form.session"))}
<select
id="cron-session-target"
.value=${props.form.sessionTarget}
@change=${(e: Event) =>
props.onFormChange({
sessionTarget: (e.target as HTMLSelectElement)
.value as CronFormState["sessionTarget"],
})}
>
<option value="main">${t("cron.form.main")}</option>
<option value="isolated">${t("cron.form.isolated")}</option>
</select>
<div class="cron-help">${t("cron.form.sessionHelp")}</div>
</label>
<label class="field">
${renderFieldLabel(t("cron.form.wakeMode"))}
<select
id="cron-wake-mode"
.value=${props.form.wakeMode}
@change=${(e: Event) =>
props.onFormChange({
wakeMode: (e.target as HTMLSelectElement).value as CronFormState["wakeMode"],
})}
>
<option value="now">${t("cron.form.now")}</option>
<option value="next-heartbeat">${t("cron.form.nextHeartbeat")}</option>
</select>
<div class="cron-help">${t("cron.form.wakeModeHelp")}</div>
</label>
<label class="field ${isAgentTurn ? "" : "cron-span-2"}">
${renderFieldLabel(t("cron.form.payloadKind"))}
<select
id="cron-payload-kind"
.value=${props.form.payloadKind}
@change=${(e: Event) =>
props.onFormChange({
payloadKind: (e.target as HTMLSelectElement)
.value as CronFormState["payloadKind"],
})}
>
<option value="systemEvent">${t("cron.form.systemEvent")}</option>
<option value="agentTurn">${t("cron.form.agentTurn")}</option>
</select>
<div class="cron-help">
${props.form.payloadKind === "systemEvent"
? t("cron.form.systemEventHelp")
: t("cron.form.agentTurnHelp")}
</div>
</label>
${isAgentTurn
? html`
<label class="field">
${renderFieldLabel(t("cron.form.timeoutSeconds"))}
<input
id="cron-timeout-seconds"
.value=${props.form.timeoutSeconds}
placeholder=${t("cron.form.timeoutPlaceholder")}
aria-invalid=${props.fieldErrors.timeoutSeconds ? "true" : "false"}
aria-describedby=${ifDefined(
props.fieldErrors.timeoutSeconds
? errorIdForField("timeoutSeconds")
: undefined,
)}
@input=${(e: Event) =>
props.onFormChange({
timeoutSeconds: (e.target as HTMLInputElement).value,
})}
/>
<div class="cron-help">${t("cron.form.timeoutHelp")}</div>
${renderFieldError(
props.fieldErrors.timeoutSeconds,
errorIdForField("timeoutSeconds"),
)}
</label>
`
: nothing}
</div>
<label class="field cron-span-2">
${renderFieldLabel(
props.form.payloadKind === "systemEvent"
? t("cron.form.mainTimelineMessage")
: t("cron.form.assistantTaskPrompt"),
true,
)}
<textarea
id="cron-payload-text"
.value=${props.form.payloadText}
aria-invalid=${props.fieldErrors.payloadText ? "true" : "false"}
aria-describedby=${ifDefined(
props.fieldErrors.payloadText ? errorIdForField("payloadText") : undefined,
)}
@input=${(e: Event) =>
props.onFormChange({
payloadText: (e.target as HTMLTextAreaElement).value,
})}
rows="4"
></textarea>
${renderFieldError(props.fieldErrors.payloadText, errorIdForField("payloadText"))}
</label>
</section>
<section class="cron-form-section">
<div class="cron-form-section__title">${t("cron.form.deliverySection")}</div>
<div class="cron-form-section__sub">${t("cron.form.deliverySub")}</div>
<div class="form-grid cron-form-grid">
<label class="field ${selectedDeliveryMode === "none" ? "cron-span-2" : ""}">
${renderFieldLabel(t("cron.form.resultDelivery"))}
<select
id="cron-delivery-mode"
.value=${selectedDeliveryMode}
@change=${(e: Event) =>
props.onFormChange({
deliveryMode: (e.target as HTMLSelectElement)
.value as CronFormState["deliveryMode"],
})}
>
${supportsAnnounce
? html` <option value="announce">${t("cron.form.announceDefault")}</option> `
: nothing}
<option value="webhook">${t("cron.form.webhookPost")}</option>
<option value="none">${t("cron.form.noneInternal")}</option>
</select>
<div class="cron-help">${t("cron.form.deliveryHelp")}</div>
</label>
${selectedDeliveryMode !== "none"
? html`
<label class="field ${selectedDeliveryMode === "webhook" ? "cron-span-2" : ""}">
${renderFieldLabel(
selectedDeliveryMode === "webhook"
? t("cron.form.webhookUrl")
: t("cron.form.channel"),
selectedDeliveryMode === "webhook",
)}
${selectedDeliveryMode === "webhook"
? html`
<input
id="cron-delivery-to"
.value=${props.form.deliveryTo}
list="cron-delivery-to-suggestions"
aria-invalid=${props.fieldErrors.deliveryTo ? "true" : "false"}
aria-describedby=${ifDefined(
props.fieldErrors.deliveryTo
? errorIdForField("deliveryTo")
: undefined,
)}
@input=${(e: Event) =>
props.onFormChange({
deliveryTo: (e.target as HTMLInputElement).value,
})}
placeholder=${t("cron.form.webhookPlaceholder")}
/>
`
: html`
<select
id="cron-delivery-channel"
.value=${props.form.deliveryChannel || "last"}
@change=${(e: Event) =>
props.onFormChange({
deliveryChannel: (e.target as HTMLSelectElement).value,
})}
>
${channelOptions.map(
(channel) =>
html`<option value=${channel}>
${resolveChannelLabel(props, channel)}
</option>`,
)}
</select>
`}
${selectedDeliveryMode === "announce"
? html` <div class="cron-help">${t("cron.form.channelHelp")}</div> `
: html` <div class="cron-help">${t("cron.form.webhookHelp")}</div> `}
</label>
${selectedDeliveryMode === "announce"
? html`
<label class="field cron-span-2">
${renderFieldLabel(t("cron.form.to"))}
<input
id="cron-delivery-to"
.value=${props.form.deliveryTo}
list="cron-delivery-to-suggestions"
@input=${(e: Event) =>
props.onFormChange({
deliveryTo: (e.target as HTMLInputElement).value,
})}
placeholder=${t("cron.form.toPlaceholder")}
/>
<div class="cron-help">${t("cron.form.toHelp")}</div>
</label>
`
: nothing}
${selectedDeliveryMode === "webhook"
? renderFieldError(
props.fieldErrors.deliveryTo,
errorIdForField("deliveryTo"),
)
: nothing}
`
: nothing}
</div>
</section>
<details class="cron-advanced">
<summary class="cron-advanced__summary">${t("cron.form.advanced")}</summary>
<div class="cron-help">${t("cron.form.advancedHelp")}</div>
<div class="form-grid cron-form-grid">
<label class="field checkbox cron-checkbox">
<input
type="checkbox"
.checked=${props.form.deleteAfterRun}
@change=${(e: Event) =>
props.onFormChange({
deleteAfterRun: (e.target as HTMLInputElement).checked,
})}
/>
<span class="field-checkbox__label">${t("cron.form.deleteAfterRun")}</span>
<div class="cron-help">${t("cron.form.deleteAfterRunHelp")}</div>
</label>
<label class="field checkbox cron-checkbox">
<input
type="checkbox"
.checked=${props.form.clearAgent}
@change=${(e: Event) =>
props.onFormChange({
clearAgent: (e.target as HTMLInputElement).checked,
})}
/>
<span class="field-checkbox__label">${t("cron.form.clearAgentOverride")}</span>
<div class="cron-help">${t("cron.form.clearAgentHelp")}</div>
</label>
<label class="field cron-span-2">
${renderFieldLabel("Session key")}
<input
id="cron-session-key"
.value=${props.form.sessionKey}
@input=${(e: Event) =>
props.onFormChange({
sessionKey: (e.target as HTMLInputElement).value,
})}
placeholder="agent:main:main"
/>
<div class="cron-help">Optional routing key for job delivery and wake routing.</div>
</label>
${isCronSchedule
? html`
<label class="field checkbox cron-checkbox cron-span-2">
<input
type="checkbox"
.checked=${props.form.scheduleExact}
@change=${(e: Event) =>
props.onFormChange({
scheduleExact: (e.target as HTMLInputElement).checked,
})}
/>
<span class="field-checkbox__label">${t("cron.form.exactTiming")}</span>
<div class="cron-help">${t("cron.form.exactTimingHelp")}</div>
</label>
<div class="cron-stagger-group cron-span-2">
<label class="field">
${renderFieldLabel(t("cron.form.staggerWindow"))}
<input
id="cron-stagger-amount"
.value=${props.form.staggerAmount}
?disabled=${props.form.scheduleExact}
aria-invalid=${props.fieldErrors.staggerAmount ? "true" : "false"}
aria-describedby=${ifDefined(
props.fieldErrors.staggerAmount
? errorIdForField("staggerAmount")
: undefined,
)}
@input=${(e: Event) =>
props.onFormChange({
staggerAmount: (e.target as HTMLInputElement).value,
})}
placeholder=${t("cron.form.staggerPlaceholder")}
/>
${renderFieldError(
props.fieldErrors.staggerAmount,
errorIdForField("staggerAmount"),
)}
</label>
<label class="field">
<span>${t("cron.form.staggerUnit")}</span>
<select
.value=${props.form.staggerUnit}
?disabled=${props.form.scheduleExact}
@change=${(e: Event) =>
props.onFormChange({
staggerUnit: (e.target as HTMLSelectElement)
.value as CronFormState["staggerUnit"],
})}
>
<option value="seconds">${t("cron.form.seconds")}</option>
<option value="minutes">${t("cron.form.minutes")}</option>
</select>
</label>
</div>
`
: nothing}
${isAgentTurn
? html`
<label class="field cron-span-2">
${renderFieldLabel("Account ID")}
<input
id="cron-delivery-account-id"
.value=${props.form.deliveryAccountId}
list="cron-delivery-account-suggestions"
?disabled=${selectedDeliveryMode !== "announce"}
@input=${(e: Event) =>
props.onFormChange({
deliveryAccountId: (e.target as HTMLInputElement).value,
})}
placeholder="default"
/>
<div class="cron-help">
Optional channel account ID for multi-account setups.
</div>
</label>
<label class="field checkbox cron-checkbox cron-span-2">
<input
type="checkbox"
.checked=${props.form.payloadLightContext}
@change=${(e: Event) =>
props.onFormChange({
payloadLightContext: (e.target as HTMLInputElement).checked,
})}
/>
<span class="field-checkbox__label">Light context</span>
<div class="cron-help">
Use lightweight bootstrap context for this agent job.
</div>
</label>
<label class="field">
${renderFieldLabel(t("cron.form.model"))}
<input
id="cron-payload-model"
.value=${props.form.payloadModel}
list="cron-model-suggestions"
@input=${(e: Event) =>
props.onFormChange({
payloadModel: (e.target as HTMLInputElement).value,
})}
placeholder=${t("cron.form.modelPlaceholder")}
/>
<div class="cron-help">${t("cron.form.modelHelp")}</div>
</label>
<label class="field">
${renderFieldLabel(t("cron.form.thinking"))}
<input
id="cron-payload-thinking"
.value=${props.form.payloadThinking}
list="cron-thinking-suggestions"
@input=${(e: Event) =>
props.onFormChange({
payloadThinking: (e.target as HTMLInputElement).value,
})}
placeholder=${t("cron.form.thinkingPlaceholder")}
/>
<div class="cron-help">${t("cron.form.thinkingHelp")}</div>
</label>
`
: nothing}
${isAgentTurn
? html`
<label class="field cron-span-2">
${renderFieldLabel("Failure alerts")}
<select
.value=${props.form.failureAlertMode}
@change=${(e: Event) =>
props.onFormChange({
failureAlertMode: (e.target as HTMLSelectElement)
.value as CronFormState["failureAlertMode"],
})}
>
<option value="inherit">Inherit global setting</option>
<option value="disabled">Disable for this job</option>
<option value="custom">Custom per-job settings</option>
</select>
<div class="cron-help">
Control when this job sends repeated-failure alerts.
</div>
</label>
${props.form.failureAlertMode === "custom"
? html`
<label class="field">
${renderFieldLabel("Alert after")}
<input
id="cron-failure-alert-after"
.value=${props.form.failureAlertAfter}
aria-invalid=${props.fieldErrors.failureAlertAfter ? "true" : "false"}
aria-describedby=${ifDefined(
props.fieldErrors.failureAlertAfter
? errorIdForField("failureAlertAfter")
: undefined,
)}
@input=${(e: Event) =>
props.onFormChange({
failureAlertAfter: (e.target as HTMLInputElement).value,
})}
placeholder="2"
/>
<div class="cron-help">Consecutive errors before alerting.</div>
${renderFieldError(
props.fieldErrors.failureAlertAfter,
errorIdForField("failureAlertAfter"),
)}
</label>
<label class="field">
${renderFieldLabel("Cooldown (seconds)")}
<input
id="cron-failure-alert-cooldown-seconds"
.value=${props.form.failureAlertCooldownSeconds}
aria-invalid=${props.fieldErrors.failureAlertCooldownSeconds
? "true"
: "false"}
aria-describedby=${ifDefined(
props.fieldErrors.failureAlertCooldownSeconds
? errorIdForField("failureAlertCooldownSeconds")
: undefined,
)}
@input=${(e: Event) =>
props.onFormChange({
failureAlertCooldownSeconds: (e.target as HTMLInputElement).value,
})}
placeholder="3600"
/>
<div class="cron-help">Minimum seconds between alerts.</div>
${renderFieldError(
props.fieldErrors.failureAlertCooldownSeconds,
errorIdForField("failureAlertCooldownSeconds"),
)}
</label>
<label class="field">
${renderFieldLabel("Alert channel")}
<select
.value=${props.form.failureAlertChannel || "last"}
@change=${(e: Event) =>
props.onFormChange({
failureAlertChannel: (e.target as HTMLSelectElement).value,
})}
>
${channelOptions.map(
(channel) =>
html`<option value=${channel}>
${resolveChannelLabel(props, channel)}
</option>`,
)}
</select>
</label>
<label class="field">
${renderFieldLabel("Alert to")}
<input
.value=${props.form.failureAlertTo}
list="cron-delivery-to-suggestions"
@input=${(e: Event) =>
props.onFormChange({
failureAlertTo: (e.target as HTMLInputElement).value,
})}
placeholder="+1555... or chat id"
/>
<div class="cron-help">
Optional recipient override for failure alerts.
</div>
</label>
<label class="field">
${renderFieldLabel("Alert mode")}
<select
.value=${props.form.failureAlertDeliveryMode || "announce"}
@change=${(e: Event) =>
props.onFormChange({
failureAlertDeliveryMode: (e.target as HTMLSelectElement)
.value as CronFormState["failureAlertDeliveryMode"],
})}
>
<option value="announce">Announce (via channel)</option>
<option value="webhook">Webhook (HTTP POST)</option>
</select>
</label>
<label class="field">
${renderFieldLabel("Alert account ID")}
<input
.value=${props.form.failureAlertAccountId}
@input=${(e: Event) =>
props.onFormChange({
failureAlertAccountId: (e.target as HTMLInputElement).value,
})}
placeholder="Account ID for multi-account setups"
/>
</label>
`
: nothing}
`
: nothing}
${selectedDeliveryMode !== "none"
? html`
<label class="field checkbox cron-checkbox cron-span-2">
<input
type="checkbox"
.checked=${props.form.deliveryBestEffort}
@change=${(e: Event) =>
props.onFormChange({
deliveryBestEffort: (e.target as HTMLInputElement).checked,
})}
/>
<span class="field-checkbox__label"
>${t("cron.form.bestEffortDelivery")}</span
>
<div class="cron-help">${t("cron.form.bestEffortHelp")}</div>
</label>
`
: nothing}
</div>
</details>
</div>
${blockedByValidation
? html`
<div class="cron-form-status" role="status" aria-live="polite">
<div class="cron-form-status__title">${t("cron.form.cantAddYet")}</div>
<div class="cron-help">${t("cron.form.fillRequired")}</div>
<ul class="cron-form-status__list">
${blockingFields.map(
(field) => html`
<li>
<button
type="button"
class="cron-form-status__link"
@click=${() => focusFormField(field.inputId)}
>
${field.label}: ${t(field.message)}
</button>
</li>
`,
)}
</ul>
</div>
`
: nothing}
<div class="row cron-form-actions">
<button
class="btn primary"
?disabled=${props.busy || !props.canSubmit}
@click=${props.onAdd}
>
${props.busy
? t("cron.form.saving")
: isEditing
? t("cron.form.saveChanges")
: t("cron.form.addJob")}
</button>
${submitDisabledReason
? html`<div class="cron-submit-reason" aria-live="polite">${submitDisabledReason}</div>`
: nothing}
${isEditing
? html`
<button class="btn" ?disabled=${props.busy} @click=${props.onCancelEdit}>
${t("cron.form.cancel")}
</button>
`
: nothing}
</div>
</section>
</section>
${renderSuggestionList("cron-agent-suggestions", props.agentSuggestions)}
${renderSuggestionList("cron-model-suggestions", props.modelSuggestions)}
${renderSuggestionList("cron-thinking-suggestions", props.thinkingSuggestions)}
${renderSuggestionList("cron-tz-suggestions", props.timezoneSuggestions)}
${renderSuggestionList("cron-delivery-to-suggestions", props.deliveryToSuggestions)}
${renderSuggestionList("cron-delivery-account-suggestions", props.accountSuggestions)}
`;
}
function renderScheduleFields(props: CronProps) {
const form = props.form;
if (form.scheduleKind === "at") {
return html`
<label class="field cron-span-2" style="margin-top: 12px;">
${renderFieldLabel(t("cron.form.runAt"), true)}
<input
id="cron-schedule-at"
type="datetime-local"
.value=${form.scheduleAt}
aria-invalid=${props.fieldErrors.scheduleAt ? "true" : "false"}
aria-describedby=${ifDefined(
props.fieldErrors.scheduleAt ? errorIdForField("scheduleAt") : undefined,
)}
@input=${(e: Event) =>
props.onFormChange({
scheduleAt: (e.target as HTMLInputElement).value,
})}
/>
${renderFieldError(props.fieldErrors.scheduleAt, errorIdForField("scheduleAt"))}
</label>
`;
}
if (form.scheduleKind === "every") {
return html`
<div class="form-grid cron-form-grid" style="margin-top: 12px;">
<label class="field">
${renderFieldLabel(t("cron.form.every"), true)}
<input
id="cron-every-amount"
.value=${form.everyAmount}
aria-invalid=${props.fieldErrors.everyAmount ? "true" : "false"}
aria-describedby=${ifDefined(
props.fieldErrors.everyAmount ? errorIdForField("everyAmount") : undefined,
)}
@input=${(e: Event) =>
props.onFormChange({
everyAmount: (e.target as HTMLInputElement).value,
})}
placeholder=${t("cron.form.everyAmountPlaceholder")}
/>
${renderFieldError(props.fieldErrors.everyAmount, errorIdForField("everyAmount"))}
</label>
<label class="field">
<span>${t("cron.form.unit")}</span>
<select
.value=${form.everyUnit}
@change=${(e: Event) =>
props.onFormChange({
everyUnit: (e.target as HTMLSelectElement).value as CronFormState["everyUnit"],
})}
>
<option value="minutes">${t("cron.form.minutes")}</option>
<option value="hours">${t("cron.form.hours")}</option>
<option value="days">${t("cron.form.days")}</option>
</select>
</label>
</div>
`;
}
return html`
<div class="form-grid cron-form-grid" style="margin-top: 12px;">
<label class="field">
${renderFieldLabel(t("cron.form.expression"), true)}
<input
id="cron-cron-expr"
.value=${form.cronExpr}
aria-invalid=${props.fieldErrors.cronExpr ? "true" : "false"}
aria-describedby=${ifDefined(
props.fieldErrors.cronExpr ? errorIdForField("cronExpr") : undefined,
)}
@input=${(e: Event) =>
props.onFormChange({ cronExpr: (e.target as HTMLInputElement).value })}
placeholder=${t("cron.form.expressionPlaceholder")}
/>
${renderFieldError(props.fieldErrors.cronExpr, errorIdForField("cronExpr"))}
</label>
<label class="field">
<span>${t("cron.form.timezoneOptional")}</span>
<input
.value=${form.cronTz}
list="cron-tz-suggestions"
@input=${(e: Event) =>
props.onFormChange({ cronTz: (e.target as HTMLInputElement).value })}
placeholder=${t("cron.form.timezonePlaceholder")}
/>
<div class="cron-help">${t("cron.form.timezoneHelp")}</div>
</label>
<div class="cron-help cron-span-2">${t("cron.form.jitterHelp")}</div>
</div>
`;
}
function renderFieldError(message?: string, id?: string) {
if (!message) {
return nothing;
}
return html`<div id=${ifDefined(id)} class="cron-help cron-error">${t(message)}</div>`;
}
function renderJob(job: CronJob, props: CronProps) {
const isSelected = props.runsJobId === job.id;
const itemClass = `list-item list-item-clickable cron-job${isSelected ? " list-item-selected" : ""}`;
const selectAnd = (action: () => void) => {
props.onLoadRuns(job.id);
action();
};
return html`
<div class=${itemClass} @click=${() => props.onLoadRuns(job.id)}>
<div class="list-main">
<div class="list-title">${job.name}</div>
<div class="list-sub">${formatCronSchedule(job)}</div>
${renderJobPayload(job)}
${job.agentId
? html`<div class="muted cron-job-agent">
${t("cron.jobDetail.agent")}: ${job.agentId}
</div>`
: nothing}
</div>
<div class="list-meta">${renderJobState(job)}</div>
<div class="cron-job-footer">
<div class="chip-row cron-job-chips">
<span class=${`chip ${job.enabled ? "chip-ok" : "chip-danger"}`}>
${job.enabled ? t("cron.jobList.enabled") : t("cron.jobList.disabled")}
</span>
<span class="chip">${job.sessionTarget}</span>
<span class="chip">${job.wakeMode}</span>
</div>
<div class="row cron-job-actions">
<button
class="btn"
?disabled=${props.busy}
@click=${(event: Event) => {
event.stopPropagation();
selectAnd(() => props.onEdit(job));
}}
>
${t("cron.jobList.edit")}
</button>
<button
class="btn"
?disabled=${props.busy}
@click=${(event: Event) => {
event.stopPropagation();
selectAnd(() => props.onClone(job));
}}
>
${t("cron.jobList.clone")}
</button>
<button
class="btn"
?disabled=${props.busy}
@click=${(event: Event) => {
event.stopPropagation();
selectAnd(() => props.onToggle(job, !job.enabled));
}}
>
${job.enabled ? t("cron.jobList.disable") : t("cron.jobList.enable")}
</button>
<button
class="btn"
?disabled=${props.busy}
@click=${(event: Event) => {
event.stopPropagation();
selectAnd(() => props.onRun(job, "force"));
}}
>
${t("cron.jobList.run")}
</button>
<button
class="btn"
?disabled=${props.busy}
@click=${(event: Event) => {
event.stopPropagation();
selectAnd(() => props.onRun(job, "due"));
}}
>
Run if due
</button>
<button
class="btn"
?disabled=${props.busy}
@click=${(event: Event) => {
event.stopPropagation();
props.onLoadRuns(job.id);
}}
>
${t("cron.jobList.history")}
</button>
<button
class="btn danger"
?disabled=${props.busy}
@click=${(event: Event) => {
event.stopPropagation();
selectAnd(() => props.onRemove(job));
}}
>
${t("cron.jobList.remove")}
</button>
</div>
</div>
</div>
`;
}
function renderJobPayload(job: CronJob) {
if (job.payload.kind === "systemEvent") {
return html`<div class="cron-job-detail">
<span class="cron-job-detail-label">${t("cron.jobDetail.system")}</span>
<span class="muted cron-job-detail-value">${job.payload.text}</span>
</div>`;
}
const delivery = job.delivery;
const deliveryTarget =
delivery?.mode === "webhook"
? delivery.to
? ` (${delivery.to})`
: ""
: delivery?.channel || delivery?.to
? ` (${delivery.channel ?? "last"}${delivery.to ? ` -> ${delivery.to}` : ""})`
: "";
return html`
<div class="cron-job-detail">
<span class="cron-job-detail-label">${t("cron.jobDetail.prompt")}</span>
<span class="muted cron-job-detail-value">${job.payload.message}</span>
</div>
${delivery
? html`<div class="cron-job-detail">
<span class="cron-job-detail-label">${t("cron.jobDetail.delivery")}</span>
<span class="muted cron-job-detail-value">${delivery.mode}${deliveryTarget}</span>
</div>`
: nothing}
`;
}
function formatStateRelative(ms?: number) {
if (typeof ms !== "number" || !Number.isFinite(ms)) {
return t("common.na");
}
return formatRelativeTimestamp(ms);
}
function formatRunNextLabel(nextRunAtMs: number, nowMs = Date.now()) {
const rel = formatRelativeTimestamp(nextRunAtMs);
return nextRunAtMs > nowMs ? t("cron.runEntry.next", { rel }) : t("cron.runEntry.due", { rel });
}
function renderJobState(job: CronJob) {
const rawStatus = job.state?.lastStatus;
const statusClass =
rawStatus === "ok"
? "cron-job-status-ok"
: rawStatus === "error"
? "cron-job-status-error"
: rawStatus === "skipped"
? "cron-job-status-skipped"
: "cron-job-status-na";
const statusLabel =
rawStatus === "ok"
? t("cron.runs.runStatusOk")
: rawStatus === "error"
? t("cron.runs.runStatusError")
: rawStatus === "skipped"
? t("cron.runs.runStatusSkipped")
: t("common.na");
const nextRunAtMs = job.state?.nextRunAtMs;
const lastRunAtMs = job.state?.lastRunAtMs;
return html`
<div class="cron-job-state">
<div class="cron-job-state-row">
<span class="cron-job-state-key">${t("cron.jobState.status")}</span>
<span class=${`cron-job-status-pill ${statusClass}`}>${statusLabel}</span>
</div>
<div class="cron-job-state-row">
<span class="cron-job-state-key">${t("cron.jobState.next")}</span>
<span class="cron-job-state-value" title=${formatMs(nextRunAtMs)}>
${formatStateRelative(nextRunAtMs)}
</span>
</div>
<div class="cron-job-state-row">
<span class="cron-job-state-key">${t("cron.jobState.last")}</span>
<span class="cron-job-state-value" title=${formatMs(lastRunAtMs)}>
${formatStateRelative(lastRunAtMs)}
</span>
</div>
</div>
`;
}
function runStatusLabel(value: string): string {
switch (value) {
case "ok":
return t("cron.runs.runStatusOk");
case "error":
return t("cron.runs.runStatusError");
case "skipped":
return t("cron.runs.runStatusSkipped");
default:
return t("cron.runs.runStatusUnknown");
}
}
function runDeliveryLabel(value: string): string {
switch (value) {
case "delivered":
return t("cron.runs.deliveryDelivered");
case "not-delivered":
return t("cron.runs.deliveryNotDelivered");
case "not-requested":
return t("cron.runs.deliveryNotRequested");
case "unknown":
return t("cron.runs.deliveryUnknown");
default:
return t("cron.runs.deliveryUnknown");
}
}
function renderRun(
entry: CronRunLogEntry,
basePath: string,
onNavigateToChat?: (sessionKey: string) => void,
) {
const chatUrl =
typeof entry.sessionKey === "string" && entry.sessionKey.trim().length > 0
? `${pathForTab("chat", basePath)}?session=${encodeURIComponent(entry.sessionKey)}`
: null;
const status = runStatusLabel(entry.status ?? "unknown");
const delivery = runDeliveryLabel(entry.deliveryStatus ?? "not-requested");
const usage = entry.usage;
const usageSummary =
usage && typeof usage.total_tokens === "number"
? `${usage.total_tokens} tokens`
: usage && typeof usage.input_tokens === "number" && typeof usage.output_tokens === "number"
? `${usage.input_tokens} in / ${usage.output_tokens} out`
: null;
return html`
<div class="list-item cron-run-entry">
<div class="list-main cron-run-entry__main">
<div class="list-title cron-run-entry__title">
${entry.jobName ?? entry.jobId}
<span class="muted"> · ${status}</span>
</div>
<div class="list-sub cron-run-entry__summary">
${entry.summary ?? entry.error ?? t("cron.runEntry.noSummary")}
</div>
<div class="chip-row" style="margin-top: 6px;">
<span class="chip">${delivery}</span>
${entry.model ? html`<span class="chip">${entry.model}</span>` : nothing}
${entry.provider ? html`<span class="chip">${entry.provider}</span>` : nothing}
${usageSummary ? html`<span class="chip">${usageSummary}</span>` : nothing}
</div>
</div>
<div class="list-meta cron-run-entry__meta">
<div>${formatMs(entry.ts)}</div>
${typeof entry.runAtMs === "number"
? html`<div class="muted">${t("cron.runEntry.runAt")} ${formatMs(entry.runAtMs)}</div>`
: nothing}
<div class="muted">${entry.durationMs ?? 0}ms</div>
${typeof entry.nextRunAtMs === "number"
? html`<div class="muted">${formatRunNextLabel(entry.nextRunAtMs)}</div>`
: nothing}
${chatUrl
? html`<div>
<a
class="session-link"
href=${chatUrl}
@click=${(e: MouseEvent) => {
if (
e.defaultPrevented ||
e.button !== 0 ||
e.metaKey ||
e.ctrlKey ||
e.shiftKey ||
e.altKey
) {
return;
}
if (onNavigateToChat && entry.sessionKey) {
e.preventDefault();
onNavigateToChat(entry.sessionKey);
}
}}
>${t("cron.runEntry.openRunChat")}</a
>
</div>`
: nothing}
${entry.error ? html`<div class="muted">${entry.error}</div>` : nothing}
${entry.deliveryError ? html`<div class="muted">${entry.deliveryError}</div>` : nothing}
</div>
</div>
`;
}
¤ Dauer der Verarbeitung: 0.39 Sekunden
(vorverarbeitet am 2026-04-27)
¤
*© Formatika GbR, Deutschland
|
|