/** * rSchedule Automerge document schemas. * * Granularity: one Automerge document per space (all jobs + execution log). * DocId format: {space}:schedule:jobs */ import type { DocSchema } from '../../shared/local-first/document'; // ── Document types ── export type ActionType = 'email' | 'webhook' | 'calendar-event' | 'broadcast' | 'backlog-briefing' | 'calendar-reminder'; export interface ScheduleJob { id: string; name: string; description: string; enabled: boolean; // Timing cronExpression: string; timezone: string; // Action actionType: ActionType; actionConfig: Record; // Execution state lastRunAt: number | null; lastRunStatus: 'success' | 'error' | null; lastRunMessage: string; nextRunAt: number | null; runCount: number; // Metadata createdBy: string; createdAt: number; updatedAt: number; } export interface ExecutionLogEntry { id: string; jobId: string; status: 'success' | 'error'; message: string; durationMs: number; timestamp: number; } export interface Reminder { id: string; title: string; description: string; remindAt: number; // epoch ms — when to fire allDay: boolean; timezone: string; notifyEmail: string | null; notified: boolean; // has email been sent? completed: boolean; // dismissed by user? // Cross-module reference (null for free-form reminders) sourceModule: string | null; // "rwork", "rnotes", etc. sourceEntityId: string | null; sourceLabel: string | null; // "rWork Task" sourceColor: string | null; // "#f97316" // Optional recurrence cronExpression: string | null; // Link to rCal event (bidirectional) calendarEventId: string | null; createdBy: string; createdAt: number; updatedAt: number; } // ── Workflow / Automation types ── export type AutomationNodeType = // Triggers | 'trigger-cron' | 'trigger-data-change' | 'trigger-webhook' | 'trigger-manual' | 'trigger-proximity' // Conditions | 'condition-compare' | 'condition-geofence' | 'condition-time-window' | 'condition-data-filter' // Actions | 'action-send-email' | 'action-post-webhook' | 'action-create-event' | 'action-create-task' | 'action-send-notification' | 'action-update-data'; export type AutomationNodeCategory = 'trigger' | 'condition' | 'action'; export interface WorkflowNodePort { name: string; type: 'trigger' | 'data' | 'boolean'; } export interface WorkflowNode { id: string; type: AutomationNodeType; label: string; position: { x: number; y: number }; config: Record; // Runtime state (not persisted to Automerge — set during execution) runtimeStatus?: 'idle' | 'running' | 'success' | 'error'; runtimeMessage?: string; runtimeDurationMs?: number; } export interface WorkflowEdge { id: string; fromNode: string; fromPort: string; toNode: string; toPort: string; } export interface Workflow { id: string; name: string; enabled: boolean; nodes: WorkflowNode[]; edges: WorkflowEdge[]; lastRunAt: number | null; lastRunStatus: 'success' | 'error' | null; runCount: number; createdAt: number; updatedAt: number; } export interface AutomationNodeDef { type: AutomationNodeType; category: AutomationNodeCategory; label: string; icon: string; description: string; inputs: WorkflowNodePort[]; outputs: WorkflowNodePort[]; configSchema: { key: string; label: string; type: 'text' | 'number' | 'select' | 'textarea' | 'cron'; options?: string[]; placeholder?: string }[]; } export const NODE_CATALOG: AutomationNodeDef[] = [ // ── Triggers ── { type: 'trigger-cron', category: 'trigger', label: 'Cron Schedule', icon: '⏰', description: 'Fire on a cron schedule', inputs: [], outputs: [{ name: 'trigger', type: 'trigger' }, { name: 'timestamp', type: 'data' }], configSchema: [ { key: 'cronExpression', label: 'Cron Expression', type: 'cron', placeholder: '0 9 * * *' }, { key: 'timezone', label: 'Timezone', type: 'text', placeholder: 'America/Vancouver' }, ], }, { type: 'trigger-data-change', category: 'trigger', label: 'Data Change', icon: '📊', description: 'Fire when data changes in any rApp', inputs: [], outputs: [{ name: 'trigger', type: 'trigger' }, { name: 'data', type: 'data' }], configSchema: [ { key: 'module', label: 'Module', type: 'select', options: ['rnotes', 'rwork', 'rcal', 'rnetwork', 'rfiles', 'rvote', 'rflows'] }, { key: 'field', label: 'Field to Watch', type: 'text', placeholder: 'status' }, ], }, { type: 'trigger-webhook', category: 'trigger', label: 'Webhook Incoming', icon: '🔗', description: 'Fire when an external webhook is received', inputs: [], outputs: [{ name: 'trigger', type: 'trigger' }, { name: 'payload', type: 'data' }], configSchema: [ { key: 'hookId', label: 'Hook ID', type: 'text', placeholder: 'auto-generated' }, ], }, { type: 'trigger-manual', category: 'trigger', label: 'Manual Trigger', icon: '👆', description: 'Fire manually via button click', inputs: [], outputs: [{ name: 'trigger', type: 'trigger' }], configSchema: [], }, { type: 'trigger-proximity', category: 'trigger', label: 'Location Proximity', icon: '📍', description: 'Fire when a location approaches a point', inputs: [], outputs: [{ name: 'trigger', type: 'trigger' }, { name: 'distance', type: 'data' }], configSchema: [ { key: 'lat', label: 'Latitude', type: 'number', placeholder: '49.2827' }, { key: 'lng', label: 'Longitude', type: 'number', placeholder: '-123.1207' }, { key: 'radiusKm', label: 'Radius (km)', type: 'number', placeholder: '1' }, ], }, // ── Conditions ── { type: 'condition-compare', category: 'condition', label: 'Compare Values', icon: '⚖️', description: 'Compare two values', inputs: [{ name: 'trigger', type: 'trigger' }, { name: 'value', type: 'data' }], outputs: [{ name: 'true', type: 'trigger' }, { name: 'false', type: 'trigger' }], configSchema: [ { key: 'operator', label: 'Operator', type: 'select', options: ['equals', 'not-equals', 'greater-than', 'less-than', 'contains'] }, { key: 'compareValue', label: 'Compare To', type: 'text', placeholder: 'value' }, ], }, { type: 'condition-geofence', category: 'condition', label: 'Geofence Check', icon: '🗺️', description: 'Check if coordinates are within a radius', inputs: [{ name: 'trigger', type: 'trigger' }, { name: 'coords', type: 'data' }], outputs: [{ name: 'inside', type: 'trigger' }, { name: 'outside', type: 'trigger' }], configSchema: [ { key: 'centerLat', label: 'Center Lat', type: 'number', placeholder: '49.2827' }, { key: 'centerLng', label: 'Center Lng', type: 'number', placeholder: '-123.1207' }, { key: 'radiusKm', label: 'Radius (km)', type: 'number', placeholder: '5' }, ], }, { type: 'condition-time-window', category: 'condition', label: 'Time Window', icon: '🕐', description: 'Check if current time is within a window', inputs: [{ name: 'trigger', type: 'trigger' }], outputs: [{ name: 'in-window', type: 'trigger' }, { name: 'outside', type: 'trigger' }], configSchema: [ { key: 'startHour', label: 'Start Hour (0-23)', type: 'number', placeholder: '9' }, { key: 'endHour', label: 'End Hour (0-23)', type: 'number', placeholder: '17' }, { key: 'days', label: 'Days', type: 'text', placeholder: '1,2,3,4,5' }, ], }, { type: 'condition-data-filter', category: 'condition', label: 'Data Filter', icon: '🔍', description: 'Filter data by field value', inputs: [{ name: 'trigger', type: 'trigger' }, { name: 'data', type: 'data' }], outputs: [{ name: 'match', type: 'trigger' }, { name: 'no-match', type: 'trigger' }, { name: 'filtered', type: 'data' }], configSchema: [ { key: 'field', label: 'Field Path', type: 'text', placeholder: 'status' }, { key: 'operator', label: 'Operator', type: 'select', options: ['equals', 'not-equals', 'contains', 'exists'] }, { key: 'value', label: 'Value', type: 'text', placeholder: 'completed' }, ], }, // ── Actions ── { type: 'action-send-email', category: 'action', label: 'Send Email', icon: '📧', description: 'Send an email via SMTP', inputs: [{ name: 'trigger', type: 'trigger' }, { name: 'data', type: 'data' }], outputs: [{ name: 'done', type: 'trigger' }, { name: 'result', type: 'data' }], configSchema: [ { key: 'to', label: 'To', type: 'text', placeholder: 'user@example.com' }, { key: 'subject', label: 'Subject', type: 'text', placeholder: 'Notification from rSpace' }, { key: 'bodyTemplate', label: 'Body (HTML)', type: 'textarea', placeholder: '

