From 020a0437eb9b80a8fe66d42563ad01b6ca8544d3 Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Wed, 25 Feb 2026 08:25:08 -0800 Subject: [PATCH 1/3] Adding workflow module and updating home view. --- docs/api/workflows.md | 433 ++++++++++++++ docs/configuration/workflows.md | 552 ++++++++++++++++++ docs/reference/workflow-variables.md | 518 ++++++++++++++++ docs/web-app/overview.md | 24 +- docs/web-app/workflows.md | 349 +++++++++++ src/components/HomepageFeatures/index.js | 210 +++++-- .../HomepageFeatures/styles.module.css | 248 +++++++- src/pages/index.js | 51 +- src/pages/index.module.css | 150 ++++- 9 files changed, 2470 insertions(+), 65 deletions(-) create mode 100644 docs/api/workflows.md create mode 100644 docs/configuration/workflows.md create mode 100644 docs/reference/workflow-variables.md create mode 100644 docs/web-app/workflows.md diff --git a/docs/api/workflows.md b/docs/api/workflows.md new file mode 100644 index 0000000..bf695ce --- /dev/null +++ b/docs/api/workflows.md @@ -0,0 +1,433 @@ +--- +sidebar_position: 3 +--- + +# Workflows API + +The Workflows API provides full CRUD operations for workflows, workflow steps, credentials, and access to execution history and health metrics. All endpoints are scoped to the authenticated user's department and require Department Admin permissions. + +**Base URL:** `/api/v4/workflows` + +## Authentication + +All Workflows API endpoints require a valid JWT token. See [API Authentication](authentication) for details. + +## Workflows + +### List Workflows + +Returns all workflows for the authenticated user's department. + +``` +GET /api/v4/workflows +``` + +**Response:** Array of `WorkflowResult` objects. + +### Get Workflow + +Returns a specific workflow by ID, including its steps. + +``` +GET /api/v4/workflows/{id} +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `id` | int | Workflow ID | + +**Response:** `WorkflowResult` object with steps. + +### Create Workflow + +Creates a new workflow. + +``` +POST /api/v4/workflows +``` + +**Request Body:** `WorkflowInput` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `Name` | string | Yes | Workflow name (max 250 characters) | +| `Description` | string | No | Description (max 1000 characters) | +| `TriggerEventType` | int | Yes | Event type enum value (see [Event Types](#event-types)) | +| `IsEnabled` | bool | No | Enabled state (default: true) | +| `MaxRetryCount` | int | No | Max retry attempts (default: 3) | +| `RetryBackoffBaseSeconds` | int | No | Backoff base in seconds (default: 5) | + +**Response:** Created `WorkflowResult` object. + +### Update Workflow + +Updates an existing workflow. + +``` +PUT /api/v4/workflows/{id} +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `id` | int | Workflow ID | + +**Request Body:** `WorkflowInput` (same as create). + +**Response:** Updated `WorkflowResult` object. + +### Delete Workflow + +Deletes a workflow and all its steps. + +``` +DELETE /api/v4/workflows/{id} +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `id` | int | Workflow ID | + +**Response:** `200 OK` on success. + +## Workflow Steps + +### Add Step + +Adds a step to a workflow. + +``` +POST /api/v4/workflows/{id}/steps +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `id` | int | Workflow ID | + +**Request Body:** `WorkflowStepInput` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `ActionType` | int | Yes | Action type enum value (see [Action Types](#action-types)) | +| `StepOrder` | int | Yes | Execution order | +| `OutputTemplate` | string | Yes | Scriban template text | +| `ActionConfig` | string | No | JSON action-specific settings | +| `WorkflowCredentialId` | int | No | Credential ID to use | +| `IsEnabled` | bool | No | Enabled state (default: true) | + +**Response:** Created `WorkflowStepResult` object. + +### Update Step + +Updates an existing workflow step. + +``` +PUT /api/v4/workflows/{id}/steps/{stepId} +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `id` | int | Workflow ID | +| `stepId` | int | Step ID | + +**Request Body:** `WorkflowStepInput` (same as add). + +**Response:** Updated `WorkflowStepResult` object. + +### Delete Step + +Deletes a workflow step. + +``` +DELETE /api/v4/workflows/{id}/steps/{stepId} +``` + +**Response:** `200 OK` on success. + +## Workflow Credentials + +### List Credentials + +Returns all credentials for the department. Secret values are masked. + +``` +GET /api/v4/workflows/credentials +``` + +**Response:** Array of `WorkflowCredentialResult` objects (secrets shown as `••••••`). + +### Get Credential + +Returns a specific credential by ID. Secret values are masked. + +``` +GET /api/v4/workflows/credentials/{id} +``` + +**Response:** `WorkflowCredentialResult` object. + +### Create Credential + +Creates a new credential. Accepts plaintext secret values which are encrypted before storage. + +``` +POST /api/v4/workflows/credentials +``` + +**Request Body:** `WorkflowCredentialInput` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `Name` | string | Yes | Friendly name (max 250 characters) | +| `CredentialType` | int | Yes | Credential type enum value (see [Credential Types](#credential-types)) | +| `Data` | object | Yes | Plaintext credential data (type-specific fields) | + +**Response:** Created `WorkflowCredentialResult` object (secrets masked). + +:::warning Write-Only Secrets +Credential secret values are encrypted at rest and never returned in API responses. You can only set them when creating or updating a credential. +::: + +### Update Credential + +Updates an existing credential. Existing secrets are preserved unless new values are provided. + +``` +PUT /api/v4/workflows/credentials/{id} +``` + +**Response:** Updated `WorkflowCredentialResult` object. + +### Delete Credential + +Deletes a credential. Workflows referencing this credential will fail on next execution. + +``` +DELETE /api/v4/workflows/credentials/{id} +``` + +**Response:** `200 OK` on success. + +## Workflow Testing + +### Test Workflow + +Manually triggers a workflow with a sample event payload for testing purposes. + +``` +POST /api/v4/workflows/{id}/test +``` + +**Request Body:** `WorkflowTestInput` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `EventPayloadJson` | string | No | Custom JSON payload (uses sample data if omitted) | + +**Response:** `WorkflowRunResult` object with execution details. + +## Workflow Runs + +### List Runs by Workflow + +Returns paginated runs for a specific workflow. + +``` +GET /api/v4/workflows/{id}/runs?page={page}&pageSize={pageSize} +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `id` | int | — | Workflow ID | +| `page` | int | 1 | Page number | +| `pageSize` | int | 20 | Results per page | + +**Response:** Array of `WorkflowRunResult` objects. + +### List All Runs + +Returns paginated runs for the entire department. + +``` +GET /api/v4/workflows/runs?page={page}&pageSize={pageSize} +``` + +**Response:** Array of `WorkflowRunResult` objects. + +### List Pending Runs + +Returns all pending and in-progress runs for the department. + +``` +GET /api/v4/workflows/runs/pending +``` + +**Response:** Array of `WorkflowRunResult` objects. + +### Get Run Logs + +Returns detailed step-by-step logs for a specific run. + +``` +GET /api/v4/workflows/runs/{runId}/logs +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `runId` | long | Workflow Run ID | + +**Response:** Array of `WorkflowRunLogResult` objects. + +| Field | Type | Description | +|-------|------|-------------| +| `WorkflowRunLogId` | long | Log entry ID | +| `WorkflowStepId` | int | Step that was executed | +| `Status` | int | Step execution status | +| `RenderedOutput` | string | The Scriban-rendered content | +| `ActionResult` | string | HTTP status, SMTP response, etc. | +| `ErrorMessage` | string | Error details (if failed) | +| `StartedOn` | datetime | Step start time | +| `CompletedOn` | datetime | Step completion time | +| `DurationMs` | long | Step execution time in milliseconds | + +### Cancel Run + +Cancels a pending workflow run. + +``` +POST /api/v4/workflows/runs/{runId}/cancel +``` + +**Response:** `200 OK` on success. Returns error if the run is already completed. + +### Clear Pending Runs + +Cancels all pending runs for the department. + +``` +POST /api/v4/workflows/runs/clear +``` + +**Response:** `200 OK` with count of cancelled runs. + +## Workflow Health + +Returns health metrics for a specific workflow. + +``` +GET /api/v4/workflows/{id}/health +``` + +**Response:** `WorkflowHealthResult` + +| Field | Type | Description | +|-------|------|-------------| +| `TotalRuns24h` | int | Total runs in last 24 hours | +| `SuccessRuns24h` | int | Successful runs in last 24 hours | +| `FailedRuns24h` | int | Failed runs in last 24 hours | +| `TotalRuns7d` | int | Total runs in last 7 days | +| `SuccessRuns7d` | int | Successful runs in last 7 days | +| `FailedRuns7d` | int | Failed runs in last 7 days | +| `TotalRuns30d` | int | Total runs in last 30 days | +| `SuccessRuns30d` | int | Successful runs in last 30 days | +| `FailedRuns30d` | int | Failed runs in last 30 days | +| `SuccessRatePercent` | double | Overall success rate percentage | +| `AverageDurationMs` | long | Average run duration in milliseconds | +| `LastRunOn` | datetime | Timestamp of last execution | +| `LastError` | string | Most recent error message | + +## Event Types + +Returns the list of available trigger event types with display names and descriptions. + +``` +GET /api/v4/workflows/eventtypes +``` + +**Response:** Array of event type descriptors with available template variables for each. + +### Event Type Enum Values + +| Value | Name | Description | +|-------|------|-------------| +| 0 | CallAdded | New call/dispatch created | +| 1 | CallUpdated | Existing call updated | +| 2 | CallClosed | Call closed | +| 3 | UnitStatusChanged | Unit status changed | +| 4 | PersonnelStaffingChanged | Personnel staffing level changed | +| 5 | PersonnelStatusChanged | Personnel action status changed | +| 6 | UserCreated | New user added to department | +| 7 | UserAssignedToGroup | User assigned to a group | +| 8 | DocumentAdded | Document uploaded | +| 9 | NoteAdded | Note created | +| 10 | UnitAdded | Unit created | +| 11 | LogAdded | Log entry created | +| 12 | CalendarEventAdded | Calendar event created | +| 13 | CalendarEventUpdated | Calendar event updated | +| 14 | ShiftCreated | Shift created | +| 15 | ShiftUpdated | Shift updated | +| 16 | ResourceOrderAdded | Resource order created | +| 17 | ShiftTradeRequested | Shift trade requested | +| 18 | ShiftTradeFilled | Shift trade filled | +| 19 | MessageSent | New message sent | +| 20 | TrainingAdded | Training created | +| 21 | TrainingUpdated | Training updated | +| 22 | InventoryAdjusted | Inventory quantity changed | +| 23 | CertificationExpiring | Certification nearing expiry | +| 24 | FormSubmitted | Form submitted | +| 25 | PersonnelRoleChanged | User role assignment changed | +| 26 | GroupAdded | Department group created | +| 27 | GroupUpdated | Department group updated | + +## Action Types + +### Action Type Enum Values + +| Value | Name | Description | +|-------|------|-------------| +| 0 | SendEmail | Send email via SMTP | +| 1 | SendSms | Send SMS via Twilio | +| 2 | CallApiGet | HTTP GET request | +| 3 | CallApiPost | HTTP POST request | +| 4 | CallApiPut | HTTP PUT request | +| 5 | CallApiDelete | HTTP DELETE request | +| 6 | UploadFileFtp | Upload file via FTP | +| 7 | UploadFileSftp | Upload file via SFTP | +| 8 | UploadFileS3 | Upload file to Amazon S3 | +| 9 | SendTeamsMessage | Post message to Microsoft Teams | +| 10 | SendSlackMessage | Post message to Slack | +| 11 | SendDiscordMessage | Post message to Discord | +| 12 | UploadFileAzureBlob | Upload file to Azure Blob Storage | +| 13 | UploadFileBox | Upload file to Box | +| 14 | UploadFileDropbox | Upload file to Dropbox | + +## Credential Types + +### Credential Type Enum Values + +| Value | Name | Description | +|-------|------|-------------| +| 0 | Smtp | SMTP email server | +| 1 | Twilio | Twilio SMS service | +| 2 | Ftp | FTP server | +| 3 | Sftp | SFTP server | +| 4 | AwsS3 | Amazon S3 | +| 5 | HttpBearer | HTTP Bearer token authentication | +| 6 | HttpBasic | HTTP Basic authentication | +| 7 | HttpApiKey | HTTP API Key authentication | +| 8 | MicrosoftTeams | Microsoft Teams Incoming Webhook | +| 9 | Slack | Slack Incoming Webhook | +| 10 | Discord | Discord Webhook | +| 11 | AzureBlobStorage | Azure Blob Storage | +| 12 | Box | Box cloud storage | +| 13 | Dropbox | Dropbox cloud storage | + +## Run Status Values + +| Value | Name | Description | +|-------|------|-------------| +| 0 | Pending | Queued for processing | +| 1 | Running | Currently executing | +| 2 | Completed | Finished successfully | +| 3 | Failed | Failed after all retries | +| 4 | Cancelled | Cancelled by user | +| 5 | Retrying | Failed, waiting for retry | diff --git a/docs/configuration/workflows.md b/docs/configuration/workflows.md new file mode 100644 index 0000000..5959bd0 --- /dev/null +++ b/docs/configuration/workflows.md @@ -0,0 +1,552 @@ +--- +sidebar_position: 22 +--- + +# Workflows + +Workflows in Resgrid provide an event-driven automation engine that connects system events to external actions. When something happens in the system — a call is created, a unit changes status, a personnel staffing level changes — workflows can automatically send emails, post messages to Slack or Teams, call external APIs, upload files to cloud storage, and more. Each workflow uses Scriban templates to transform event data into the exact output format your external systems require. + +## Why Workflows Matter + +Departments increasingly need to integrate Resgrid with external systems — CAD platforms, records management systems, alerting services, analytics dashboards, communication channels, and cloud storage. Without workflows, these integrations require custom development or manual processes. Workflows let department admins create these integrations through the web interface without writing code, using a visual template editor with preview capabilities. + +## Scope + +Workflows are department-wide. Each workflow subscribes to a specific system event type and runs for every occurrence of that event within the department. Multiple workflows can subscribe to the same event type. Workflow management is restricted to **Department Admins** by default. + +## How Workflows Work + +1. A system event occurs (e.g., a new call is dispatched) +2. Resgrid checks for active workflows that subscribe to that event type +3. For each matching workflow, the event data is enqueued for background processing +4. Each workflow's steps are executed in order: + - The event data is merged with department and user context + - The step's Scriban template transforms the data into the desired output + - The configured action executes (send email, call API, etc.) +5. Results are recorded — every execution and every step is fully logged + +:::info Asynchronous Processing +Workflow execution happens asynchronously in the background via a message queue. This ensures workflows never slow down the core system — a new call is dispatched immediately, and the workflow processing happens separately. +::: + +## Setting Up Your First Workflow + +### Step 1: Create Credentials (if needed) + +If your workflow needs to send emails, SMS messages, or connect to external services, you'll need to store the authentication credentials first. + +Navigate to **Department → Workflows → Credentials** and click **New Credential**. + +Select the credential type and fill in the required fields: + +| Credential Type | Required Fields | +|----------------|-----------------| +| SMTP | Host, Port, Username, Password, Use SSL, From Address | +| Twilio | Account SID, Auth Token, From Number | +| HTTP Bearer | Bearer Token | +| HTTP Basic | Username, Password | +| HTTP API Key | Header Name, API Key Value | +| FTP | Host, Port, Username, Password | +| SFTP | Host, Port, Username, Password or Private Key | +| AWS S3 | Access Key, Secret Key, Region, Bucket | +| Microsoft Teams | Incoming Webhook URL | +| Slack | Incoming Webhook URL | +| Discord | Webhook URL | +| Azure Blob Storage | Connection String (or Account Name + Account Key), Container Name | +| Box | Developer Token or JWT credentials (Client ID, Client Secret, Enterprise ID, Private Key) | +| Dropbox | App Key, App Secret, OAuth2 Refresh Token | + +:::tip Credential Security +All credential secret values are encrypted at rest using AES-256 encryption with department-specific key derivation. Each department's secrets are cryptographically isolated. Secret values are **write-only** — they are never displayed in the UI or returned via the API after creation. +::: + +### Step 2: Create a Workflow + +Navigate to **Department → Workflows** and click **New Workflow**. + +| Field | Required | Description | +|-------|----------|-------------| +| Name | Yes | A descriptive name (e.g., "Send call alerts to Slack") | +| Description | No | Optional description of the workflow's purpose | +| Trigger Event Type | Yes | The system event that triggers this workflow | +| Enabled | Yes | Whether the workflow is active (default: enabled) | +| Max Retry Count | No | Number of retry attempts on failure (default: 3) | +| Retry Backoff Base | No | Base delay in seconds for exponential backoff (default: 5) | + +### Step 3: Add Steps + +After creating the workflow, add one or more steps. Each step defines a specific action to take with the event data. + +| Field | Required | Description | +|-------|----------|-------------| +| Action Type | Yes | What to do (send email, call API, etc.) | +| Credential | Conditional | The stored credential to use for authentication | +| Output Template | Yes | A Scriban template that transforms the event data | +| Action Configuration | Conditional | Action-specific settings (recipients, URLs, etc.) | +| Step Order | Yes | Execution order when multiple steps exist | +| Enabled | Yes | Whether this step is active | + +### Step 4: Write a Template + +The template editor provides: + +- **Scriban syntax highlighting** via the embedded Ace Editor +- **Variable side panel** showing all available variables for the selected event type +- **Preview** to render the template with sample data +- **Test** to execute the workflow with sample data and see real results + +#### Example: Slack Notification for New Calls + +``` +🚨 *New Call: {{ call.name }}* + +*Nature:* {{ call.nature }} +*Priority:* {{ call.priority_text }} +*Address:* {{ call.address }} +*Logged:* {{ call.logged_on | date.to_string "%Y-%m-%d %H:%M" }} +*Reported By:* {{ user.full_name }} + +Department: {{ department.name }} +``` + +#### Example: JSON Payload for an API POST + +```json +{ + "event": "call_added", + "department": "{{ department.name }}", + "call": { + "id": {{ call.id }}, + "name": "{{ call.name }}", + "nature": "{{ call.nature }}", + "priority": {{ call.priority }}, + "address": "{{ call.address }}", + "loggedOn": "{{ call.logged_on | date.to_string "%Y-%m-%dT%H:%M:%SZ" }}" + }, + "reportedBy": "{{ user.full_name }}" +} +``` + +#### Example: Email Body for Unit Status Changes + +```html +

