mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 19:35:28 -06:00
d26e097263
* improve(ui): unify sidebar footer and issues panel * improve(ui): compact sidebar inbox footer * improve(ui): enlarge inbox mascot * improve(ui): enlarge inbox heading * improve(ui): add inbox item context * fix(ui): align identity menu trailing rail * fix(ui): restore identity menu build age * improve(ui): add sidebar update indicator * fix(mock): restore pending inbox approvals * improve(ui): remove sidebar footer border * improve(ui): align identity menu header * fix(ui): center identity theme icon * improve(ui): refine build hovercard * improve(ui): unify inbox attention red * fix(ui): gate sidebar update indicator * improve(ui): clarify inbox item rows * improve(ui): clarify inbox triage * experiment(ui): separate inbox items * fix(ui): scope inbox to chat sidebar * improve(ui): pad build hovercard * improve(ui): add inbox empty state * improve(ui): align inbox popover * fix(ui): square inbox separators * improve(ui): reorder sidebar update * improve(ui): simplify approval inbox rows * fix(ui): shift inbox beside update * improve(ui): add mobile inbox sheet * fix(ui): align sidebar footer controls * improve(ui): space footer actions * fix(ui): delay update hover expansion * improve(ui): shorten mobile inbox sheet * fix(ui): extend inbox row hover * improve(ui): strengthen inbox chrome * fix(ui): remove update tooltip * improve(ui): anchor inbox above trigger * fix(ui): align approval row padding * fix(ui): hide initial inbox dismiss * improve(ui): lift approval command surface * improve(ui): clarify approval command surface * improve(ui): tighten inbox header * improve(ui): anchor sidebar catalog alerts * fix(ui): gate footer update indicator * improve(ui): soften sidebar catalog alert * fix(ui): emphasize deny on hover * fix(ui): unify inbox row separators * fix(ui): lift sidebar catalog alert * fix(ui): preserve sidebar scroll fade * fix(ui): align inbox icon rail * improve(ui): emphasize approval action * fix(ui): fill approval primary action * fix(ui): preserve approval action fill * fix(mock): clear sidebar catalog alert * fix(ui): show approval action fill * fix(ui): hide adjacent inbox separators * improve(ui): refine sidebar footer divider * fix(ui): square inbox row hover * fix(ui): preserve inbox inline action contrast * improve(ui): mute neutral inbox icons * improve(ui): move reconnect into details * improve(ui): refine mobile inbox sheet * improve(ui): remove duplicate agent approvals * refactor(ui): close out sidebar inbox * test(ui): remove duplicated sidebar coverage * fix(ui): restore sidebar inbox boundaries * test(ui): include lazy update styles in cursor policy
375 lines
13 KiB
TypeScript
375 lines
13 KiB
TypeScript
import type {
|
|
CronJob,
|
|
CronJobsListResult,
|
|
CronRunLogEntry,
|
|
CronRunsResult,
|
|
CronStatus,
|
|
} from "../ui/src/api/types.ts";
|
|
|
|
const CRON_LIST_SNAPSHOT_REVISION = "control-ui-mock-cron";
|
|
|
|
function listResult(
|
|
jobs: CronJob[],
|
|
options: { total?: number; limit?: number; offset?: number } = {},
|
|
): CronJobsListResult {
|
|
const total = options.total ?? jobs.length;
|
|
const limit = options.limit ?? 50;
|
|
const offset = options.offset ?? 0;
|
|
const nextOffset = offset + jobs.length;
|
|
const hasMore = nextOffset < total;
|
|
return {
|
|
jobs,
|
|
snapshotRevision: CRON_LIST_SNAPSHOT_REVISION,
|
|
total,
|
|
offset,
|
|
limit,
|
|
hasMore,
|
|
nextOffset: hasMore ? nextOffset : null,
|
|
};
|
|
}
|
|
|
|
function runsResult(entries: CronRunLogEntry[]): CronRunsResult {
|
|
return {
|
|
entries,
|
|
total: entries.length,
|
|
offset: 0,
|
|
limit: 50,
|
|
hasMore: false,
|
|
nextOffset: null,
|
|
};
|
|
}
|
|
|
|
function singleJobListCases(jobs: CronJob[], match: Record<string, unknown>) {
|
|
return jobs.map((job, offset) => ({
|
|
match: { ...match, offset },
|
|
response: listResult([job], { total: jobs.length, limit: 1, offset }),
|
|
}));
|
|
}
|
|
|
|
export function buildCronMocks(baseTime: number, options: { richAttention?: boolean } = {}) {
|
|
const richAttention = options.richAttention === true;
|
|
const minute = 60_000;
|
|
const hour = 60 * minute;
|
|
const day = 24 * hour;
|
|
const failedJob: CronJob = {
|
|
id: "mock-cron-calendar-sync",
|
|
agentId: "main",
|
|
name: "Sync team calendar",
|
|
description: "Refresh the shared calendar cache for the morning briefing.",
|
|
enabled: true,
|
|
createdAtMs: baseTime - 30 * day,
|
|
updatedAtMs: baseTime - 4 * minute,
|
|
schedule: { kind: "cron", expr: "0 */6 * * *", tz: "America/New_York" },
|
|
sessionTarget: "isolated",
|
|
wakeMode: "now",
|
|
payload: {
|
|
kind: "agentTurn",
|
|
message: "Sync the team calendar and summarize schedule conflicts.",
|
|
},
|
|
delivery: { mode: "announce", channel: "telegram", to: "@operations" },
|
|
state: {
|
|
nextRunAtMs: baseTime + 5 * hour,
|
|
lastRunAtMs: baseTime - 5 * minute,
|
|
lastRunStatus: "error",
|
|
lastError:
|
|
"OAuth refresh failed: invalid_grant. The provider rejected the stored refresh token because it was revoked or expired. Reconnect Google Calendar before the next scheduled sync.",
|
|
lastDurationMs: 8_420,
|
|
consecutiveErrors: 2,
|
|
lastDeliveryStatus: "not-requested",
|
|
},
|
|
};
|
|
const extraFailedJobs: CronJob[] = richAttention
|
|
? [
|
|
{
|
|
id: "mock-cron-release-notify",
|
|
agentId: "main",
|
|
name: "Notify release stakeholders about deployment readiness and rollback constraints",
|
|
description:
|
|
"Send the release decision, deploy window, rollback owner, and incident contact to every stakeholder group.",
|
|
enabled: true,
|
|
createdAtMs: baseTime - 24 * day,
|
|
updatedAtMs: baseTime - 7 * minute,
|
|
schedule: { kind: "every", everyMs: 20 * minute, anchorMs: baseTime - 24 * day },
|
|
sessionTarget: "isolated",
|
|
wakeMode: "now",
|
|
payload: {
|
|
kind: "agentTurn",
|
|
message:
|
|
"Prepare the release notification, verify the rollback owner, and publish the final deployment readiness summary.",
|
|
},
|
|
delivery: { mode: "announce", channel: "slack", to: "#release-operations" },
|
|
state: {
|
|
nextRunAtMs: baseTime + 20 * minute,
|
|
lastRunAtMs: baseTime - 9 * minute,
|
|
lastRunStatus: "error",
|
|
lastError:
|
|
"Delivery failed after the provider accepted the request but closed the stream before the final acknowledgement. The retry queue retained the payload, the release channel has not been notified, and the notification fan-out must be reconciled before another deployment attempt.",
|
|
lastDurationMs: 18_640,
|
|
consecutiveErrors: 3,
|
|
lastDeliveryStatus: "not-delivered",
|
|
},
|
|
},
|
|
{
|
|
id: "mock-cron-backup-verify",
|
|
agentId: "main",
|
|
name: "Verify encrypted backup rotation before the retention window closes",
|
|
description:
|
|
"Check the latest encrypted backup, key rotation receipt, and restore manifest before retention pruning.",
|
|
enabled: true,
|
|
createdAtMs: baseTime - 42 * day,
|
|
updatedAtMs: baseTime - 11 * minute,
|
|
schedule: { kind: "cron", expr: "15 * * * *", tz: "UTC" },
|
|
sessionTarget: "isolated",
|
|
wakeMode: "now",
|
|
payload: {
|
|
kind: "agentTurn",
|
|
message:
|
|
"Verify backup rotation and report any missing restore manifest or key receipt.",
|
|
},
|
|
delivery: { mode: "none" },
|
|
state: {
|
|
nextRunAtMs: baseTime + 15 * minute,
|
|
lastRunAtMs: baseTime - 13 * minute,
|
|
lastRunStatus: "error",
|
|
lastError:
|
|
"Restore verification could not read the encrypted manifest: checksum mismatch after the object store returned a partial range. Keep the current backup, do not prune the retention window, and retry after the storage replica is healthy.",
|
|
lastDurationMs: 42_900,
|
|
consecutiveErrors: 4,
|
|
lastDeliveryStatus: "not-requested",
|
|
},
|
|
},
|
|
]
|
|
: [];
|
|
const overdueJob: CronJob = {
|
|
id: "mock-cron-inbox-triage",
|
|
agentId: "main",
|
|
name: "Triage support inbox",
|
|
description: "Classify new support mail and prepare the daily response queue.",
|
|
enabled: true,
|
|
createdAtMs: baseTime - 18 * day,
|
|
updatedAtMs: baseTime - 35 * minute,
|
|
schedule: { kind: "every", everyMs: 15 * minute, anchorMs: baseTime - 18 * day },
|
|
sessionTarget: "isolated",
|
|
wakeMode: "now",
|
|
payload: { kind: "agentTurn", message: "Triage the support inbox." },
|
|
delivery: { mode: "none" },
|
|
state: {
|
|
nextRunAtMs: baseTime - 20 * minute,
|
|
lastRunAtMs: baseTime - 35 * minute,
|
|
lastRunStatus: "ok",
|
|
lastDurationMs: 24_180,
|
|
lastDeliveryStatus: "not-requested",
|
|
},
|
|
};
|
|
const extraOverdueJobs: CronJob[] = richAttention
|
|
? [
|
|
{
|
|
id: "mock-cron-security-digest",
|
|
agentId: "main",
|
|
name: "Prepare the daily security digest",
|
|
description: "Summarize new security advisories and unresolved remediation work.",
|
|
enabled: true,
|
|
createdAtMs: baseTime - 31 * day,
|
|
updatedAtMs: baseTime - 50 * minute,
|
|
schedule: { kind: "every", everyMs: hour, anchorMs: baseTime - 31 * day },
|
|
sessionTarget: "isolated",
|
|
wakeMode: "now",
|
|
payload: { kind: "agentTurn", message: "Prepare the daily security digest." },
|
|
delivery: { mode: "none" },
|
|
state: {
|
|
nextRunAtMs: baseTime - 42 * minute,
|
|
lastRunAtMs: baseTime - 102 * minute,
|
|
lastRunStatus: "ok",
|
|
lastDurationMs: 38_420,
|
|
lastDeliveryStatus: "not-requested",
|
|
},
|
|
},
|
|
]
|
|
: [];
|
|
const healthyJob: CronJob = {
|
|
id: "mock-cron-release-digest",
|
|
agentId: "main",
|
|
name: "Publish release digest",
|
|
description: "Summarize merged changes for the engineering channel.",
|
|
enabled: true,
|
|
createdAtMs: baseTime - 9 * day,
|
|
updatedAtMs: baseTime - 30 * minute,
|
|
schedule: { kind: "cron", expr: "30 9 * * 1-5", tz: "America/New_York" },
|
|
sessionTarget: "isolated",
|
|
wakeMode: "now",
|
|
payload: { kind: "agentTurn", message: "Draft and publish the release digest." },
|
|
delivery: { mode: "announce", channel: "slack", to: "#engineering" },
|
|
state: {
|
|
nextRunAtMs: baseTime + 30 * minute,
|
|
lastRunAtMs: baseTime - 30 * minute,
|
|
lastRunStatus: "ok",
|
|
lastDurationMs: 51_230,
|
|
lastDelivered: true,
|
|
lastDeliveryStatus: "delivered",
|
|
},
|
|
};
|
|
const jobs = [overdueJob, ...extraOverdueJobs, healthyJob, failedJob, ...extraFailedJobs];
|
|
const failedJobs = [failedJob, ...extraFailedJobs];
|
|
const failedRuns: CronRunLogEntry[] = failedJobs.map((job, index) => ({
|
|
ts: baseTime - (5 + index * 4) * minute,
|
|
runAtMs: baseTime - (5 + index * 4) * minute,
|
|
jobId: job.id,
|
|
jobName: job.name,
|
|
action: "finished",
|
|
status: "error",
|
|
durationMs: job.state?.lastDurationMs,
|
|
error: job.state?.lastError,
|
|
deliveryStatus: "not-requested",
|
|
model: index === 1 ? "claude-sonnet-4-6" : "gpt-5.6-sol",
|
|
provider: index === 1 ? "anthropic" : "openai",
|
|
}));
|
|
const runs: CronRunLogEntry[] = [
|
|
...failedRuns,
|
|
{
|
|
ts: baseTime - 30 * minute,
|
|
runAtMs: baseTime - 30 * minute,
|
|
jobId: healthyJob.id,
|
|
jobName: healthyJob.name,
|
|
action: "finished",
|
|
status: "ok",
|
|
durationMs: healthyJob.state?.lastDurationMs,
|
|
summary: "Published a digest covering 14 merged changes.",
|
|
delivered: true,
|
|
deliveryStatus: "delivered",
|
|
model: "claude-sonnet-4-6",
|
|
provider: "anthropic",
|
|
},
|
|
{
|
|
ts: baseTime - 35 * minute,
|
|
runAtMs: baseTime - 35 * minute,
|
|
jobId: overdueJob.id,
|
|
jobName: overdueJob.name,
|
|
action: "finished",
|
|
status: "ok",
|
|
durationMs: overdueJob.state?.lastDurationMs,
|
|
summary: "Classified 23 messages and prepared 6 replies.",
|
|
deliveryStatus: "not-requested",
|
|
model: "gpt-5.6-sol",
|
|
provider: "openai",
|
|
},
|
|
];
|
|
const queuedRuns: Array<{ runId: string; entry: CronRunLogEntry }> = jobs.map((job, index) => ({
|
|
runId: `mock-cron-manual-${job.id}`,
|
|
entry: {
|
|
ts: baseTime + index,
|
|
runAtMs: baseTime + index,
|
|
jobId: job.id,
|
|
jobName: job.name,
|
|
action: "finished",
|
|
status: "ok",
|
|
durationMs: 42_000 + index * 2_500,
|
|
summary: `Completed an on-demand run for ${job.name}.`,
|
|
deliveryStatus: "not-requested",
|
|
model: "gpt-5.6-sol",
|
|
provider: "openai",
|
|
},
|
|
}));
|
|
const status: CronStatus = {
|
|
enabled: true,
|
|
triggersEnabled: true,
|
|
jobs: jobs.length,
|
|
nextWakeAtMs: Math.min(
|
|
...jobs.flatMap((job) =>
|
|
job.state?.nextRunAtMs === undefined ? [] : [job.state.nextRunAtMs],
|
|
),
|
|
),
|
|
};
|
|
const runByJobId = new Map(runs.map((entry) => [entry.jobId, entry]));
|
|
const sortedJobLists = [
|
|
{
|
|
match: { sortBy: "nextRunAtMs", sortDir: "asc" },
|
|
jobs: jobs.toSorted(
|
|
(left, right) => (left.state?.nextRunAtMs ?? 0) - (right.state?.nextRunAtMs ?? 0),
|
|
),
|
|
},
|
|
{
|
|
match: { sortBy: "nextRunAtMs", sortDir: "desc" },
|
|
jobs: jobs.toSorted(
|
|
(left, right) => (right.state?.nextRunAtMs ?? 0) - (left.state?.nextRunAtMs ?? 0),
|
|
),
|
|
},
|
|
{
|
|
match: { sortBy: "updatedAtMs", sortDir: "asc" },
|
|
jobs: jobs.toSorted((left, right) => (left.updatedAtMs ?? 0) - (right.updatedAtMs ?? 0)),
|
|
},
|
|
{
|
|
match: { sortBy: "updatedAtMs", sortDir: "desc" },
|
|
jobs: jobs.toSorted((left, right) => (right.updatedAtMs ?? 0) - (left.updatedAtMs ?? 0)),
|
|
},
|
|
{
|
|
match: { sortBy: "name", sortDir: "asc" },
|
|
jobs: jobs.toSorted((left, right) => left.name.localeCompare(right.name)),
|
|
},
|
|
{
|
|
match: { sortBy: "name", sortDir: "desc" },
|
|
jobs: jobs.toSorted((left, right) => right.name.localeCompare(left.name)),
|
|
},
|
|
];
|
|
|
|
return {
|
|
"cron.status": status,
|
|
"cron.list": {
|
|
// Cases mirror the concrete queries today's Cron UI issues. Unknown combinations fall back
|
|
// to the full fixture list; dynamic evaluation is intentionally out of scope because the
|
|
// scenario is JSON-serialized into the page rather than installed as a live responder.
|
|
cases: [
|
|
{
|
|
match: { enabled: "enabled", lastRunStatus: "error" },
|
|
response: listResult(failedJobs, { limit: failedJobs.length }),
|
|
},
|
|
{ match: { enabled: "disabled" }, response: listResult([]) },
|
|
...singleJobListCases(jobs, {
|
|
enabled: "enabled",
|
|
sortBy: "nextRunAtMs",
|
|
sortDir: "asc",
|
|
limit: 1,
|
|
}),
|
|
...singleJobListCases(jobs, { includeDisabled: true, limit: 1 }),
|
|
...sortedJobLists.map((entry) => ({
|
|
match: entry.match,
|
|
response: listResult(entry.jobs),
|
|
})),
|
|
{ response: listResult(jobs) },
|
|
],
|
|
},
|
|
"cron.runs": {
|
|
cases: [
|
|
...queuedRuns.map((run) => ({
|
|
match: { runId: run.runId },
|
|
response: runsResult([run.entry]),
|
|
})),
|
|
...jobs.flatMap((job) => {
|
|
const jobRun = runByJobId.get(job.id);
|
|
return [
|
|
{
|
|
match: { scope: "job", id: job.id, statuses: ["error"] },
|
|
response: runsResult(jobRun?.status === "error" ? [jobRun] : []),
|
|
},
|
|
{
|
|
match: { scope: "job", id: job.id },
|
|
response: runsResult(jobRun ? [jobRun] : []),
|
|
},
|
|
];
|
|
}),
|
|
{ match: { statuses: ["error"] }, response: runsResult(failedRuns) },
|
|
{ response: runsResult(runs) },
|
|
],
|
|
},
|
|
// Writes acknowledge the UI action but intentionally keep the fixture snapshot immutable.
|
|
"cron.add": { id: "mock-cron-created" },
|
|
"cron.update": { ok: true },
|
|
"cron.remove": { ok: true },
|
|
"cron.run": {
|
|
cases: queuedRuns.map((run) => ({
|
|
match: { id: run.entry.jobId },
|
|
response: { ok: true, enqueued: true, runId: run.runId },
|
|
})),
|
|
},
|
|
};
|
|
}
|