LEGACY DOCUMENT — This file is superseded by the canonical documentation in
docs/. When there is a conflict,docs/wins. See docs/README.md.
| Layer | Technology | Version |
|---|---|---|
| Framework | React | 19+ |
| Build | Vite | 6+ |
| Language | TypeScript | 5.x |
| UI Components | shadcn/ui (Radix UI + Tailwind) | Latest |
| CSS | Tailwind CSS | 4+ |
| Routing | TanStack Router | Latest |
| Tables | TanStack Table | v8 |
| Forms | React Hook Form + Zod | Latest |
| HTTP Client | ky (or fetch wrapper) | Latest |
| State | TanStack Query (React Query) | v5 |
| Icons | Lucide React | Latest |
| Charts | Recharts | Latest |
| Date/Time | date-fns | Latest |
| Cron Preview | cronstrue | Latest |
| Code/Output Viewer | @monaco-editor/react or simple pre | Latest |
frontend/
├── src/
│ ├── main.tsx # Entry point
│ ├── App.tsx # Router + providers
│ ├── routeTree.gen.ts # TanStack Router generated
│ │
│ ├── api/ # API client layer
│ │ ├── client.ts # Base HTTP client (ky instance with auth)
│ │ ├── types.ts # Auto-generated from OpenAPI (or manual)
│ │ ├── auth.ts # Registration, login, logout
│ │ ├── tenants.ts # Tenant info, usage, settings
│ │ ├── processes.ts # Process CRUD + actions
│ │ ├── slots.ts # Slot listing, cancel, kill, output
│ │ ├── queues.ts # Queue CRUD
│ │ ├── jobs.ts # Job enqueue, list, replay, cancel
│ │ ├── dashboard.ts # Dashboard aggregations
│ │ └── admin.ts # Users, API keys, SSH keys
│ │
│ ├── hooks/ # Custom hooks
│ │ ├── use-auth.ts # Auth state + Google OAuth
│ │ ├── use-tenant.ts # Current tenant context
│ │ ├── use-polling.ts # Auto-refresh for running executions
│ │ └── use-plan.ts # Plan limits + usage
│ │
│ ├── components/ # Shared components
│ │ ├── ui/ # shadcn/ui components (owned, customizable)
│ │ │ ├── button.tsx
│ │ │ ├── card.tsx
│ │ │ ├── badge.tsx
│ │ │ ├── dialog.tsx
│ │ │ ├── dropdown-menu.tsx
│ │ │ ├── input.tsx
│ │ │ ├── select.tsx
│ │ │ ├── table.tsx
│ │ │ ├── tabs.tsx
│ │ │ ├── toast.tsx
│ │ │ ├── tooltip.tsx
│ │ │ ├── sheet.tsx
│ │ │ ├── skeleton.tsx
│ │ │ ├── progress.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── command.tsx # Command palette
│ │ │ └── ...
│ │ │
│ │ ├── layout/
│ │ │ ├── app-layout.tsx # Sidebar + header + main content
│ │ │ ├── sidebar.tsx # Navigation sidebar
│ │ │ ├── header.tsx # Top bar with user menu + tenant info
│ │ │ └── breadcrumbs.tsx
│ │ │
│ │ ├── data/
│ │ │ ├── data-table.tsx # Generic TanStack Table wrapper
│ │ │ ├── data-table-toolbar.tsx
│ │ │ ├── data-table-pagination.tsx
│ │ │ ├── data-table-faceted-filter.tsx
│ │ │ └── data-table-column-header.tsx
│ │ │
│ │ ├── domain/
│ │ │ ├── state-badge.tsx # Colored badge for slot/job states
│ │ │ ├── origin-badge.tsx # Badge for planner/manual/one_time/recovery
│ │ │ ├── target-icon.tsx # Icon for execution target (local/http/ssh/ssm/k8s)
│ │ │ ├── progress-bar.tsx # Heartbeat progress (total/current/%)
│ │ │ ├── cron-preview.tsx # Human-readable cron expression
│ │ │ ├── duration-display.tsx # Formatted duration
│ │ │ ├── time-ago.tsx # Relative time display
│ │ │ ├── output-viewer.tsx # stdout/stderr viewer (monospace, tabs)
│ │ │ ├── heartbeat-timeline.tsx # Visual timeline of heartbeat events
│ │ │ ├── usage-meter.tsx # Plan usage bar (executions/limit)
│ │ │ ├── plan-badge.tsx # Free/Pro/Enterprise badge
│ │ │ └── upgrade-prompt.tsx # Inline upgrade CTA for free tier
│ │ │
│ │ └── forms/
│ │ ├── process-form.tsx # Create/edit process form
│ │ ├── queue-form.tsx # Create/edit queue form
│ │ ├── target-config-form.tsx # Dynamic form per execution target
│ │ ├── cron-input.tsx # Cron expression input with preview
│ │ ├── env-var-editor.tsx # Key-value pair editor
│ │ ├── tag-input.tsx # Tag/label input
│ │ └── json-editor.tsx # JSON payload editor
│ │
│ ├── routes/ # TanStack Router file-based routes
│ │ ├── __root.tsx # Root layout
│ │ ├── login.tsx # Login page (public)
│ │ ├── register.tsx # Registration page (public)
│ │ ├── verify-email.tsx # Email verification (public)
│ │ ├── _authenticated.tsx # Auth guard layout
│ │ ├── _authenticated/
│ │ │ ├── index.tsx # Dashboard (home)
│ │ │ ├── processes/
│ │ │ │ ├── index.tsx # Process list
│ │ │ │ ├── new.tsx # Create process
│ │ │ │ └── $processId.tsx # Process detail
│ │ │ ├── executions/
│ │ │ │ ├── index.tsx # Execution history
│ │ │ │ ├── upcoming.tsx # Upcoming slots
│ │ │ │ ├── timeline.tsx # Timeline view
│ │ │ │ └── $slotId.tsx # Execution detail
│ │ │ ├── queues/
│ │ │ │ ├── index.tsx # Queue overview
│ │ │ │ ├── new.tsx # Create queue
│ │ │ │ └── $queueId.tsx # Queue detail + job list
│ │ │ ├── jobs/
│ │ │ │ ├── index.tsx # All jobs
│ │ │ │ ├── failed.tsx # Failed jobs view
│ │ │ │ └── $jobId.tsx # Job detail
│ │ │ └── settings/
│ │ │ ├── index.tsx # Tenant settings
│ │ │ ├── billing.tsx # Plan + usage
│ │ │ ├── users.tsx # User management
│ │ │ ├── api-keys.tsx # API key management
│ │ │ └── ssh-keys.tsx # SSH key management
│ │
│ ├── lib/ # Utilities
│ │ ├── utils.ts # cn() helper, formatters
│ │ ├── constants.ts # State colors, target icons, plan limits
│ │ └── validators.ts # Zod schemas for forms
│ │
│ └── styles/
│ └── globals.css # Tailwind base + shadcn theme
│
├── index.html
├── vite.config.ts
├── tailwind.config.ts
├── tsconfig.json
├── components.json # shadcn/ui config
└── package.json
Built on shadcn/ui default theme with CronControl customizations:
- Primary: Indigo/Blue (action buttons, active states)
- Destructive: Red (kill, delete, failed states)
- Warning: Amber (hung, retrying, approaching limits)
- Success: Green (completed, healthy)
- Muted: Gray (disabled, skipped, paused)
Dark mode supported via shadcn theme toggle.
| State | Color | Badge variant |
|---|---|---|
pending |
Blue | outline |
queued |
Blue | secondary |
running |
Indigo | default (pulsing dot) |
completed |
Green | success |
failed |
Red | destructive |
hung |
Amber | warning |
killed |
Red | destructive (outline) |
skipped |
Gray | secondary |
cancelled |
Gray | outline |
paused |
Amber | warning (outline) |
retrying |
Amber | warning |
| Target | Icon (Lucide) |
|---|---|
local |
Terminal |
http |
Globe |
ssh |
KeyRound |
ssm |
Cloud |
k8s |
Container |
- Sidebar: Fixed left, collapsible, 240px wide
- Logo + tenant name
- Navigation groups: Scheduler (Dashboard, Processes, Executions, Timeline), Queue (Queues, Jobs, Failed), Settings
- Plan badge at bottom (Free/Pro/Enterprise)
- Header: Top bar with breadcrumbs, search (Command+K), user menu (avatar, role, logout)
- Content: Scrollable main area with page-level padding
- Desktop-first (primary use case is monitoring dashboard)
- Sidebar collapses to icons on medium screens
- Tables switch to card layout on mobile
- Dialogs → full-screen sheets on mobile
- Google OAuth button (primary)
- Email + password form (secondary)
- Link to registration
- Clean, centered layout (no sidebar)
- Email, name, password fields
- Google OAuth as alternative
- On success: redirect to dashboard with toast showing API key
- "Your API key:
cc_live_...— save it now, it won't be shown again"
- Auto-verify on load
- Success: redirect to dashboard
- Error: show message + resend option
┌──────────────────────────────────────────────────────┐
│ Summary Cards (4) │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │Procs │ │Runnin│ │Failed│ │Queue │ │
│ │ 12 │ │ 3 │ │ 2 │ │ 45 │ │
│ └──────┘ └──────┘ └──────┘ └──────┘ │
│ │
│ ┌─────────────────────┐ ┌──────────────────────────┐│
│ │ Recent Executions │ │ Process Status ││
│ │ (table, last 10) │ │ (list with status dots) ││
│ │ │ │ ││
│ │ name | state | time │ │ daily-report ● ok ││
│ │ ... │ │ sync-users ● running││
│ │ │ │ cleanup ✕ failed ││
│ └─────────────────────┘ └──────────────────────────┘│
│ │
│ ┌──────────────────────────────────────────────────┐│
│ │ Usage This Month ││
│ │ ████████████░░░░░░ 680/1000 executions (68%) ││
│ │ ██████░░░░░░░░░░░░ 120/500 queue jobs (24%) ││
│ └──────────────────────────────────────────────────┘│
│ Auto-refresh: 10s │
└──────────────────────────────────────────────────────┘
- TanStack Table with columns: name, schedule (cron preview), target (icon), state (last execution), next run, tags, actions
- Toolbar: search, filter by tag/schedule_type/target/enabled, create button
- Row actions: trigger, pause/resume, edit, delete
- Free tier: "Create Process" shows upgrade prompt if at limit (5)
- Header: Name, schedule badge, target icon, enabled/paused toggle, trigger/edit/delete buttons
- Tabs:
- Overview: Config summary, dependency chain visualization, stats (success rate, avg duration, last 7d)
- Executions: Data table of execution history (filterable by state, date)
- Upcoming: Pending/queued slots with cancel action
- Audit: Configuration change log
- React Hook Form + Zod validation
- Step 1: Basic info (name, schedule type, cron/interval/manual)
- Cron input with real-time preview ("Every day at 13:00 UTC")
- Interval input with human-readable display
- Step 2: Execution target (select target → dynamic config form)
- HTTP: URL, method, headers, body template
- SSH: host/discovery URL, user, key (select from uploaded keys), command
- SSM: region, tag key/value, command
- K8s: namespace, image, command, resources
- Local: command, working directory (disabled on SaaS with tooltip)
- Free tier: Only HTTP selectable, others greyed out with "Pro plan" badge
- Step 3: Behavior (timeout, heartbeat, parallelism, overlap, miss policy)
- Step 4: Advanced (env vars, tags, dependencies)
- Preview before submit
┌──────────────────────────────────────────────────────┐
│ Process: daily-report Target: HTTP Origin: planner │
│ State: ● running Started: 2min ago │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Progress │ │
│ │ ████████████████░░░░░░░░░░ 450/1000 (45%) │ │
│ │ "Processing batch 5 of 10" │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Heartbeat Timeline │ │
│ │ ●────●────●────●────●────● │ │
│ │ 0% 10% 20% 30% 40% 45% │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Output [stdout] [stderr] │ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ > Loading configuration... │ │ │
│ │ │ > Connecting to database... │ │ │
│ │ │ > Processing batch 1 of 10... │ │ │
│ │ │ > Processing batch 2 of 10... │ │ │
│ │ │ █ │ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ │ Auto-scroll: ON │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ [Kill Execution] │
│ Polling: 5s │
└──────────────────────────────────────────────────────────┘
- Progress bar: animated, shows total/current/% + message
- Heartbeat timeline: horizontal dots with timestamps on hover
- Output viewer: monospace, tabs for stdout/stderr, auto-scroll while running, line numbers
- Kill button: confirmation dialog
- Polling every 5s while running, stops when terminal state
- Table of pending/queued slots ordered by scheduled_at
- Columns: process name, scheduled_at, state (pending/queued), origin
- Actions: cancel, view process
- "Add one-time execution" button (opens dialog: select process, pick datetime)
- Horizontal timeline (Recharts or custom)
- Y axis: processes, X axis: time
- Bars colored by state
- Click bar → go to execution detail
- Date range picker (last 1h, 6h, 24h, 7d, custom)
- Cards per queue (shadcn Card):
- Name, target icon, enabled/paused badge
- Stats: pending | running | failed (24h) | completed (24h)
- Health indicator (green/yellow/red based on failure rate)
- Create queue button
- Click card → queue detail
- Queue config + stats header
- Job data table: state, priority, reference, created_at, attempts, actions
- Filters: state, date range, reference search
- Bulk actions: cancel all pending, replay all failed
- Pause/resume queue
┌──────────────────────────────────────────────────────┐
│ Queue: emails State: ● failed Attempts: 3/3 │
│ Reference: order-12345 │
│ │
│ Attempt History │
│ ┌────────────────────────────────────────────────┐ │
│ │ #1 ✕ failed 2s HTTP 500 │ │
│ │ ┌ Request ─────────────────────────────────┐ │ │
│ │ │ POST https://api.example.com/send-email │ │ │
│ │ │ Content-Type: application/json │ │ │
│ │ │ {"to": "user@example.com", ...} │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ │ ┌ Response ────────────────────────────────┐ │ │
│ │ │ 500 Internal Server Error │ │ │
│ │ │ {"error": "SMTP connection failed"} │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ ├────────────────────────────────────────────────┤ │
│ │ #2 ✕ failed 1s HTTP 500 (retry +5m) │ │
│ │ ... │ │
│ ├────────────────────────────────────────────────┤ │
│ │ #3 ✕ failed 1s HTTP 500 (retry +15m) │ │
│ │ ... │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ [Replay Job] (opens form to override fields) │
└──────────────────────────────────────────────────────┘
- Collapsible attempt sections (expand to see full request/response)
- JSON syntax highlighting for bodies
- Retry timeline: visual representation of attempts and wait times
- Replay button: dialog with pre-filled fields, user can override
- Grouped by queue with error distribution:
- "emails: 15 timeout, 3 connection refused, 7 HTTP 500"
- "invoices: 2 HTTP 400"
- One-click replay per job
- Bulk replay per queue or selection
- Date range filter
- Tenant name, timezone selector
- Webhook URL
- Allowed domains (URL allowlist) — tag input
- Current plan card (Free/Pro/Enterprise)
- Usage meters: executions, queue jobs, processes, queues
- Feature comparison table (what's included in each plan)
- Upgrade button (→ Stripe checkout)
- Table: email, name, role, last login, actions
- Invite user dialog (email + role)
- Change role dropdown
- Remove user (confirmation)
- Table: name, prefix (
cc_live_abc1...), role, last used, created by - Create key dialog: name + role → shows full key ONCE in a copiable box
- Revoke key (confirmation)
- Table: name, fingerprint, created at
- Upload key dialog: name + paste private key (file upload or textarea)
- Delete key (confirmation)
- Hidden on free tier (greyed out with upgrade prompt)
All API calls use TanStack Query for caching, deduplication, and background refetching:
// Example: process list
const { data, isLoading } = useQuery({
queryKey: ['processes', { page, filters }],
queryFn: () => api.processes.list({ page, ...filters }),
})
// Example: mutation with cache invalidation
const createProcess = useMutation({
mutationFn: api.processes.create,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['processes'] })
toast.success('Process created')
},
onError: (error) => {
toast.error(error.message, { description: error.hint })
},
})const { data } = useQuery({
queryKey: ['slot', slotId],
queryFn: () => api.slots.get(slotId),
refetchInterval: (query) => {
const state = query.state.data?.data.state
return state === 'running' ? 5000 : false // poll every 5s while running
},
})All API errors return { error: { code, message, hint } }. The HTTP client intercepts errors and creates typed error objects:
class ApiError extends Error {
code: string
hint?: string
}
// Toast errors automatically show hint
toast.error(error.message, { description: error.hint })Plan limit errors (HTTP 402) show an inline upgrade prompt component instead of a toast.
- User clicks "Sign in with Google"
- Redirect to
/auth/google/login - Google OAuth flow
- Backend creates session cookie
- Redirect to
/(dashboard) - Frontend detects session cookie, fetches
/api/v1/tenantto get user + tenant info
- User fills login form → POST
/auth/login - Backend validates, creates session cookie
- Frontend redirects to dashboard
- Session cookie is HTTP-only (not accessible from JS)
- Frontend calls
GET /api/v1/tenanton page load to check if authenticated - If 401 → redirect to
/login - Logout: POST
/auth/logout→ clear cookie → redirect to/login
// _authenticated.tsx (TanStack Router layout)
export const Route = createFileRoute('/_authenticated')({
beforeLoad: async ({ context }) => {
if (!context.auth.isAuthenticated) {
throw redirect({ to: '/login' })
}
},
component: AuthenticatedLayout,
})shadcn Command component. Actions:
- Search processes by name
- Search jobs by reference
- Quick navigate to any page
- Quick actions: trigger process, create queue, view failed jobs
shadcn Sonner toast. Events:
- Success: "Process created", "Execution triggered", "Job replayed"
- Error: API error message + hint
- Warning: "Approaching plan limit (80%)"
- Info: "Email verification sent"
shadcn AlertDialog. For destructive actions:
- Kill execution
- Delete process/queue
- Cancel slot
- Revoke API key
- Bulk replay (shows count)
- Running executions: pulsing dot on state badge
- Dashboard: auto-refresh indicator ("Updated 3s ago")
- Execution detail: live progress bar + auto-scrolling output
Free tier users see the full UI but with clear limitations:
| Feature | Free tier behavior |
|---|---|
| SSH/SSM/K8s targets | Target selector shows lock icon + "Pro" badge, tooltip: "Upgrade to Pro to use SSH targets" |
| Dependencies | Dependency picker disabled + upgrade prompt |
| Webhook alerts | Settings shows "Upgrade to Pro" instead of webhook URL field |
| Users | "Invite user" disabled + upgrade prompt |
| SSH keys | Entire section hidden / shows upgrade card |
| Usage meters | Always visible, turns amber at 80%, red at 95% |
| Creating beyond limit | Dialog shows: "You've reached the free plan limit of 5 processes. Upgrade to Pro for up to 50." with upgrade button |
The upgrade CTA is never blocking — users can always see and navigate the full UI.