Unit Status Change

+

Unit: {{ unit.name }} ({{ unit.type }})

+

New Status: {{ unit_status.state_text }}

+

Previous Status: {{ previous_unit_status.state_text }}

+

Time: {{ unit_status.timestamp | date.to_string "%H:%M:%S" }}

+{% if unit_status.note != "" %} +

Note: {{ unit_status.note }}

+{% end %} +
+

{{ department.name }} — {{ timestamp.department_now | date.to_string "%Y-%m-%d %H:%M" }}

+``` + +#### Example: Conditional Template for Certification Expiration + +``` +{% if certification.days_until_expiry <= 7 %} +⚠️ URGENT: {{ user.full_name }}'s certification "{{ certification.name }}" expires in {{ certification.days_until_expiry }} days! +{% else %} +📋 Reminder: {{ user.full_name }}'s certification "{{ certification.name }}" expires on {{ certification.expires_on | date.to_string "%Y-%m-%d" }} ({{ certification.days_until_expiry }} days remaining). +{% end %} +``` + +## Trigger Event Types + +The following system events can trigger workflows: + +### Call Events + +| Event | Description | Key Variables | +|-------|-------------|---------------| +| Call Added | New call/dispatch created | `call.*`, `user.*` (reporting user) | +| Call Updated | Existing call updated | `call.*`, `user.*` (reporting user) | +| Call Closed | Call closed | `call.*`, `user.*` (reporting user) | + +### Personnel Events + +| Event | Description | Key Variables | +|-------|-------------|---------------| +| Personnel Staffing Changed | Staffing level changed | `staffing.*`, `previous_staffing.*`, `user.*` | +| Personnel Status Changed | Action status changed | `status.*`, `previous_status.*`, `user.*` | +| User Created | New user added to department | `new_user.*` | +| User Assigned to Group | User assigned to a group | `assigned_user.*`, `group.*`, `previous_group.*` | +| Personnel Role Changed | Role assignment changed | `role_change.*`, `user.*` | + +### Unit Events + +| Event | Description | Key Variables | +|-------|-------------|---------------| +| Unit Status Changed | Unit status changed | `unit_status.*`, `unit.*`, `previous_unit_status.*` | +| Unit Added | New unit created | `unit.*` | + +### Group Events + +| Event | Description | Key Variables | +|-------|-------------|---------------| +| Group Added | Department group created | `group.*` | +| Group Updated | Department group updated | `group.*` | + +### Scheduling Events + +| Event | Description | Key Variables | +|-------|-------------|---------------| +| Shift Created | Shift created | `shift.*` | +| Shift Updated | Shift updated | `shift.*` | +| Shift Trade Requested | Trade requested | `shift_trade.*` | +| Shift Trade Filled | Trade completed | `shift_trade.*`, `user.*` | +| Calendar Event Added | Calendar event created | `calendar.*`, `user.*` | +| Calendar Event Updated | Calendar event updated | `calendar.*`, `user.*` | + +### Content Events + +| Event | Description | Key Variables | +|-------|-------------|---------------| +| Document Added | Document uploaded | `document.*`, `user.*` | +| Note Added | Note created | `note.*`, `user.*` | +| Log Added | Log entry created | `log.*`, `user.*` | +| Message Sent | Message sent | `message.*`, `user.*` | +| Training Added | Training created | `training.*`, `user.*` | +| Training Updated | Training updated | `training.*`, `user.*` | +| Form Submitted | Form submitted | `form.*`, `user.*` | + +### Operational Events + +| Event | Description | Key Variables | +|-------|-------------|---------------| +| Resource Order Added | Resource order created | `order.*` | +| Inventory Adjusted | Inventory changed | `inventory.*`, `user.*` | +| Certification Expiring | Certification nearing expiry | `certification.*`, `user.*` | + +## Template Variables + +Every workflow template has access to three categories of variables: + +### Common Variables (Always Available) + +These are available in every workflow regardless of trigger event type: + +#### Department Variables + +| Variable | Description | +|----------|-------------| +| `{{ department.id }}` | Department ID | +| `{{ department.name }}` | Department name | +| `{{ department.code }}` | 4-character department code | +| `{{ department.type }}` | Department type (Fire, EMS, etc.) | +| `{{ department.time_zone }}` | Department time zone | +| `{{ department.use_24_hour_time }}` | 24-hour time preference (true/false) | +| `{{ department.address.street }}` | Street address | +| `{{ department.address.city }}` | City | +| `{{ department.address.state }}` | State/Province | +| `{{ department.address.postal_code }}` | Postal/ZIP code | +| `{{ department.address.country }}` | Country | +| `{{ department.address.full }}` | Full formatted address | +| `{{ department.phone_number }}` | Department phone number | + +#### Timestamp Variables + +| Variable | Description | +|----------|-------------| +| `{{ timestamp.utc_now }}` | Current UTC timestamp | +| `{{ timestamp.department_now }}` | Current time in department's time zone | +| `{{ timestamp.date }}` | Current date (department TZ) as `yyyy-MM-dd` | +| `{{ timestamp.time }}` | Current time (department TZ) | +| `{{ timestamp.day_of_week }}` | Day name (e.g., "Monday") | + +#### User Variables (Triggering User) + +Populated from the user who triggered the event. Empty if no specific user is associated with the event. + +| Variable | Description | +|----------|-------------| +| `{{ user.id }}` | User ID | +| `{{ user.first_name }}` | First name | +| `{{ user.last_name }}` | Last name | +| `{{ user.full_name }}` | Full name ("First Last") | +| `{{ user.email }}` | Email address | +| `{{ user.mobile_number }}` | Mobile phone number | +| `{{ user.home_number }}` | Home phone number | +| `{{ user.identification_number }}` | ID/badge number | +| `{{ user.username }}` | Login username | +| `{{ user.time_zone }}` | User's personal time zone | + +### Event-Specific Variables + +Each trigger event type adds its own set of variables. For the complete reference of all event-specific variables, see the [Workflow Template Variable Reference](../reference/workflow-variables) page. + +## Action Types + +### Send Email + +Sends an email via a department-supplied SMTP server. The Scriban template renders the **HTML email body**. + +**Credential:** SMTP +**Action Config:** To, CC (optional), Subject + +### Send SMS + +Sends an SMS via Twilio. The Scriban template renders the **message body**. + +**Credential:** Twilio +**Action Config:** To number(s) + +### Send Teams Message + +Posts a message to Microsoft Teams via an Incoming Webhook URL. The Scriban template renders the **message body** — either plain text or an Adaptive Card JSON payload. + +**Credential:** Microsoft Teams (webhook URL) +**Action Config:** Title (optional), Theme Color (optional) + +### Send Slack Message + +Posts a message to Slack via an Incoming Webhook URL. The Scriban template renders the **message text** (supports Slack mrkdwn formatting). + +**Credential:** Slack (webhook URL) +**Action Config:** Channel override (optional), Username (optional), Icon Emoji (optional) + +### Send Discord Message + +Posts a message to Discord via a Webhook URL. The Scriban template renders the **message content**. If the rendered content is valid embed JSON, it is sent as a rich embed. + +**Credential:** Discord (webhook URL) +**Action Config:** Username override (optional), Avatar URL (optional) + +### Call API (GET / POST / PUT / DELETE) + +Sends an HTTP request to an external API endpoint. For POST and PUT, the Scriban template renders the **request body**. For GET and DELETE, the template is not used as a body. + +**Credential:** Optional — HTTP Bearer, HTTP Basic, or HTTP API Key +**Action Config:** URL, Headers (optional), Content Type (optional, default: `application/json`) + +### Upload File (FTP / SFTP) + +Uploads a file to an FTP or SFTP server. The Scriban template renders the **file content**. + +**Credential:** FTP or SFTP +**Action Config:** Remote Path, Filename template + +### Upload File (S3) + +Uploads a file to Amazon S3. The Scriban template renders the **file content**. + +**Credential:** AWS S3 +**Action Config:** S3 key/path + +### Upload File (Azure Blob) + +Uploads a file to Azure Blob Storage. The Scriban template renders the **file content**. + +**Credential:** Azure Blob Storage +**Action Config:** Blob name/path template, Content Type (optional) + +### Upload File (Box) + +Uploads a file to Box cloud storage. The Scriban template renders the **file content**. + +**Credential:** Box (JWT or Developer Token) +**Action Config:** Folder ID, Filename template + +### Upload File (Dropbox) + +Uploads a file to Dropbox. The Scriban template renders the **file content**. + +**Credential:** Dropbox (OAuth2) +**Action Config:** Target path, Filename template, Write Mode (optional, default: add) + +## Retry Behavior + +When a workflow step fails, automatic retries are attempted with exponential backoff: + +| Attempt | Delay | +|---------|-------| +| 1st retry | `base × 2⁰` = 5 seconds (default) | +| 2nd retry | `base × 2¹` = 10 seconds | +| 3rd retry | `base × 2²` = 20 seconds | + +- If retries are exhausted, the run is marked as **Failed** with the final error message. +- All attempts are recorded in the run logs for auditing. +- You can configure `Max Retry Count` and `Retry Backoff Base` per workflow. + +## Rate Limiting + +Workflow execution is rate-limited to **60 executions per minute per department** by default. If a department exceeds this limit, additional workflow events are skipped and a warning is logged. This prevents a single department from overwhelming the system. + +## Monitoring Workflow Runs + +### Viewing Run History + +Navigate to **Department → Workflows → Runs** to see a paginated list of all workflow executions. Each run shows: + +- Timestamp, workflow name, status (color-coded), duration, attempt number, and error summary +- Click to expand and see per-step details (rendered output, action result, errors, duration) + +### Health Dashboard + +Navigate to **Department → Workflows** and click **Health** on a workflow to see: + +- Success/failure counts over 24h, 7d, and 30d +- Success rate percentage +- Average execution duration +- Last run timestamp and last error +- Recent run timeline + +### Managing Pending Runs + +Navigate to **Department → Workflows → Pending** to see all currently queued or in-progress executions. You can: + +- **Cancel** a specific pending run +- **Clear All** pending runs for the entire department + +:::warning Clearing Pending Runs +Clearing all pending runs is a destructive action. Cancelled runs are not retried and the associated events will not trigger the workflow again. +::: + +## Scriban Template Syntax Reference + +Workflows use [Scriban](https://github.com/scriban/scriban) as the template engine. Here is a quick syntax reference: + +### Variable Output + +``` +{{ variable_name }} +{{ object.property }} +``` + +### Conditionals + +``` +{% if call.is_critical %} +CRITICAL ALERT +{% else %} +Standard notification +{% end %} +``` + +### Loops + +``` +{% for item in collection %} +- {{ item.name }} +{% end %} +``` + +### Date Formatting + +``` +{{ call.logged_on | date.to_string "%Y-%m-%d %H:%M:%S" }} +``` + +### String Functions + +``` +{{ call.name | string.upcase }} +{{ call.nature | string.truncate 100 }} +{{ call.address | string.replace " " "+" }} +``` + +### Default Values + +``` +{{ call.notes | object.default "No notes provided" }} +``` + +For full Scriban documentation, see the [Scriban Language Reference](https://github.com/scriban/scriban/blob/master/doc/language.md). + +## Common Workflow Recipes + +### Send a Slack Alert for High-Priority Calls + +**Trigger:** Call Added +**Action:** Send Slack Message + +``` +{% if call.priority <= 1 %} +🚨 *HIGH PRIORITY CALL* + +*{{ call.name }}* +Priority: {{ call.priority_text }} +Nature: {{ call.nature }} +Address: {{ call.address }} +Reported by: {{ user.full_name }} +Time: {{ call.logged_on | date.to_string "%H:%M" }} +{% end %} +``` + +### Post Call Data to an External CAD System + +**Trigger:** Call Added +**Action:** Call API (POST) + +```json +{ + "incidentId": "{{ call.incident_number }}", + "name": "{{ call.name }}", + "nature": "{{ call.nature }}", + "priority": {{ call.priority }}, + "address": "{{ call.address }}", + "coordinates": "{{ call.geo_location }}", + "reportedBy": "{{ user.full_name }}", + "department": "{{ department.name }}", + "timestamp": "{{ call.logged_on | date.to_string "%Y-%m-%dT%H:%M:%SZ" }}" +} +``` + +### Email Alert When Certification Is Expiring + +**Trigger:** Certification Expiring +**Action:** Send Email +**Subject:** `Certification Expiring: {{ certification.name }}` + +```html +