Hello {{name}}

' }, ], }, { type: 'action-post-webhook', category: 'action', label: 'POST Webhook', icon: '🌐', description: 'Send an HTTP POST to an external URL', inputs: [{ name: 'trigger', type: 'trigger' }, { name: 'data', type: 'data' }], outputs: [{ name: 'done', type: 'trigger' }, { name: 'response', type: 'data' }], configSchema: [ { key: 'url', label: 'URL', type: 'text', placeholder: 'https://api.example.com/hook' }, { key: 'method', label: 'Method', type: 'select', options: ['POST', 'PUT', 'PATCH'] }, { key: 'bodyTemplate', label: 'Body Template', type: 'textarea', placeholder: '{"event": "{{event}}"}' }, ], }, { type: 'action-create-event', category: 'action', label: 'Create Calendar Event', icon: '📅', description: 'Create an event in rCal', inputs: [{ name: 'trigger', type: 'trigger' }, { name: 'data', type: 'data' }], outputs: [{ name: 'done', type: 'trigger' }, { name: 'eventId', type: 'data' }], configSchema: [ { key: 'title', label: 'Event Title', type: 'text', placeholder: 'Meeting' }, { key: 'durationMinutes', label: 'Duration (min)', type: 'number', placeholder: '60' }, ], }, { type: 'action-create-task', category: 'action', label: 'Create Task', icon: '✅', description: 'Create a task in rWork', inputs: [{ name: 'trigger', type: 'trigger' }, { name: 'data', type: 'data' }], outputs: [{ name: 'done', type: 'trigger' }, { name: 'taskId', type: 'data' }], configSchema: [ { key: 'title', label: 'Task Title', type: 'text', placeholder: 'New task' }, { key: 'description', label: 'Description', type: 'textarea', placeholder: 'Task details...' }, { key: 'priority', label: 'Priority', type: 'select', options: ['low', 'medium', 'high', 'urgent'] }, ], }, { type: 'action-send-notification', category: 'action', label: 'Send Notification', icon: '🔔', description: 'Send an in-app notification', inputs: [{ name: 'trigger', type: 'trigger' }, { name: 'data', type: 'data' }], outputs: [{ name: 'done', type: 'trigger' }], configSchema: [ { key: 'title', label: 'Title', type: 'text', placeholder: 'Notification' }, { key: 'message', label: 'Message', type: 'textarea', placeholder: 'Something happened...' }, { key: 'level', label: 'Level', type: 'select', options: ['info', 'warning', 'error', 'success'] }, ], }, { type: 'action-update-data', category: 'action', label: 'Update Data', icon: '💾', description: 'Update data in an rApp document', inputs: [{ name: 'trigger', type: 'trigger' }, { name: 'data', type: 'data' }], outputs: [{ name: 'done', type: 'trigger' }, { name: 'result', type: 'data' }], configSchema: [ { key: 'module', label: 'Target Module', type: 'select', options: ['rnotes', 'rwork', 'rcal', 'rnetwork'] }, { key: 'operation', label: 'Operation', type: 'select', options: ['create', 'update', 'delete'] }, { key: 'template', label: 'Data Template (JSON)', type: 'textarea', placeholder: '{"field": "{{value}}"}' }, ], }, ]; export interface ScheduleDoc { meta: { module: string; collection: string; version: number; spaceSlug: string; createdAt: number; }; jobs: Record; reminders: Record; workflows: Record; log: ExecutionLogEntry[]; } // ── Schema registration ── export const scheduleSchema: DocSchema = { module: 'schedule', collection: 'jobs', version: 1, init: (): ScheduleDoc => ({ meta: { module: 'schedule', collection: 'jobs', version: 1, spaceSlug: '', createdAt: Date.now(), }, jobs: {}, reminders: {}, workflows: {}, log: [], }), }; // ── Helpers ── export function scheduleDocId(space: string) { return `${space}:schedule:jobs` as const; } /** Maximum execution log entries to keep per doc */ export const MAX_LOG_ENTRIES = 200; /** Maximum reminders per space */ export const MAX_REMINDERS = 500;