403 lines
12 KiB
TypeScript
403 lines
12 KiB
TypeScript
/**
|
|
* 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<string, unknown>;
|
|
|
|
// 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<string, unknown>;
|
|
// 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: '<p>Hello {{name}}</p>' },
|
|
],
|
|
},
|
|
{
|
|
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<string, ScheduleJob>;
|
|
reminders: Record<string, Reminder>;
|
|
workflows: Record<string, Workflow>;
|
|
log: ExecutionLogEntry[];
|
|
}
|
|
|
|
// ── Schema registration ──
|
|
|
|
export const scheduleSchema: DocSchema<ScheduleDoc> = {
|
|
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;
|