Certification Expiration Notice

+

The following certification is expiring soon:

+ + + + + + +
Person:{{ user.full_name }}
Certification:{{ certification.name }}
Number:{{ certification.number }}
Expires:{{ certification.expires_on | date.to_string "%Y-%m-%d" }}
Days Remaining:{{ certification.days_until_expiry }}
+

This is an automated alert from {{ department.name }}.

+``` + +### Upload Call Report to S3 + +**Trigger:** Call Closed +**Action:** Upload File (S3) +**S3 Key:** `reports/calls/{{ call.id }}-{{ call.logged_on | date.to_string "%Y%m%d" }}.json` + +```json +{ + "callId": {{ call.id }}, + "name": "{{ call.name }}", + "nature": "{{ call.nature }}", + "address": "{{ call.address }}", + "priority": "{{ call.priority_text }}", + "loggedOn": "{{ call.logged_on | date.to_string "%Y-%m-%dT%H:%M:%SZ" }}", + "closedOn": "{{ call.closed_on | date.to_string "%Y-%m-%dT%H:%M:%SZ" }}", + "completedNotes": "{{ call.completed_notes }}", + "department": "{{ department.name }}" +} +``` + +### Notify Discord When Inventory Runs Low + +**Trigger:** Inventory Adjusted +**Action:** Send Discord Message + +``` +{% if inventory.amount < 10 %} +⚠️ **Low Inventory Alert** + +**Item:** {{ inventory.type_name }} +**Current Amount:** {{ inventory.amount }} {{ inventory.unit_of_measure }} +**Previous Amount:** {{ inventory.previous_amount }} +**Location:** {{ inventory.location }} +**Adjusted by:** {{ user.full_name }} + +_{{ department.name }} — {{ timestamp.department_now | date.to_string "%Y-%m-%d %H:%M" }}_ +{% end %} +``` diff --git a/docs/reference/workflow-variables.md b/docs/reference/workflow-variables.md new file mode 100644 index 0000000..8bdb30d --- /dev/null +++ b/docs/reference/workflow-variables.md @@ -0,0 +1,518 @@ +--- +sidebar_position: 7 +title: Workflow Template Variables +--- + +# Workflow Template Variable Reference + +This page provides the complete reference for all template variables available in Resgrid Workflows. Variables use [Scriban](https://github.com/scriban/scriban) syntax: `{{ variable.property }}`. + +Every workflow template has access to **common variables** (department, timestamp, user) plus **event-specific variables** that depend on the trigger event type. + +## Common Variables + +These variables are available in **every** workflow regardless of trigger event type. + +### Department Variables + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ department.id }}` | int | Department ID | +| `{{ department.name }}` | string | Department name | +| `{{ department.code }}` | string | 4-character department code | +| `{{ department.type }}` | string | Department type (Fire, EMS, etc.) | +| `{{ department.time_zone }}` | string | Department time zone ID | +| `{{ department.use_24_hour_time }}` | bool | 24-hour time preference | +| `{{ department.created_on }}` | datetime | Department creation date | +| `{{ department.address.street }}` | string | Street address | +| `{{ department.address.city }}` | string | City | +| `{{ department.address.state }}` | string | State/Province | +| `{{ department.address.postal_code }}` | string | Postal/ZIP code | +| `{{ department.address.country }}` | string | Country | +| `{{ department.address.full }}` | string | Full formatted address | +| `{{ department.phone_number }}` | string | Department phone number (from settings) | + +### Timestamp Variables + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ timestamp.utc_now }}` | datetime | Current UTC timestamp | +| `{{ timestamp.department_now }}` | datetime | Current time in department's time zone | +| `{{ timestamp.date }}` | string | Current date (department TZ) as `yyyy-MM-dd` | +| `{{ timestamp.time }}` | string | Current time (department TZ) as `HH:mm:ss` or `hh:mm tt` | +| `{{ timestamp.day_of_week }}` | string | Day name (e.g., "Monday") | + +### User Variables (Triggering User) + +Populated from the user who triggered the event. If no specific user is associated with the event (e.g., Unit Added, Shift Created), these variables are empty/null. + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ user.id }}` | string | User ID | +| `{{ user.first_name }}` | string | First name | +| `{{ user.last_name }}` | string | Last name | +| `{{ user.full_name }}` | string | Full name ("First Last") | +| `{{ user.email }}` | string | Email address | +| `{{ user.mobile_number }}` | string | Mobile phone number | +| `{{ user.home_number }}` | string | Home phone number | +| `{{ user.identification_number }}` | string | ID/badge number | +| `{{ user.username }}` | string | Login username | +| `{{ user.time_zone }}` | string | User's personal time zone | + +--- + +## Event-Specific Variables + +### Call Added / Call Updated / Call Closed + +**Triggering user:** The reporting user (`Call.ReportingUserId`) + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ call.id }}` | int | Call ID | +| `{{ call.number }}` | string | Call number | +| `{{ call.name }}` | string | Call name | +| `{{ call.nature }}` | string | Nature of call | +| `{{ call.notes }}` | string | Call notes | +| `{{ call.address }}` | string | Call address | +| `{{ call.geo_location }}` | string | GPS coordinates | +| `{{ call.type }}` | string | Call type | +| `{{ call.incident_number }}` | string | Incident number | +| `{{ call.reference_number }}` | string | Reference number | +| `{{ call.map_page }}` | string | Map page reference | +| `{{ call.priority }}` | int | Priority level | +| `{{ call.priority_text }}` | string | Priority as text (Low, Medium, High, Emergency) | +| `{{ call.is_critical }}` | bool | Whether the call is critical | +| `{{ call.state }}` | int | Call state code | +| `{{ call.state_text }}` | string | Call state as text (Active, Closed, etc.) | +| `{{ call.source }}` | int | Call source code | +| `{{ call.external_id }}` | string | External identifier | +| `{{ call.logged_on }}` | datetime | When the call was logged | +| `{{ call.closed_on }}` | datetime | When the call was closed (null if open) | +| `{{ call.completed_notes }}` | string | Closure/completion notes | +| `{{ call.contact_name }}` | string | Contact person name | +| `{{ call.contact_number }}` | string | Contact phone number | +| `{{ call.w3w }}` | string | What3Words location | +| `{{ call.dispatch_count }}` | int | Number of dispatched resources | +| `{{ call.dispatch_on }}` | datetime | When dispatch occurred | +| `{{ call.form_data }}` | string | Custom form data | +| `{{ call.is_deleted }}` | bool | Whether the call is deleted | +| `{{ call.deleted_reason }}` | string | Deletion reason | + +--- + +### Unit Status Changed + +**Triggering user:** None (unit-based event; `user.*` variables will be empty) + +#### Current Status + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ unit_status.id }}` | int | Unit state record ID | +| `{{ unit_status.state }}` | int | Current state code | +| `{{ unit_status.state_text }}` | string | Current state as text | +| `{{ unit_status.timestamp }}` | datetime | When the status changed | +| `{{ unit_status.note }}` | string | Status change note | +| `{{ unit_status.latitude }}` | decimal | Latitude at time of change | +| `{{ unit_status.longitude }}` | decimal | Longitude at time of change | +| `{{ unit_status.destination_id }}` | int | Destination ID (if applicable) | + +#### Unit Details + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ unit.id }}` | int | Unit ID | +| `{{ unit.name }}` | string | Unit name | +| `{{ unit.type }}` | string | Unit type | +| `{{ unit.vin }}` | string | Vehicle Identification Number | +| `{{ unit.plate_number }}` | string | License plate number | +| `{{ unit.station_group_id }}` | int | Assigned station group ID | + +#### Previous Status + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ previous_unit_status.state }}` | int | Previous state code | +| `{{ previous_unit_status.state_text }}` | string | Previous state as text | +| `{{ previous_unit_status.timestamp }}` | datetime | Previous status timestamp | + +--- + +### Personnel Staffing Changed + +**Triggering user:** The user whose staffing changed (`UserState.UserId`) + +#### Current Staffing + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ staffing.id }}` | int | Staffing record ID | +| `{{ staffing.state }}` | int | Current staffing state code | +| `{{ staffing.state_text }}` | string | Staffing state as text (Available, Delayed, etc.) | +| `{{ staffing.timestamp }}` | datetime | When the staffing changed | +| `{{ staffing.note }}` | string | Staffing change note | + +#### Previous Staffing + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ previous_staffing.state }}` | int | Previous staffing state code | +| `{{ previous_staffing.state_text }}` | string | Previous staffing state as text | +| `{{ previous_staffing.timestamp }}` | datetime | Previous staffing timestamp | + +--- + +### Personnel Status Changed + +**Triggering user:** The user whose status changed (`ActionLog.UserId`) + +#### Current Status + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ status.id }}` | int | Action log record ID | +| `{{ status.action_type }}` | int | Action type code | +| `{{ status.action_text }}` | string | Action as text (Standing By, Responding, etc.) | +| `{{ status.timestamp }}` | datetime | When the status changed | +| `{{ status.geo_location }}` | string | GPS coordinates at time of change | +| `{{ status.destination_id }}` | int | Destination ID (if applicable) | +| `{{ status.note }}` | string | Status change note | + +#### Previous Status + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ previous_status.action_type }}` | int | Previous action type code | +| `{{ previous_status.action_text }}` | string | Previous action as text | +| `{{ previous_status.timestamp }}` | datetime | Previous status timestamp | + +--- + +### User Created + +**Triggering user:** The newly created user + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ new_user.id }}` | string | New user's ID | +| `{{ new_user.username }}` | string | New user's username | +| `{{ new_user.email }}` | string | New user's email | +| `{{ new_user.name }}` | string | New user's display name | + +--- + +### User Assigned to Group + +**Triggering user:** The user being assigned (`UserAssignedToGroupEvent.UserId`) + +#### Assigned User + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ assigned_user.id }}` | string | User ID | +| `{{ assigned_user.name }}` | string | User name | + +#### New Group + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ group.id }}` | int | Group ID | +| `{{ group.name }}` | string | Group name | +| `{{ group.type }}` | int | Group type | +| `{{ group.dispatch_email }}` | string | Group dispatch email | +| `{{ group.latitude }}` | string | Group latitude | +| `{{ group.longitude }}` | string | Group longitude | + +#### Previous Group + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ previous_group.id }}` | int | Previous group ID | +| `{{ previous_group.name }}` | string | Previous group name | + +--- + +### Document Added + +**Triggering user:** The user who uploaded the document (`Document.UserId`) + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ document.id }}` | int | Document ID | +| `{{ document.name }}` | string | Document name | +| `{{ document.category }}` | string | Document category | +| `{{ document.description }}` | string | Document description | +| `{{ document.type }}` | string | Document type | +| `{{ document.filename }}` | string | Original filename | +| `{{ document.admins_only }}` | bool | Whether restricted to admins | +| `{{ document.added_on }}` | datetime | Upload timestamp | + +--- + +### Note Added + +**Triggering user:** The user who created the note (`Note.UserId`) + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ note.id }}` | int | Note ID | +| `{{ note.title }}` | string | Note title | +| `{{ note.body }}` | string | Note body text | +| `{{ note.color }}` | string | Note color | +| `{{ note.category }}` | string | Note category | +| `{{ note.is_admin_only }}` | bool | Whether restricted to admins | +| `{{ note.added_on }}` | datetime | Creation timestamp | +| `{{ note.expires_on }}` | datetime | Expiration date (if set) | + +--- + +### Unit Added + +**Triggering user:** None + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ unit.id }}` | int | Unit ID | +| `{{ unit.name }}` | string | Unit name | +| `{{ unit.type }}` | string | Unit type | +| `{{ unit.vin }}` | string | Vehicle Identification Number | +| `{{ unit.plate_number }}` | string | License plate number | +| `{{ unit.station_group_id }}` | int | Assigned station group ID | +| `{{ unit.four_wheel }}` | bool | Four-wheel drive capability | +| `{{ unit.special_permit }}` | bool | Special permit status | + +--- + +### Log Added + +**Triggering user:** The user who created the log (`Log.LoggedByUserId`) + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ log.id }}` | int | Log ID | +| `{{ log.narrative }}` | string | Log narrative | +| `{{ log.type }}` | string | Log type name | +| `{{ log.log_type }}` | int | Log type code | +| `{{ log.external_id }}` | string | External identifier | +| `{{ log.initial_report }}` | string | Initial report text | +| `{{ log.course }}` | string | Training course name | +| `{{ log.course_code }}` | string | Training course code | +| `{{ log.instructors }}` | string | Instructor names | +| `{{ log.cause }}` | string | Incident cause | +| `{{ log.contact_name }}` | string | Contact person name | +| `{{ log.contact_number }}` | string | Contact phone number | +| `{{ log.location }}` | string | Log location | +| `{{ log.started_on }}` | datetime | Activity start time | +| `{{ log.ended_on }}` | datetime | Activity end time | +| `{{ log.logged_on }}` | datetime | When the log was created | +| `{{ log.other_agencies }}` | string | Other agencies involved | +| `{{ log.other_units }}` | string | Other units involved | +| `{{ log.other_personnel }}` | string | Other personnel involved | +| `{{ log.call_id }}` | int | Associated call ID (if linked) | + +--- + +### Calendar Event Added / Calendar Event Updated + +**Triggering user:** The event creator (`CalendarItem.CreatorUserId`) + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ calendar.id }}` | int | Calendar item ID | +| `{{ calendar.title }}` | string | Event title | +| `{{ calendar.description }}` | string | Event description | +| `{{ calendar.location }}` | string | Event location | +| `{{ calendar.start }}` | datetime | Start date/time | +| `{{ calendar.end }}` | datetime | End date/time | +| `{{ calendar.is_all_day }}` | bool | Whether it's an all-day event | +| `{{ calendar.item_type }}` | int | Calendar item type code | +| `{{ calendar.signup_type }}` | int | Signup type code | +| `{{ calendar.is_public }}` | bool | Whether the event is public | + +--- + +### Shift Created / Shift Updated + +**Triggering user:** None + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ shift.id }}` | int | Shift ID | +| `{{ shift.name }}` | string | Shift name | +| `{{ shift.code }}` | string | Shift code | +| `{{ shift.schedule_type }}` | int | Schedule type code | +| `{{ shift.assignment_type }}` | int | Assignment type code | +| `{{ shift.color }}` | string | Display color | +| `{{ shift.start_day }}` | datetime | Shift start day | +| `{{ shift.start_time }}` | string | Shift start time | +| `{{ shift.end_time }}` | string | Shift end time | +| `{{ shift.hours }}` | int | Shift duration in hours | +| `{{ shift.department_number }}` | string | Department number | + +--- + +### Resource Order Added + +**Triggering user:** None (department-level event) + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ order.id }}` | int | Resource order ID | +| `{{ order.title }}` | string | Order title | +| `{{ order.incident_number }}` | string | Incident number | +| `{{ order.incident_name }}` | string | Incident name | +| `{{ order.incident_address }}` | string | Incident address | +| `{{ order.summary }}` | string | Order summary | +| `{{ order.open_date }}` | datetime | Order open date | +| `{{ order.needed_by }}` | datetime | Resources needed by date | +| `{{ order.contact_name }}` | string | Contact person name | +| `{{ order.contact_number }}` | string | Contact phone number | +| `{{ order.special_instructions }}` | string | Special instructions | +| `{{ order.meetup_location }}` | string | Meetup location | +| `{{ order.financial_code }}` | string | Financial/billing code | + +--- + +### Shift Trade Requested + +**Triggering user:** None + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ shift_trade.id }}` | int | Shift trade ID | +| `{{ shift_trade.department_number }}` | string | Department number | + +--- + +### Shift Trade Filled + +**Triggering user:** The user who filled the trade (`ShiftTradeFilledEvent.UserId`) + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ shift_trade.id }}` | int | Shift trade ID | +| `{{ shift_trade.filled_by_user_id }}` | string | User who filled the trade | +| `{{ shift_trade.department_number }}` | string | Department number | + +--- + +### Message Sent + +**Triggering user:** The user who sent the message (`Message.SendingUserId`) + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ message.id }}` | int | Message ID | +| `{{ message.subject }}` | string | Message subject | +| `{{ message.body }}` | string | Message body | +| `{{ message.is_broadcast }}` | bool | Whether it's a broadcast message | +| `{{ message.sent_on }}` | datetime | When the message was sent | +| `{{ message.type }}` | int | Message type code | +| `{{ message.recipients }}` | string | Message recipients | +| `{{ message.expire_on }}` | datetime | Message expiration date (if set) | + +--- + +### Training Added / Training Updated + +**Triggering user:** The user who created the training (`Training.CreatedByUserId`) + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ training.id }}` | int | Training ID | +| `{{ training.name }}` | string | Training name | +| `{{ training.description }}` | string | Training description | +| `{{ training.training_text }}` | string | Training content text | +| `{{ training.minimum_score }}` | double | Minimum passing score | +| `{{ training.created_on }}` | datetime | Creation date | +| `{{ training.to_be_completed_by }}` | datetime | Completion deadline (if set) | + +--- + +### Inventory Adjusted + +**Triggering user:** The user who made the adjustment (`Inventory.AddedByUserId`) + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ inventory.id }}` | int | Inventory record ID | +| `{{ inventory.type_name }}` | string | Inventory type name | +| `{{ inventory.type_description }}` | string | Inventory type description | +| `{{ inventory.unit_of_measure }}` | string | Unit of measure | +| `{{ inventory.batch }}` | string | Batch identifier | +| `{{ inventory.note }}` | string | Adjustment note | +| `{{ inventory.location }}` | string | Storage location | +| `{{ inventory.amount }}` | double | Current amount | +| `{{ inventory.previous_amount }}` | double | Amount before adjustment | +| `{{ inventory.timestamp }}` | datetime | Adjustment timestamp | +| `{{ inventory.group_id }}` | int | Group/station ID | + +--- + +### Certification Expiring + +**Triggering user:** The user whose certification is expiring (`PersonnelCertification.UserId`) + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ certification.id }}` | int | Certification record ID | +| `{{ certification.name }}` | string | Certification name | +| `{{ certification.number }}` | string | Certification number | +| `{{ certification.type }}` | string | Certification type | +| `{{ certification.area }}` | string | Certification area/specialty | +| `{{ certification.issued_by }}` | string | Issuing authority | +| `{{ certification.expires_on }}` | datetime | Expiration date | +| `{{ certification.received_on }}` | datetime | Date received | +| `{{ certification.days_until_expiry }}` | int | Days until expiration | + +--- + +### Form Submitted + +**Triggering user:** The user who submitted the form (`FormSubmittedEvent.SubmittedByUserId`) + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ form.id }}` | string | Form ID | +| `{{ form.name }}` | string | Form name | +| `{{ form.type }}` | int | Form type code | +| `{{ form.submitted_data }}` | string | Submitted form data (JSON) | +| `{{ form.submitted_by_user_id }}` | string | Submitting user ID | +| `{{ form.submitted_on }}` | datetime | Submission timestamp | + +--- + +### Personnel Role Changed + +**Triggering user:** The user whose role changed (`PersonnelRoleChangedEvent.UserId`) + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ role_change.user_id }}` | string | Affected user ID | +| `{{ role_change.role_id }}` | int | Personnel role ID | +| `{{ role_change.role_name }}` | string | Role name | +| `{{ role_change.role_description }}` | string | Role description | +| `{{ role_change.action }}` | string | "Added" or "Removed" | + +--- + +### Group Added / Group Updated + +**Triggering user:** None + +| Variable | Type | Description | +|----------|------|-------------| +| `{{ group.id }}` | int | Group ID | +| `{{ group.name }}` | string | Group name | +| `{{ group.type }}` | int | Group type code | +| `{{ group.dispatch_email }}` | string | Dispatch email address | +| `{{ group.message_email }}` | string | Message email address | +| `{{ group.latitude }}` | string | Latitude | +| `{{ group.longitude }}` | string | Longitude | +| `{{ group.what3words }}` | string | What3Words location | +| `{{ group.address.street }}` | string | Street address | +| `{{ group.address.city }}` | string | City | +| `{{ group.address.state }}` | string | State/Province | +| `{{ group.address.postal_code }}` | string | Postal/ZIP code | +| `{{ group.address.country }}` | string | Country | diff --git a/docs/web-app/overview.md b/docs/web-app/overview.md index 077e5f0..3191539 100644 --- a/docs/web-app/overview.md +++ b/docs/web-app/overview.md @@ -83,6 +83,7 @@ The User Area is organized into the following major feature areas: | [Voice & Audio](voice-audio) | Voice channels and audio streams | `VoiceController` | | [Connect](connect) | Public department profile and posts | `ConnectController` | | [Search](search) | Quick navigation search | `SearchController` | +| [Workflows](workflows) | Event-driven automation engine with templates and external actions | `WorkflowsController` | ## Event System @@ -100,9 +101,25 @@ The application uses an event aggregation system (`IEventAggregator`) to decoupl | `ShiftTradeFilledEvent` | Completing a shift trade | Confirms trade | | `AuditEvent` | Most write operations | Audit trail recording | | `SecurityRefreshEvent` | Permission changes | Cache invalidation | -| `UnitAddedEvent` | Creating a unit | System integration | -| `DocumentAddedEvent` | Uploading a document | Notification delivery | -| `LogAddedEvent` | Creating a work log | Notification delivery | +| `UnitAddedEvent` | Creating a unit | System integration, workflow triggers | +| `DocumentAddedEvent` | Uploading a document | Notification delivery, workflow triggers | +| `LogAddedEvent` | Creating a work log | Notification delivery, workflow triggers | +| `NoteAddedEvent` | Creating a note | Workflow triggers | +| `UserCreatedEvent` | Adding a user to department | Workflow triggers | +| `UserAssignedToGroupEvent` | Assigning user to a group | Workflow triggers | +| `UserStaffingEvent` | Personnel staffing change | Workflow triggers | +| `UserStatusEvent` | Personnel status change | Workflow triggers | +| `UnitStatusEvent` | Unit status change | Workflow triggers | +| `ResourceOrderAddedEvent` | Creating a resource order | Workflow triggers | +| `MessageSentEvent` | Sending a message | Workflow triggers | +| `TrainingAddedEvent` | Creating a training | Workflow triggers | +| `TrainingUpdatedEvent` | Updating a training | Workflow triggers | +| `InventoryAdjustedEvent` | Adjusting inventory | Workflow triggers | +| `CertificationExpiringEvent` | Certification nearing expiry | Workflow triggers (daily scheduled check) | +| `FormSubmittedEvent` | Submitting a form | Workflow triggers | +| `PersonnelRoleChangedEvent` | Changing user role assignment | Workflow triggers | +| `GroupAddedEvent` | Creating a department group | Workflow triggers | +| `GroupUpdatedEvent` | Updating a department group | Workflow triggers | ## Queue System @@ -110,3 +127,4 @@ Time-sensitive operations like call dispatch use the `IQueueService` to enqueue - **Call Broadcast Queue** — Sends push notifications, SMS, and email to dispatched personnel - **CQRS Event Queue** — Handles cache clearing and other eventual-consistency operations +- **Workflow Queue** — Processes workflow executions asynchronously (event → template rendering → action execution) diff --git a/docs/web-app/workflows.md b/docs/web-app/workflows.md new file mode 100644 index 0000000..56f8888 --- /dev/null +++ b/docs/web-app/workflows.md @@ -0,0 +1,349 @@ +--- +sidebar_position: 37 +title: Workflows +--- + +# Workflows + +The Workflows module provides a powerful event-driven automation engine that lets departments subscribe to system events, transform event data using templates, and execute configurable actions such as sending emails, SMS messages, calling APIs, posting to chat platforms, or uploading files to cloud storage. It is managed by the `WorkflowsController`. + +**Authorization:** Department Admins only. Access is controlled via `ClaimsAuthorizationHelper.IsUserDepartmentAdmin()`. + +**Navigation:** Department Menu → Workflows + +## Overview + +Workflows follow an **Event → Transform → Action** pipeline: + +``` +System Event Occurs → Match Active Workflows → Render Scriban Templates → Execute Action Steps → Log Results +``` + +1. A system event fires (e.g., a new call is created, a unit changes status) +2. Resgrid evaluates all active workflows for the department that subscribe to that event type +3. For each matching workflow, the event data is transformed using user-defined [Scriban](https://github.com/scriban/scriban) templates +4. The rendered output is passed to the configured action (send email, call API, etc.) +5. Every execution is fully audited with run logs and per-step details + +Workflow execution is **asynchronous** — events are enqueued to a message queue and processed in the background, ensuring no impact on the responsiveness of the core system. + +## Workflow List (Index) + +Displays all workflows configured for the department with: + +- **Status indicators** — Enabled/disabled toggle +- **Last run status** — Color-coded badge (green = success, yellow = retrying, red = failed) +- **Success rate** — Percentage badge based on recent runs +- Quick links to create, edit, view runs, and enable/disable workflows + +## Creating a Workflow + +Navigate to **Department → Workflows** and click **New Workflow**. + +### Workflow Fields + +| Field | Required | Description | +|-------|----------|-------------| +| Name | Yes | A descriptive name for the workflow (max 250 characters) | +| Description | No | Optional description of the workflow's purpose (max 1000 characters) | +| Trigger Event Type | Yes | The system event that triggers this workflow (see [Trigger Event Types](#trigger-event-types)) | +| Enabled | Yes | Whether the workflow is active (default: enabled) | +| Max Retry Count | No | Number of retry attempts on failure (default: 3) | +| Retry Backoff Base (seconds) | No | Base delay for exponential backoff between retries (default: 5) | + +## Editing a Workflow + +The edit view allows you to modify the workflow settings and manage its steps inline. + +### Workflow Steps + +Each workflow can have one or more **steps** that execute in sequence. Each step defines: + +| Field | Required | Description | +|-------|----------|-------------| +| Action Type | Yes | The action to perform (see [Action Types](#action-types)) | +| Credential | Conditional | The stored credential to use (filtered by compatible credential types) | +| Output Template | Yes | A [Scriban template](#template-editor) that transforms event data into the action's input | +| Action Config | Conditional | Action-specific settings (e.g., email subject/recipients, API URL, S3 bucket) | +| Enabled | Yes | Whether this step is active (default: enabled) | +| Step Order | Yes | Execution order when multiple steps exist | + +Steps are executed sequentially in `Step Order`. If a step fails, subsequent steps still execute (failures are logged per-step). Disabled steps are skipped. + +### Template Editor + +The workflow editor embeds an **Ace Editor** with Scriban syntax highlighting for editing output templates. Features include: + +- **Syntax highlighting** for Scriban template syntax (`{{ variable }}`, `{% if %}`, loops, etc.) +- **Variable side panel** — Lists all available template variables for the selected trigger event type (e.g., for `CallAdded`: `{{ call.name }}`, `{{ call.nature }}`, `{{ call.address }}`, etc.) +- **Preview button** — Renders the template with sample data and shows the output inline +- **Test button** — Triggers a real execution with sample data and shows the run result + +See the [Workflows Configuration](../configuration/workflows) page for the full template variable reference. + +## Trigger Event Types + +Workflows can subscribe to any of the following system events: + +| Event | Description | +|-------|-------------| +| **Call Added** | New call/dispatch created | +| **Call Updated** | Existing call updated | +| **Call Closed** | Call closed | +| **Unit Status Changed** | Unit status changed | +| **Personnel Staffing Changed** | Personnel staffing level changed | +| **Personnel Status Changed** | Personnel action status changed | +| **User Created** | New user added to department | +| **User Assigned to Group** | User assigned to a group | +| **Document Added** | Document uploaded | +| **Note Added** | Note created | +| **Unit Added** | Unit created | +| **Log Added** | Log entry created | +| **Calendar Event Added** | Calendar event created | +| **Calendar Event Updated** | Calendar event updated | +| **Shift Created** | Shift created | +| **Shift Updated** | Shift updated | +| **Resource Order Added** | Resource order created | +| **Shift Trade Requested** | Shift trade requested | +| **Shift Trade Filled** | Shift trade filled | +| **Message Sent** | New message sent | +| **Training Added** | Training created | +| **Training Updated** | Training updated | +| **Inventory Adjusted** | Inventory quantity changed | +| **Certification Expiring** | Personnel certification nearing expiry | +| **Form Submitted** | Form submitted | +| **Personnel Role Changed** | User role assignment changed | +| **Group Added** | Department group created | +| **Group Updated** | Department group updated | + +## Action Types + +Each workflow step supports one of the following action types: + +### Communication Actions + +| Action | Description | Credential Required | +|--------|-------------|---------------------| +| **Send Email** | Sends an email via SMTP. Template renders the HTML email body. | SMTP (host, port, username, password, from address) | +| **Send SMS** | Sends an SMS message via Twilio. Template renders the message body. | Twilio (Account SID, Auth Token, from number) | +| **Send Teams Message** | Posts a message to Microsoft Teams via Incoming Webhook. Template renders the message body (plain text or Adaptive Card JSON). | Microsoft Teams (webhook URL) | +| **Send Slack Message** | Posts a message to Slack via Incoming Webhook. Template renders the message text (supports Slack mrkdwn). | Slack (webhook URL) | +| **Send Discord Message** | Posts a message to Discord via Webhook. Template renders the message content (supports embed JSON). | Discord (webhook URL) | + +### API Actions + +| Action | Description | Credential Required | +|--------|-------------|---------------------| +| **Call API (GET)** | Sends an HTTP GET request to a URL. | Optional (Bearer, Basic, or API Key) | +| **Call API (POST)** | Sends an HTTP POST request. Template renders the request body. | Optional | +| **Call API (PUT)** | Sends an HTTP PUT request. Template renders the request body. | Optional | +| **Call API (DELETE)** | Sends an HTTP DELETE request. | Optional | + +### File Upload Actions + +| Action | Description | Credential Required | +|--------|-------------|---------------------| +| **Upload File (FTP)** | Uploads a file via FTP. Template renders the file content. | FTP (host, port, username, password) | +| **Upload File (SFTP)** | Uploads a file via SFTP. Template renders the file content. | SFTP (host, port, username, password/key) | +| **Upload File (S3)** | Uploads a file to Amazon S3. Template renders the file content. | AWS S3 (Access Key, Secret Key, Region, Bucket) | +| **Upload File (Azure Blob)** | Uploads a file to Azure Blob Storage. Template renders the file content. | Azure Blob Storage (Connection String, Container Name) | +| **Upload File (Box)** | Uploads a file to Box. Template renders the file content. | Box (JWT credentials or Developer Token) | +| **Upload File (Dropbox)** | Uploads a file to Dropbox. Template renders the file content. | Dropbox (OAuth2 refresh token, app key/secret) | + +### Action Configuration + +Each action type has specific configuration fields set via `Action Config`: + +#### Email Action Config + +| Field | Required | Description | +|-------|----------|-------------| +| To | Yes | Recipient email address(es) | +| CC | No | CC email address(es) | +| Subject | Yes | Email subject line | + +#### SMS Action Config + +| Field | Required | Description | +|-------|----------|-------------| +| To | Yes | Recipient phone number(s) | + +#### API Action Config + +| Field | Required | Description | +|-------|----------|-------------| +| URL | Yes | The API endpoint URL | +| Headers | No | Custom HTTP headers (key-value pairs) | +| Content Type | No | Request content type (default: `application/json`) | + +#### Teams Action Config + +| Field | Required | Description | +|-------|----------|-------------| +| Title | No | Optional message title | +| Theme Color | No | Optional theme color (hex) | + +#### Slack Action Config + +| Field | Required | Description | +|-------|----------|-------------| +| Channel | No | Channel override | +| Username | No | Bot username override | +| Icon Emoji | No | Bot icon emoji | + +#### Discord Action Config + +| Field | Required | Description | +|-------|----------|-------------| +| Username | No | Username override | +| Avatar URL | No | Avatar URL override | + +#### File Upload Action Config (FTP/SFTP) + +| Field | Required | Description | +|-------|----------|-------------| +| Remote Path | Yes | Destination directory on the server | +| Filename | Yes | Filename template for the uploaded file | + +#### S3 Action Config + +| Field | Required | Description | +|-------|----------|-------------| +| Key/Path | Yes | S3 object key/path | + +#### Azure Blob Action Config + +| Field | Required | Description | +|-------|----------|-------------| +| Blob Name/Path | Yes | Blob name or path template | +| Content Type | No | Blob content type | + +#### Box Action Config + +| Field | Required | Description | +|-------|----------|-------------| +| Folder ID | Yes | Target Box folder ID | +| Filename | Yes | Filename template | + +#### Dropbox Action Config + +| Field | Required | Description | +|-------|----------|-------------| +| Target Path | Yes | Destination path in Dropbox | +| Filename | Yes | Filename template | +| Write Mode | No | Overwrite or add (default: add) | + +## Workflow Credentials + +Credentials store the authentication details needed by workflow actions (SMTP passwords, API keys, webhook URLs, etc.). All credentials are **encrypted at rest** using AES-256 encryption with department-specific key derivation, ensuring each department's secrets are isolated. + +### Managing Credentials + +Navigate to **Department → Workflows → Credentials** to manage stored credentials. + +- Credentials are grouped by type for easy browsing +- Secret values are always displayed as `••••••` — they are **write-only** and never returned in responses +- When editing a credential, existing secret values are preserved unless you explicitly provide new values + +### Credential Types + +| Type | Fields | +|------|--------| +| **SMTP** | Host, Port, Username, Password, Use SSL, From Address | +| **Twilio** | Account SID, Auth Token, From Number | +| **FTP** | Host, Port, Username, Password | +| **SFTP** | Host, Port, Username, Password/Private Key | +| **AWS S3** | Access Key, Secret Key, Region, Bucket | +| **HTTP Bearer** | Bearer Token | +| **HTTP Basic** | Username, Password | +| **HTTP API Key** | Header Name, API Key Value | +| **Microsoft Teams** | Webhook URL | +| **Slack** | Webhook URL | +| **Discord** | Webhook URL | +| **Azure Blob Storage** | Connection String (or Account Name + Account Key), Container Name | +| **Box** | Developer Token or JWT credentials (Client ID, Client Secret, Enterprise ID, Private Key) | +| **Dropbox** | App Key, App Secret, OAuth2 Refresh Token | + +### Creating a Credential + +Click **New Credential** and select the credential type. Fill in the type-specific fields — all secret fields are encrypted before storage. + +## Workflow Runs (Execution History) + +The **Runs** view provides a paginated audit trail of all workflow executions: + +| Column | Description | +|--------|-------------| +| Timestamp | When the workflow execution started | +| Workflow Name | The workflow that was triggered | +| Status | Color-coded badge: Pending, Running, Completed, Failed, Cancelled, Retrying | +| Duration | Total execution time | +| Attempt | Current attempt number (if retrying) | +| Error Summary | Brief error description (if failed) | + +Click a run to expand and see per-step `WorkflowRunLog` detail, including: +- Rendered template output +- Action result (HTTP status, SMTP response, etc.) +- Error messages +- Duration per step + +### Filtering Runs + +Filter runs by: +- **Status** — Pending, Running, Completed, Failed, Cancelled, Retrying +- **Date range** — Start and end date +- **Workflow** — Specific workflow + +## Workflow Health + +The **Health** view provides per-workflow health metrics: + +| Metric | Description | +|--------|-------------| +| Success/Failure counts | Broken down by 24h, 7d, and 30d windows | +| Success rate | Percentage of successful runs | +| Average duration | Mean execution time | +| Last run timestamp | When the workflow last executed | +| Last error | Most recent error message | +| Recent run timeline | Visual timeline of recent executions | + +## Pending Runs + +The **Pending** view lists all currently pending and in-progress workflow runs for the department. Actions available: + +- **Cancel** — Cancel an individual pending run +- **Clear All** — Cancel all pending runs for the department (with confirmation dialog) + +## Retry Behavior + +When a workflow step fails: + +1. If `Attempt Number < Max Retry Count`, the run is re-enqueued with exponential backoff delay (`Retry Backoff Base × 2^(attempt - 1)` seconds) +2. Status is set to **Retrying** during the delay +3. If max retries are exceeded, status is set to **Failed** with the final error message +4. All retry attempts are visible in the run logs for auditing + +## Rate Limiting + +Workflow execution is rate-limited per department to prevent a single department from flooding the system. The default limit is **60 workflow executions per minute per department**. If the limit is exceeded, workflow events are skipped and a warning is logged. + +## Interactions with Other Modules + +| Module | Interaction | +|--------|-------------| +| **Dispatch & Calls** | Call Added, Call Updated, Call Closed events trigger workflows | +| **Units** | Unit Status Changed, Unit Added events trigger workflows | +| **Personnel** | Personnel Staffing/Status Changed, User Created, Role Changed events | +| **Groups & Stations** | User Assigned to Group, Group Added/Updated events | +| **Documents** | Document Added events trigger workflows | +| **Notes** | Note Added events trigger workflows | +| **Logs** | Log Added events trigger workflows | +| **Calendar** | Calendar Event Added/Updated events trigger workflows | +| **Shifts** | Shift Created/Updated, Trade Requested/Filled events | +| **Messages** | Message Sent events trigger workflows | +| **Trainings** | Training Added/Updated events trigger workflows | +| **Inventory** | Inventory Adjusted events trigger workflows | +| **Forms** | Form Submitted events trigger workflows | +| **Notifications** | Workflows complement notifications — notifications handle push/SMS to personnel, while workflows enable external integrations | +| **Resource Orders** | Resource Order Added events trigger workflows | +| **Certifications** | Certification Expiring events trigger workflows (via daily scheduled check) | diff --git a/src/components/HomepageFeatures/index.js b/src/components/HomepageFeatures/index.js index 5f3f1cb..df45e6d 100644 --- a/src/components/HomepageFeatures/index.js +++ b/src/components/HomepageFeatures/index.js @@ -1,61 +1,193 @@ import React from 'react'; import clsx from 'clsx'; +import Link from '@docusaurus/Link'; import styles from './styles.module.css'; -const FeatureList = [ +/* ── Quick-start navigation cards ── */ +const QuickLinks = [ { - title: 'Easy to Use', - Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, - description: ( - <> - Easy to use, self service options to configure and manage your Resgrid instance at your own pace. - - ), + title: 'Getting Started', + icon: '🚀', + description: 'Set up your Resgrid account in minutes and start dispatching.', + link: '/get-started/start', + linkLabel: 'Quick Start', }, { - title: 'Open Source CAD', - Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default, - description: ( - <> - Open source computer aided dispatch (CAD) solutions for Emergency Services, Businesses and Industry. - - ), + title: 'Self-Hosted', + icon: '🖥️', + description: 'Deploy Resgrid on your own infrastructure with Docker or bare-metal.', + link: '/get-started/hosted', + linkLabel: 'Installation Guide', }, { - title: 'Run Anywhere', - Svg: require('@site/static/img/undraw_docusaurus_react.svg').default, - description: ( - <> - From hosted to on-prem, mobile applications and more. Resgrid is flexible to meet your needs. - - ), + title: 'API Reference', + icon: '🔌', + description: 'Integrate Resgrid into your workflow with our REST API.', + link: '/api/information', + linkLabel: 'Explore APIs', + }, + { + title: 'Configuration', + icon: '⚙️', + description: 'Fine-tune departments, roles, notifications and more.', + link: '/configuration/setup', + linkLabel: 'Configure', + }, +]; + +/* ── Core platform capabilities ── */ +const Capabilities = [ + { + title: 'Computer Aided Dispatch', + icon: '📡', + description: + 'Create, manage and track calls with real-time status updates, mapping and automatic notifications.', + }, + { + title: 'Personnel & Units', + icon: '👥', + description: + 'Organize your team with roles, groups, stations and unit tracking across shifts.', + }, + { + title: 'Real-Time Mapping', + icon: '🗺️', + description: + 'Visualize calls, personnel and units on live maps with custom layers and geofencing.', + }, + { + title: 'Mobile Applications', + icon: '📱', + description: + 'Native iOS and Android apps for responders, dispatchers and unit commanders.', + }, + { + title: 'Shifts & Scheduling', + icon: '📅', + description: + 'Build recurring shift patterns, manage sign-ups and automate staffing coverage.', + }, + { + title: 'Open Source', + icon: '🔓', + description: + 'Fully open-source under the Apache 2.0 license — audit, extend and self-host with confidence.', }, ]; -function Feature({Svg, title, description}) { +/* ── Explore section cards ── */ +const ExploreLinks = [ + { + title: 'Apps', + description: 'Big Board, Dispatch, Responder, Unit and more.', + link: '/apps/dispatch', + icon: '📋', + }, + { + title: 'Modules', + description: 'Calls, Personnel, Units, Mapping, Shifts, Reports and more.', + link: '/category/modules', + icon: '🧩', + }, + { + title: 'How-To Guides', + description: 'Step-by-step walkthroughs for common tasks.', + link: '/category/how-tos', + icon: '📖', + }, + { + title: 'Development', + description: 'Prerequisites, solution architecture and contributing.', + link: '/development/prerequisites', + icon: '💻', + }, +]; + +/* ── Components ── */ + +function QuickLinkCard({title, icon, description, link, linkLabel}) { return ( -
-
- -
-
-

