rspace-online/modules/rschedule/schemas.ts

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;