feat(web): automations detail page (definition viewer + trigger manager)
Vertical slice at /dashboard/[id]/automations/[automation_id]. Branches
in the orchestrator are: perms loading → skeleton, no-access → access
denied panel, bad id → not-found, fetch loading → skeleton, fetch
error → not-found, loaded → header + definition + triggers.
Route:
- page.tsx — server boundary; extracts both ids.
- automation-detail-content.tsx — client orchestrator.
Header:
- automation-detail-header.tsx — back link, name, status badge,
description, pause/resume + delete actions. Delete navigates back to
the list via a new onDeleted hook on DeleteAutomationDialog so the
list page (where the row just vanishes) stays unaffected.
- automation-not-found.tsx — 404/403/NaN-id panel. We don't
distinguish missing vs. forbidden in the UI.
Definition (read-only in v1):
- automation-definition-section.tsx — wrapper Card; renders goal +
tags + execution defaults + inputs schema (if present) + plan.
- plan-step-card.tsx — one step (when, output_as, retries, timeout,
params JSON).
- execution-summary.tsx — timeout / max_retries / backoff /
concurrency + on_failure step count.
- inputs-schema-preview.tsx — formatted JSON of inputs.schema; only
rendered when the definition declares inputs.
Triggers:
- automation-triggers-section.tsx — wrapper Card, "Add via chat" CTA
(creation is intent-driven, same philosophy as automations).
- trigger-card.tsx — schedule + timezone + cron, last/next fire
hints, static_inputs JSON, enable Switch and remove button.
- delete-trigger-dialog.tsx — confirm + mutation atom.
Shared:
- lib/describe-cron.ts — moved out of automation-triggers-summary.tsx
so both list and detail can describe schedules consistently
(daily/weekdays/weekly/monthly/hourly, raw cron fallback).
Loading:
- automation-detail-loading.tsx — same shell as the loaded view so the
layout doesn't jump on data arrival.
RBAC: each interactive surface is independently gated
(canUpdate/canDelete/canCreate) so the orchestrator stays thin and the
component tree is self-documenting about what each action requires.
Out of scope (later PRs):
- Editing definition / trigger params (raw-JSON path) — PR5
- Run history — PR6
2026-05-28 01:21:54 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Minimal cron describer for the 5-field patterns the SurfSense drafter LLM
|
|
|
|
|
|
* actually produces (daily, weekdays, weekly, monthly, hourly). Falls back
|
|
|
|
|
|
* to the raw expression when unrecognized so the user still sees something
|
|
|
|
|
|
* honest instead of a guess.
|
|
|
|
|
|
*
|
2026-05-28 01:32:04 +02:00
|
|
|
|
* Lives under ``lib/automations/`` because both the dashboard slice and the
|
|
|
|
|
|
* chat ``create_automation`` approval card render schedule descriptions —
|
|
|
|
|
|
* keeping the helper outside either feature avoids a layering violation.
|
feat(web): automations detail page (definition viewer + trigger manager)
Vertical slice at /dashboard/[id]/automations/[automation_id]. Branches
in the orchestrator are: perms loading → skeleton, no-access → access
denied panel, bad id → not-found, fetch loading → skeleton, fetch
error → not-found, loaded → header + definition + triggers.
Route:
- page.tsx — server boundary; extracts both ids.
- automation-detail-content.tsx — client orchestrator.
Header:
- automation-detail-header.tsx — back link, name, status badge,
description, pause/resume + delete actions. Delete navigates back to
the list via a new onDeleted hook on DeleteAutomationDialog so the
list page (where the row just vanishes) stays unaffected.
- automation-not-found.tsx — 404/403/NaN-id panel. We don't
distinguish missing vs. forbidden in the UI.
Definition (read-only in v1):
- automation-definition-section.tsx — wrapper Card; renders goal +
tags + execution defaults + inputs schema (if present) + plan.
- plan-step-card.tsx — one step (when, output_as, retries, timeout,
params JSON).
- execution-summary.tsx — timeout / max_retries / backoff /
concurrency + on_failure step count.
- inputs-schema-preview.tsx — formatted JSON of inputs.schema; only
rendered when the definition declares inputs.
Triggers:
- automation-triggers-section.tsx — wrapper Card, "Add via chat" CTA
(creation is intent-driven, same philosophy as automations).
- trigger-card.tsx — schedule + timezone + cron, last/next fire
hints, static_inputs JSON, enable Switch and remove button.
- delete-trigger-dialog.tsx — confirm + mutation atom.
Shared:
- lib/describe-cron.ts — moved out of automation-triggers-summary.tsx
so both list and detail can describe schedules consistently
(daily/weekdays/weekly/monthly/hourly, raw cron fallback).
Loading:
- automation-detail-loading.tsx — same shell as the loaded view so the
layout doesn't jump on data arrival.
RBAC: each interactive surface is independently gated
(canUpdate/canDelete/canCreate) so the orchestrator stays thin and the
component tree is self-documenting about what each action requires.
Out of scope (later PRs):
- Editing definition / trigger params (raw-JSON path) — PR5
- Run history — PR6
2026-05-28 01:21:54 +02:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
const DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
|
|
|
|
|
|
|
|
|
|
export function describeCron(cron: string): string {
|
|
|
|
|
|
const parts = cron.trim().split(/\s+/);
|
|
|
|
|
|
if (parts.length !== 5) return cron;
|
|
|
|
|
|
|
|
|
|
|
|
const [minute, hour, dom, month, dow] = parts;
|
|
|
|
|
|
|
|
|
|
|
|
// Daily at H:MM ("0 9 * * *")
|
|
|
|
|
|
if (month === "*" && dom === "*" && dow === "*" && /^\d+$/.test(minute) && /^\d+$/.test(hour)) {
|
|
|
|
|
|
return `Daily at ${formatTime(hour, minute)}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Weekdays at H:MM ("0 9 * * 1-5")
|
|
|
|
|
|
if (month === "*" && dom === "*" && dow === "1-5" && /^\d+$/.test(minute) && /^\d+$/.test(hour)) {
|
|
|
|
|
|
return `Mon–Fri at ${formatTime(hour, minute)}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Specific weekday(s) ("0 9 * * 1" or "0 9 * * 1,3,5")
|
|
|
|
|
|
if (
|
|
|
|
|
|
month === "*" &&
|
|
|
|
|
|
dom === "*" &&
|
|
|
|
|
|
/^\d+$/.test(minute) &&
|
|
|
|
|
|
/^\d+$/.test(hour) &&
|
|
|
|
|
|
/^[\d,]+$/.test(dow)
|
|
|
|
|
|
) {
|
|
|
|
|
|
const days = dow
|
|
|
|
|
|
.split(",")
|
|
|
|
|
|
.map((d) => DAY_NAMES[Number(d) % 7])
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
.join(", ");
|
|
|
|
|
|
if (days) return `${days} at ${formatTime(hour, minute)}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Monthly on day N ("0 9 1 * *")
|
|
|
|
|
|
if (
|
|
|
|
|
|
month === "*" &&
|
|
|
|
|
|
dow === "*" &&
|
|
|
|
|
|
/^\d+$/.test(dom) &&
|
|
|
|
|
|
/^\d+$/.test(hour) &&
|
|
|
|
|
|
/^\d+$/.test(minute)
|
|
|
|
|
|
) {
|
|
|
|
|
|
return `Day ${dom} of each month at ${formatTime(hour, minute)}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Hourly ("0 * * * *")
|
|
|
|
|
|
if (month === "*" && dom === "*" && dow === "*" && hour === "*" && /^\d+$/.test(minute)) {
|
|
|
|
|
|
return minute === "0" ? "Every hour" : `Every hour at :${minute.padStart(2, "0")}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return cron;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatTime(hour: string, minute: string): string {
|
|
|
|
|
|
return `${hour.padStart(2, "0")}:${minute.padStart(2, "0")}`;
|
|
|
|
|
|
}
|