{title}

-

{description}

-
+ + {icon} +

{title}

+

{description}

+ + {linkLabel} + + + ); +} + +function CapabilityCard({title, icon, description}) { + return ( +
+ {icon} +

{title}

+

{description}

); } +function ExploreCard({title, icon, description, link}) { + return ( + + {icon} +
+

{title}

+

{description}

+
+ + + ); +} + export default function HomepageFeatures() { return ( -
-
-
- {FeatureList.map((props, idx) => ( - - ))} + <> + {/* ── Quick-start cards ── */} +
+
+
+ {QuickLinks.map((props, idx) => ( + + ))} +
-
-
+ + + {/* ── Platform capabilities ── */} +
+
+
+

Platform Capabilities

+

+ Everything you need to manage emergency and non-emergency operations — out of the box. +

+
+
+ {Capabilities.map((props, idx) => ( + + ))} +
+
+
+ + {/* ── Explore section ── */} +
+
+
+

Explore the Docs

+

+ Dive deeper into specific areas of the platform. +

+
+
+ {ExploreLinks.map((props, idx) => ( + + ))} +
+
+
+ ); } diff --git a/src/components/HomepageFeatures/styles.module.css b/src/components/HomepageFeatures/styles.module.css index b248eb2..20af6ce 100644 --- a/src/components/HomepageFeatures/styles.module.css +++ b/src/components/HomepageFeatures/styles.module.css @@ -1,11 +1,247 @@ -.features { +/* ───────────────────────────────────────────── + Quick-start navigation cards + ───────────────────────────────────────────── */ + +.quickLinks { + padding: 3rem 0 2rem; +} + +.quickLinksGrid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1.25rem; +} + +@media screen and (max-width: 996px) { + .quickLinksGrid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media screen and (max-width: 600px) { + .quickLinksGrid { + grid-template-columns: 1fr; + } +} + +.quickLinkCard { + display: flex; + flex-direction: column; + padding: 1.5rem; + border-radius: 12px; + border: 1px solid var(--docs-color-border, #dadde1); + background: var(--docs-color-background, #fff); + text-decoration: none !important; + color: inherit !important; + transition: box-shadow 0.2s ease, border-color 0.2s ease, transform 0.15s ease; +} + +.quickLinkCard:hover { + border-color: #2160fd; + box-shadow: 0 4px 20px rgba(33, 96, 253, 0.08); + transform: translateY(-2px); +} + +:global(html[data-theme='dark']) .quickLinkCard:hover { + border-color: #1a90ff; + box-shadow: 0 4px 20px rgba(26, 144, 255, 0.12); +} + +.cardIcon { + font-size: 1.75rem; + margin-bottom: 0.75rem; +} + +.cardTitle { + font-size: 1rem; + font-weight: 700; + margin: 0 0 0.5rem; + color: var(--docs-color-text, #000); +} + +.cardDescription { + font-size: 0.875rem; + line-height: 1.55; + color: var(--docs-color-text-100, #646464); + flex: 1; + margin: 0 0 1rem; +} + +.cardLink { + font-size: 0.85rem; + font-weight: 600; + color: #2160fd; + display: inline-flex; + align-items: center; + gap: 0.3rem; +} + +:global(html[data-theme='dark']) .cardLink { + color: #1a90ff; +} + +/* ───────────────────────────────────────────── + Section headers (shared) + ───────────────────────────────────────────── */ + +.sectionHeader { + text-align: center; + margin-bottom: 2.5rem; +} + +.sectionTitle { + font-size: 1.75rem; + font-weight: 800; + letter-spacing: -0.02em; + margin-bottom: 0.5rem; + color: var(--docs-color-text, #000); +} + +.sectionSubtitle { + font-size: 1.05rem; + color: var(--docs-color-text-100, #646464); + max-width: 520px; + margin: 0 auto; +} + +/* ───────────────────────────────────────────── + Platform capabilities + ───────────────────────────────────────────── */ + +.capabilities { + padding: 3rem 0; + background: var(--docs-color-background-100, #f8f8f8); +} + +.capabilitiesGrid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1.5rem; +} + +@media screen and (max-width: 996px) { + .capabilitiesGrid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media screen and (max-width: 600px) { + .capabilitiesGrid { + grid-template-columns: 1fr; + } +} + +.capabilityCard { + padding: 1.5rem; + border-radius: 12px; + background: var(--docs-color-background, #fff); + border: 1px solid var(--docs-color-border, #dadde1); + transition: box-shadow 0.2s ease; +} + +.capabilityCard:hover { + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); +} + +:global(html[data-theme='dark']) .capabilityCard:hover { + box-shadow: 0 2px 12px rgba(255, 255, 255, 0.04); +} + +.capIcon { + font-size: 1.5rem; + display: block; + margin-bottom: 0.65rem; +} + +.capTitle { + font-size: 0.95rem; + font-weight: 700; + margin: 0 0 0.35rem; + color: var(--docs-color-text, #000); +} + +.capDescription { + font-size: 0.85rem; + line-height: 1.55; + color: var(--docs-color-text-100, #646464); + margin: 0; +} + +/* ───────────────────────────────────────────── + Explore the docs + ───────────────────────────────────────────── */ + +.explore { + padding: 3rem 0 4rem; +} + +.exploreGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; +} + +@media screen and (max-width: 600px) { + .exploreGrid { + grid-template-columns: 1fr; + } +} + +.exploreCard { display: flex; align-items: center; - padding: 2rem 0; - width: 100%; + gap: 1rem; + padding: 1.15rem 1.35rem; + border-radius: 10px; + border: 1px solid var(--docs-color-border, #dadde1); + background: var(--docs-color-background, #fff); + text-decoration: none !important; + color: inherit !important; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.exploreCard:hover { + border-color: #2160fd; + box-shadow: 0 2px 12px rgba(33, 96, 253, 0.06); +} + +:global(html[data-theme='dark']) .exploreCard:hover { + border-color: #1a90ff; + box-shadow: 0 2px 12px rgba(26, 144, 255, 0.1); +} + +.exploreIcon { + font-size: 1.65rem; + flex-shrink: 0; +} + +.exploreTitle { + font-size: 0.95rem; + font-weight: 700; + margin: 0 0 0.15rem; + color: var(--docs-color-text, #000); +} + +.exploreDescription { + font-size: 0.82rem; + line-height: 1.45; + color: var(--docs-color-text-100, #646464); + margin: 0; +} + +.exploreArrow { + margin-left: auto; + font-size: 1.1rem; + color: var(--docs-color-text-100, #646464); + flex-shrink: 0; + transition: transform 0.15s ease; +} + +.exploreCard:hover .exploreArrow { + transform: translateX(3px); + color: #2160fd; } -.featureSvg { - height: 200px; - width: 200px; +:global(html[data-theme='dark']) .exploreCard:hover .exploreArrow { + color: #1a90ff; } diff --git a/src/pages/index.js b/src/pages/index.js index ee6210a..f18fbca 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -10,16 +10,45 @@ import styles from './index.module.css'; function HomepageHeader() { const {siteConfig} = useDocusaurusContext(); return ( -
+
-

{siteConfig.title}

-

{siteConfig.tagline}

-
- - View The Docs - +
+ Open Source +

Resgrid Documentation

+

+ Open-source computer-aided dispatch & emergency management + platform. Learn how to deploy, configure and extend Resgrid for + your organization. +

+
+ + Get Started + + + API Reference + +
+
+ + GitHub + + · + + Support + + · + + Guides + +
@@ -30,8 +59,8 @@ export default function Home() { const {siteConfig} = useDocusaurusContext(); return ( + title="Resgrid Documentation" + description="Official documentation for Resgrid — open-source computer-aided dispatch and emergency management platform.">
diff --git a/src/pages/index.module.css b/src/pages/index.module.css index 9f71a5d..5569d86 100644 --- a/src/pages/index.module.css +++ b/src/pages/index.module.css @@ -3,21 +3,159 @@ * and scoped locally. */ +/* ── Hero ── */ .heroBanner { - padding: 4rem 0; + padding: 5rem 0 4rem; text-align: center; position: relative; overflow: hidden; + background: linear-gradient(170deg, #f0f4ff 0%, #ffffff 50%, #f8faff 100%); } -@media screen and (max-width: 996px) { - .heroBanner { - padding: 2rem; - } +:global(html[data-theme='dark']) .heroBanner { + background: linear-gradient(170deg, #0d1117 0%, #161b22 50%, #0d1117 100%); +} + +.heroInner { + max-width: 680px; + margin: 0 auto; +} + +.heroBadge { + display: inline-block; + padding: 0.25rem 0.85rem; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + border-radius: 999px; + background: rgba(33, 96, 253, 0.1); + color: #2160fd; + margin-bottom: 1.25rem; +} + +:global(html[data-theme='dark']) .heroBadge { + background: rgba(26, 144, 255, 0.15); + color: #1a90ff; +} + +.heroTitle { + font-size: 2.75rem; + font-weight: 800; + line-height: 1.15; + letter-spacing: -0.025em; + margin-bottom: 1rem; + color: #111827; +} + +:global(html[data-theme='dark']) .heroTitle { + color: #f0f0f0; } -.buttons { +.heroSubtitle { + font-size: 1.15rem; + line-height: 1.65; + color: #4b5563; + margin-bottom: 2rem; + max-width: 560px; + margin-left: auto; + margin-right: auto; +} + +:global(html[data-theme='dark']) .heroSubtitle { + color: #9ca3af; +} + +.heroCtas { display: flex; align-items: center; justify-content: center; + gap: 0.75rem; + margin-bottom: 2rem; + flex-wrap: wrap; +} + +.ctaPrimary { + background: #2160fd !important; + color: #fff !important; + border: none !important; + border-radius: 8px !important; + font-weight: 600; + padding: 0.7rem 1.8rem !important; + transition: background 0.2s ease, transform 0.15s ease; +} + +.ctaPrimary:hover { + background: #174fd6 !important; + transform: translateY(-1px); + text-decoration: none; + color: #fff !important; +} + +.ctaSecondary { + background: transparent !important; + color: #2160fd !important; + border: 1.5px solid #2160fd !important; + border-radius: 8px !important; + font-weight: 600; + padding: 0.7rem 1.8rem !important; + transition: background 0.2s ease, transform 0.15s ease; +} + +:global(html[data-theme='dark']) .ctaSecondary { + color: #1a90ff !important; + border-color: #1a90ff !important; +} + +.ctaSecondary:hover { + background: rgba(33, 96, 253, 0.06) !important; + transform: translateY(-1px); + text-decoration: none; +} + +.heroMeta { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + font-size: 0.875rem; +} + +.heroMetaLink { + color: #6b7280; + text-decoration: none; + transition: color 0.15s; +} + +.heroMetaLink:hover { + color: #2160fd; + text-decoration: none; +} + +:global(html[data-theme='dark']) .heroMetaLink { + color: #9ca3af; +} + +:global(html[data-theme='dark']) .heroMetaLink:hover { + color: #1a90ff; +} + +.heroMetaDivider { + color: #d1d5db; +} + +:global(html[data-theme='dark']) .heroMetaDivider { + color: #4b5563; +} + +@media screen and (max-width: 996px) { + .heroBanner { + padding: 3rem 1.25rem 2.5rem; + } + .heroTitle { + font-size: 2rem; + } + .heroSubtitle { + font-size: 1rem; + } } From 2dfa30f9b28f4e6f7cc89e16e585c6f3355478d9 Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Wed, 25 Feb 2026 19:38:25 -0800 Subject: [PATCH 2/3] Adding verification changes and updating workflow docs --- docs/api/contact-verification.md | 158 +++++++++++++++++ docs/api/workflows.md | 83 ++++++++- docs/configuration/adding-personnel.md | 12 +- docs/configuration/contact-verification.md | 143 +++++++++++++++ docs/configuration/notifications.md | 3 +- docs/configuration/text-messaging.md | 7 + docs/configuration/workflows.md | 197 ++++++++++++++++++++- docs/reference/localization.md | 25 +++ docs/reference/workflow-variables.md | 104 +++++++++++ docs/web-app/dispatch-calls.md | 5 + docs/web-app/messages.md | 1 + docs/web-app/notifications.md | 1 + docs/web-app/overview.md | 1 + docs/web-app/personnel.md | 5 + docs/web-app/profile-account.md | 30 ++++ docs/web-app/voice-audio.md | 1 + docs/web-app/workflows.md | 113 ++++++++++-- 17 files changed, 869 insertions(+), 20 deletions(-) create mode 100644 docs/api/contact-verification.md create mode 100644 docs/configuration/contact-verification.md diff --git a/docs/api/contact-verification.md b/docs/api/contact-verification.md new file mode 100644 index 0000000..0ede2b3 --- /dev/null +++ b/docs/api/contact-verification.md @@ -0,0 +1,158 @@ +--- +sidebar_position: 4 +--- + +# Contact Verification API + +The Contact Verification API allows clients to programmatically send verification codes and confirm contact methods for the authenticated user. These endpoints are used by the profile UI to manage email, mobile number, and home number verification inline. + +## Authentication + +All Contact Verification API endpoints require a valid JWT token. See [API Authentication](authentication) for details. + +**Base URL:** `/api/v4/ContactVerification` + +## Send Verification Code + +Sends a verification code to the specified contact method for the authenticated user. For email, a verification email is sent. For mobile and home numbers, an SMS is sent. + +``` +POST /api/v4/ContactVerification/SendVerificationCode +``` + +**Request Body:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `Type` | int | Yes | Contact verification type: `0` = Email, `1` = MobileNumber, `2` = HomeNumber | + +**Response:** + +| Field | Type | Description | +|-------|------|-------------| +| `Success` | bool | Whether the code was sent successfully | +| `Message` | string | Descriptive message (e.g., "A verification code has been sent." or an error message) | + +**Error Scenarios:** + +| HTTP Status | Condition | +|-------------|-----------| +| `200 OK` | Code sent successfully, or a descriptive error in the response body | +| `429 Too Many Requests` | Rate limit exceeded (max 3 sends per contact method per hour) | +| `400 Bad Request` | Invalid contact type or no contact value on file | + +**Example Request:** + +```bash +curl -X POST \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + "https://api.resgrid.com/api/v4/ContactVerification/SendVerificationCode" \ + -d '{"Type": 0}' +``` + +**Example Response:** + +```json +{ + "Data": { + "Success": true, + "Message": "A verification code has been sent." + }, + "Status": "success" +} +``` + +## Confirm Verification Code + +Validates a verification code entered by the user and, if correct and not expired, marks the contact method as verified. + +``` +POST /api/v4/ContactVerification/ConfirmVerificationCode +``` + +**Request Body:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `Type` | int | Yes | Contact verification type: `0` = Email, `1` = MobileNumber, `2` = HomeNumber | +| `Code` | string | Yes | The 6-digit numeric verification code | + +**Response:** + +| Field | Type | Description | +|-------|------|-------------| +| `Success` | bool | Whether the verification was successful | +| `Message` | string | Descriptive message (e.g., "Verification successful." or an error message) | + +**Error Scenarios:** + +| HTTP Status | Condition | +|-------------|-----------| +| `200 OK` | Verification confirmed or a descriptive error in the response body | +| `429 Too Many Requests` | Daily attempt limit exceeded (max 5 attempts per contact method per day) | +| `400 Bad Request` | Invalid contact type or missing code | + +**Example Request:** + +```bash +curl -X POST \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + "https://api.resgrid.com/api/v4/ContactVerification/ConfirmVerificationCode" \ + -d '{"Type": 1, "Code": "482910"}' +``` + +**Example Response (Success):** + +```json +{ + "Data": { + "Success": true, + "Message": "Verification successful." + }, + "Status": "success" +} +``` + +**Example Response (Failure):** + +```json +{ + "Data": { + "Success": false, + "Message": "Verification failed. Please check the code and try again." + }, + "Status": "success" +} +``` + +## Contact Verification Types + +| Value | Name | Description | +|-------|------|-------------| +| `0` | Email | Email address verification (sends verification email) | +| `1` | MobileNumber | Mobile number verification (sends SMS) | +| `2` | HomeNumber | Home number verification (sends SMS) | + +## Rate Limits + +| Limit | Default | Description | +|-------|---------|-------------| +| Max sends per hour | 3 | Maximum verification code sends per contact method per hour | +| Max attempts per day | 5 | Maximum verification code entry attempts per contact method per day | +| Code expiry | 30 minutes | How long a verification code remains valid | + +When a rate limit is reached, the API returns an appropriate error message. Daily attempt counters reset at midnight UTC. + +## Verification State Model + +Each contact method on a user profile has a tri-state verification value: + +| Value | State | Communications | +|-------|-------|---------------| +| `null` | Grandfathered | Allowed (pre-existing users) | +| `false` | Pending | Blocked until verified | +| `true` | Verified | Allowed | + +For more details on how verification affects dispatches, notifications, and messaging, see the [Contact Method Verification](../configuration/contact-verification) configuration guide. diff --git a/docs/api/workflows.md b/docs/api/workflows.md index bf695ce..139ed75 100644 --- a/docs/api/workflows.md +++ b/docs/api/workflows.md @@ -54,11 +54,15 @@ POST /api/v4/workflows | `Description` | string | No | Description (max 1000 characters) | | `TriggerEventType` | int | Yes | Event type enum value (see [Event Types](#event-types)) | | `IsEnabled` | bool | No | Enabled state (default: true) | -| `MaxRetryCount` | int | No | Max retry attempts (default: 3) | +| `MaxRetryCount` | int | No | Max retry attempts (default: 3, maximum: 5) | | `RetryBackoffBaseSeconds` | int | No | Backoff base in seconds (default: 5) | **Response:** Created `WorkflowResult` object. +:::info Plan-Based Limits +The number of workflows per department is capped by subscription plan (3 for free, 28 for paid). If you exceed the limit, the API returns `400 Bad Request` with a descriptive error message. The `MaxRetryCount` field is capped at a server-side ceiling of **5** regardless of the value provided. +::: + ### Update Workflow Updates an existing workflow. @@ -109,13 +113,17 @@ POST /api/v4/workflows/{id}/steps |-------|------|----------|-------------| | `ActionType` | int | Yes | Action type enum value (see [Action Types](#action-types)) | | `StepOrder` | int | Yes | Execution order | -| `OutputTemplate` | string | Yes | Scriban template text | +| `OutputTemplate` | string | Yes | Scriban template text (max 64 KB) | | `ActionConfig` | string | No | JSON action-specific settings | | `WorkflowCredentialId` | int | No | Credential ID to use | | `IsEnabled` | bool | No | Enabled state (default: true) | **Response:** Created `WorkflowStepResult` object. +:::info Plan-Based Step Limits +The number of steps per workflow is capped by subscription plan (5 for free, 20 for paid). If you exceed the limit, the API returns `400 Bad Request` with a descriptive error message. +::: + ### Update Step Updates an existing workflow step. @@ -183,6 +191,10 @@ POST /api/v4/workflows/credentials **Response:** Created `WorkflowCredentialResult` object (secrets masked). +:::info Plan-Based Credential Limits +The number of stored credentials per department is capped by subscription plan (2 for free, 20 for paid). If you exceed the limit, the API returns `400 Bad Request`. +::: + :::warning Write-Only Secrets Credential secret values are encrypted at rest and never returned in API responses. You can only set them when creating or updating a credential. ::: @@ -400,6 +412,73 @@ GET /api/v4/workflows/eventtypes | 13 | UploadFileBox | Upload file to Box | | 14 | UploadFileDropbox | Upload file to Dropbox | +## Security & Rate Limits + +### Rate Limits + +Workflow execution is rate-limited per department based on subscription plan: + +| Limit | Free Plan | Paid Plans | +|-------|-----------|------------| +| Executions per minute | 5 | 60 | +| Daily run limit | 50 | Unlimited | + +Free-plan rate limits are strictly enforced with no exemptions for any event type. + +### Workflow & Step Caps + +| Limit | Free Plan | Paid Plans | +|-------|-----------|------------| +| Workflows per department | 3 | 28 | +| Steps per workflow | 5 | 20 | +| Credentials per department | 2 | 20 | +| Max retry count (ceiling) | 5 | 5 | + +### Daily Send Limits + +| Channel | Free Plan | Paid Plans | +|---------|-----------|------------| +| Emails per day | 10 | 500 | +| SMS per day | 5 | 200 | + +### Recipient Caps + +| Action | Free Plan | Paid Plans | +|--------|-----------|------------| +| Email (To + CC) | 1 (no CC) | 10 | +| SMS (To) | 1 | 5 | + +### SSRF Protection + +- HTTP API calls require **HTTPS** only +- Private/internal IPs (RFC 1918, loopback, link-local, cloud metadata `169.254.169.254`) are blocked +- FTP/SFTP hosts are subject to the same private-IP restrictions + +### Webhook URL Validation + +- **Teams:** hostname must end with `.webhook.office.com` or `.logic.azure.com` +- **Slack:** hostname must be `hooks.slack.com` +- **Discord:** hostname must be `discord.com` or `discordapp.com` with path starting `/api/webhooks/` + +### Template Sandboxing + +| Protection | Limit | +|------------|-------| +| Loop iterations | 500 max | +| Recursion depth | 50 max | +| Regex timeout | Enforced | +| Output template size (save) | 64 KB | +| Rendered content size | 256 KB | +| `import`/`include` built-ins | Disabled | + +### Email HTML Sanitization + +Rendered email body HTML is sanitized before sending. Dangerous elements (`