commit d1fb9db9d4b46f2ceecb23d6616263b654987358 Author: Lorentz Date: Sat Mar 21 17:52:06 2026 -0400 feat: add Autotask API guide, Pulse DB skill, and Datto RMM OpenClaw skill - AUTOTASK_API_GUIDE.md — auth, query patterns, entity examples, gotchas - PULSE_DATABASE_SKILL.md — full DB schema reference for all data domains - DATTO_RMM_OPENCLAW_SKILL.md — read-only OpenClaw API for devices/sites/alerts diff --git a/AUTOTASK_API_GUIDE.md b/AUTOTASK_API_GUIDE.md new file mode 100644 index 0000000..ac9f93d --- /dev/null +++ b/AUTOTASK_API_GUIDE.md @@ -0,0 +1,748 @@ +# Autotask REST API Development Guide + +## Overview +This guide provides comprehensive instructions for developing applications that integrate with the Autotask REST API v1.0. It's based on real-world implementation experience and includes patterns for authentication, caching, error handling, and user impersonation. + +## Prerequisites + +### Required Credentials +You'll need the following from your Autotask instance: +- **API Username**: Format typically `apiuser@YOURDOMAIN.COM` +- **API Secret/Password**: Strong password for API authentication +- **API Integration Code**: Tracking identifier for your integration +- **API Base URL**: Usually `https://webservices{X}.autotask.net/atservicesrest/v1.0` where X is your zone number + +### Environment Variables +Store these securely in a `.env` file: +```bash +AUTOTASK_API_URL=https://webservices1.autotask.net/atservicesrest/v1.0 +AUTOTASK_USERNAME=your-api-username@yourdomain.com +AUTOTASK_SECRET=your-api-password +AUTOTASK_API_INTEGRATION_CODE=your-tracking-code +``` + +## Core Implementation + +### 1. Basic Authentication Headers + +Every API request requires these headers: + +```javascript +getAuthHeaders(impersonationResourceId = null) { + const credentials = Buffer.from( + `${this.username}:${this.password}` + ).toString('base64'); + + const headers = { + 'Authorization': `Basic ${credentials}`, + 'ApiIntegrationcode': this.apiIntegrationCode, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }; + + // Add impersonation header if needed + if (impersonationResourceId) { + headers['ImpersonationResourceId'] = impersonationResourceId.toString(); + } + + return headers; +} +``` + +### 2. Common API Patterns + +#### Query Pattern +Most Autotask entities support query operations with filters: + +```javascript +async queryEntity(entityName, filters = {}) { + const filterArray = []; + + // Build filter array based on provided criteria + if (filters.fieldName) { + filterArray.push({ + op: 'eq', // Operations: eq, noteq, gt, lt, gte, lte, contains, beginsWith, endsWith + field: 'fieldName', + value: filters.fieldName + }); + } + + // Construct query + const query = filterArray.length > 0 ? { filter: filterArray } : {}; + const queryString = filterArray.length > 0 + ? `?search=${encodeURIComponent(JSON.stringify(query))}` + : ''; + + const url = `${this.baseUrl}/${entityName}/query${queryString}`; + + const response = await fetch(url, { + method: 'GET', + headers: this.getAuthHeaders() + }); + + const result = await response.json(); + return result.items || []; +} +``` + +#### Get by ID Pattern +```javascript +async getEntityById(entityName, id) { + const url = `${this.baseUrl}/${entityName}/${id}`; + + const response = await fetch(url, { + method: 'GET', + headers: this.getAuthHeaders() + }); + + const result = await response.json(); + return result.item; +} +``` + +### 3. Working with Specific Entities + +#### Resources (Users) +```javascript +// Get resource by email +async getResourceByEmail(email) { + const query = { + filter: [{ + op: 'eq', + field: 'email', + value: email + }] + }; + + const url = `${this.baseUrl}/Resources/query?search=${encodeURIComponent(JSON.stringify(query))}`; + const response = await fetch(url, { + method: 'GET', + headers: this.getAuthHeaders() + }); + + const result = await response.json(); + if (result.items && result.items.length > 0) { + const resource = result.items[0]; + return { + id: resource.id, + firstName: resource.firstName, + lastName: resource.lastName, + email: resource.email + }; + } + throw new Error('Resource not found'); +} +``` + +#### Tickets +```javascript +// Get open tickets for a resource (exclude status 5 = Complete) +async getOpenTicketsByResource(resourceId) { + const query = { + filter: [ + { op: 'eq', field: 'assignedResourceID', value: resourceId }, + { op: 'noteq', field: 'status', value: 5 } + ] + }; + + return this.queryEntity('Tickets', query); +} +``` + +#### Tasks +```javascript +// Get tasks for a resource (exclude status 5 = Complete) +async getTasksByResource(resourceId) { + const query = { + filter: [ + { op: 'eq', field: 'assignedResourceID', value: resourceId }, + { op: 'noteq', field: 'status', value: 5 } + ] + }; + + return this.queryEntity('Tasks', query); +} +``` + +#### Companies +```javascript +// Get all active companies +async getAllCompanies() { + const query = { + filter: [{ op: 'eq', field: 'isActive', value: true }] + }; + + const companies = await this.queryEntity('Companies', query); + return companies.map(company => ({ + id: company.id, + name: company.companyName || company.name + })).sort((a, b) => a.name.localeCompare(b.name)); +} +``` + +#### Configuration Items +Configuration Items represent physical or virtual assets (computers, servers, network devices, etc.) associated with companies. + +```javascript +// Get configuration items for a company +async getConfigurationItemsByCompany(companyId) { + const query = { + filter: [ + { op: 'eq', field: 'companyID', value: companyId }, + { op: 'eq', field: 'isActive', value: true } // Only active items + ] + }; + + return this.queryEntity('ConfigurationItems', query); +} + +// Get a single configuration item by ID +async getConfigurationItemById(id) { + return this.getEntityById('ConfigurationItems', id); +} + +// Update a configuration item +async updateConfigurationItem(id, updates) { + // IMPORTANT: Must fetch current item first to get required fields + const currentItem = await this.getConfigurationItemById(id); + + // Autotask requires certain fields even for updates + const updateData = { + id: id, + companyID: currentItem.companyID, // Required + productID: currentItem.productID, // Required + referenceTitle: updates.referenceTitle ?? currentItem.referenceTitle, + isActive: updates.isActive !== undefined ? updates.isActive : currentItem.isActive, + // Optional fields + serialNumber: updates.serialNumber ?? currentItem.serialNumber, + referenceNumber: updates.referenceNumber ?? currentItem.referenceNumber, + location: updates.location ?? currentItem.location, + notes: updates.notes ?? currentItem.notes + }; + + const url = `${this.baseUrl}/ConfigurationItems`; + + const response = await this.makeApiCall(url, { + method: 'PUT', + headers: this.getAuthHeaders(), + body: JSON.stringify(updateData) + }); + + // IMPORTANT: Autotask returns { itemId: X } on successful update, not { item: {...} } + // You must fetch the updated item separately + if (response.itemId) { + return this.getConfigurationItemById(response.itemId); + } + + throw new Error('Failed to update configuration item - no itemId in response'); +} +``` + +**Configuration Item Key Fields:** +- `id` - Unique identifier +- `companyID` - Associated company (required) +- `productID` - Product/asset type (required, even for updates) +- `referenceTitle` - Display name +- `serialNumber` - Device serial number +- `isActive` - Active status (true/false) +- `contactID` - Primary contact for the device +- `rmmDeviceUID` - RMM system unique identifier +- `rmmDeviceAuditIPAddress` - IP address from RMM +- `dattoSerialNumber` - Datto RMM serial number +- `dattoInternalIP` / `dattoRemoteIP` - Datto IP addresses + +#### Contacts +Contacts can be associated with configuration items to track device ownership/responsibility. + +```javascript +// Get contacts for a company +async getContactsByCompany(companyId) { + const query = { + filter: [ + { op: 'eq', field: 'companyID', value: companyId }, + { op: 'eq', field: 'isActive', value: 1 } + ] + }; + + return this.queryEntity('Contacts', query); +} + +// Batch fetch contacts to avoid rate limits +async getContactsBatch(contactIds) { + const contacts = {}; + + // Fetch in parallel but respect rate limits + const promises = contactIds.map(async (id) => { + try { + const contact = await this.getEntityById('Contacts', id); + if (contact) { + contacts[id] = contact; + } + } catch (error) { + console.error(`Failed to fetch contact ${id}:`, error.message); + // Continue with other contacts + } + }); + + await Promise.all(promises); + return contacts; +} +``` + +#### Billing Items +Billing items represent products/services that can be billed to customers. + +```javascript +// Get billing items (products) for a company +async getBillingItemsByCompany(companyId) { + const query = { + filter: [ + { op: 'eq', field: 'companyID', value: companyId } + ] + }; + + return this.queryEntity('BillingItems', query); +} + +// Get all products (for product catalog) +async getAllProducts() { + return this.queryEntity('Products', {}); +} +``` + +### 4. Working with Picklists + +Picklists provide dropdown values for fields like status, priority, etc. + +```javascript +async getPicklistValues(entityName, fieldName) { + const url = `${this.baseUrl}/${entityName}/entityInformation/fields`; + const response = await fetch(url, { + method: 'GET', + headers: this.getAuthHeaders() + }); + + const result = await response.json(); + const field = result.fields.find(f => f.name === fieldName); + + if (field && field.picklistValues) { + const picklistMap = {}; + field.picklistValues.forEach(item => { + picklistMap[item.value] = item.label; + }); + return picklistMap; + } + + return {}; +} + +// Example: Get ticket status labels +async getTicketStatusPicklist() { + return this.getPicklistValues('Tickets', 'status'); +} +``` + +### 5. Uploading Attachments + +```javascript +async uploadAttachment(entityName, entityId, fileBuffer, fileName, impersonatorEmail = null) { + // Get impersonator resource ID if email provided + let impersonationResourceId = null; + if (impersonatorEmail) { + const resource = await this.getResourceByEmail(impersonatorEmail); + impersonationResourceId = resource.id; + } + + // Convert file to base64 + const base64Data = fileBuffer.toString('base64'); + + // Prepare payload + const payload = { + id: 0, + attachmentType: 'FILE_ATTACHMENT', + fullPath: fileName, + title: fileName, + publish: 1, // 1 = All Autotask Users, 2 = Internal Users Only + data: base64Data + }; + + const url = `${this.baseUrl}/${entityName}/${entityId}/Attachments`; + + const response = await fetch(url, { + method: 'POST', + headers: this.getAuthHeaders(impersonationResourceId), + body: JSON.stringify(payload) + }); + + if (!response.ok) { + throw new Error(`Failed to upload attachment: ${response.statusText}`); + } + + return response.json(); +} +``` + +### 6. Implementing Caching with Redis + +To reduce API calls and improve performance: + +```javascript +import { createClient } from 'redis'; + +class CacheService { + constructor() { + this.client = createClient({ + url: process.env.REDIS_URL || 'redis://localhost:6379' + }); + this.client.connect(); + } + + async getCachedData(key, ttlSeconds = 3600) { + const cached = await this.client.get(key); + if (cached) { + return JSON.parse(cached); + } + return null; + } + + async setCachedData(key, data, ttlSeconds = 3600) { + await this.client.setex(key, ttlSeconds, JSON.stringify(data)); + } + + // Cache with TTL strategy + async getWithCache(key, fetchFunction, ttlSeconds = 3600) { + const cached = await this.getCachedData(key); + if (cached) return cached; + + const fresh = await fetchFunction(); + await this.setCachedData(key, fresh, ttlSeconds); + return fresh; + } +} + +// Usage example +async getCompaniesWithCache() { + return cache.getWithCache( + 'companies:all', + () => this.getAllCompanies(), + 86400 // 24 hour TTL + ); +} +``` + +### 7. Error Handling + +Implement robust error handling for API responses: + +```javascript +async makeApiCall(url, options) { + try { + const response = await fetch(url, options); + const responseText = await response.text(); + + if (!response.ok) { + console.error(`Autotask API error: ${response.status} - ${responseText}`); + + // Try to parse error details + let errorMessage = `API Error: ${response.statusText}`; + try { + const errorData = JSON.parse(responseText); + if (errorData.errors && errorData.errors.length > 0) { + errorMessage = errorData.errors.map(e => e.message).join(', '); + } + } catch (parseError) { + errorMessage = responseText || errorMessage; + } + + throw new Error(errorMessage); + } + + // Parse successful response + try { + return JSON.parse(responseText); + } catch (parseError) { + console.error('Failed to parse API response:', parseError.message); + throw new Error('Invalid JSON response from Autotask API'); + } + + } catch (error) { + console.error('API call failed:', error.message); + throw error; + } +} +``` + +### 8. Rate Limiting Considerations + +Autotask has rate limits. Implement throttling: + +```javascript +class RateLimiter { + constructor(maxRequestsPerSecond = 10) { + this.maxRequestsPerSecond = maxRequestsPerSecond; + this.requestTimes = []; + } + + async throttle() { + const now = Date.now(); + const oneSecondAgo = now - 1000; + + // Remove old request times + this.requestTimes = this.requestTimes.filter(t => t > oneSecondAgo); + + // If at limit, wait + if (this.requestTimes.length >= this.maxRequestsPerSecond) { + const oldestRequest = this.requestTimes[0]; + const waitTime = 1000 - (now - oldestRequest); + if (waitTime > 0) { + await new Promise(resolve => setTimeout(resolve, waitTime)); + } + } + + this.requestTimes.push(Date.now()); + } +} + +// Use before API calls +await rateLimiter.throttle(); +const response = await fetch(url, options); +``` + +## Docker/Container Setup + +### Docker Compose Configuration +```yaml +services: + redis: + image: redis:7-alpine + container_name: app-redis + ports: + - "6379:6379" + volumes: + - redis-data:/data + command: redis-server --save 60 1 + restart: unless-stopped + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 3s + retries: 3 + + backend: + build: ./backend + container_name: app-backend + ports: + - "5001:5001" + environment: + - AUTOTASK_API_URL=${AUTOTASK_API_URL} + - AUTOTASK_USERNAME=${AUTOTASK_USERNAME} + - AUTOTASK_SECRET=${AUTOTASK_SECRET} + - AUTOTASK_API_INTEGRATION_CODE=${AUTOTASK_API_INTEGRATION_CODE} + - REDIS_URL=redis://redis:6379 + volumes: + - ./backend:/app + - backend-node-modules:/app/node_modules + depends_on: + redis: + condition: service_healthy + restart: unless-stopped + +volumes: + redis-data: + backend-node-modules: +``` + +## Common Gotchas and Solutions + +### 1. Password Special Characters +If your API password contains `$`, escape it in `.env` files (but NOT in docker-compose.yml environment variables). + +### 2. Entity Status Values +- Tickets: Status 5 = Complete +- Tasks: Status 5 = Complete +- Use picklists to get human-readable labels + +### 3. Query Limits +- Default max results: 500 items +- Implement pagination for large datasets +- Use specific filters to reduce result sets + +### 4. Field Names +- Field names in queries are case-sensitive +- Common fields: `id`, `companyID`, `assignedResourceID`, `status` +- Check entity documentation for exact field names + +### 5. Impersonation +- Use `ImpersonationResourceId` header to act as another user +- Useful for attachments to show correct "uploaded by" user +- Requires resource ID, not email (lookup required) + +### 6. Time Zones +- Autotask uses UTC for all timestamps +- Convert to local timezone for display +- Send in UTC for updates + +### 7. Null Values +- Many fields can be null +- Always check for null before accessing nested properties +- Use optional chaining: `ticket?.companyID` + +### 8. Configuration Item Updates - CRITICAL +**The update response format is different from other entities:** + +```javascript +// ❌ WRONG - This will fail +const response = await updateConfigurationItem(id, data); +return response.item; // item doesn't exist! + +// ✅ CORRECT - Autotask returns itemId, not item +const response = await updateConfigurationItem(id, data); +if (response.itemId) { + // Must fetch the updated item separately + return await getConfigurationItemById(response.itemId); +} +``` + +**Key points:** +- PUT requests to ConfigurationItems return `{ itemId: 12345 }` not `{ item: {...} }` +- You MUST fetch the updated item with a separate GET request +- Always include `productID` and `companyID` even for updates (required fields) +- Fetch the current item first to preserve required fields you're not updating + +### 9. Rate Limiting with Batch Operations +When fetching multiple related entities (like contacts for devices): + +```javascript +// ❌ BAD - Sequential requests, very slow +for (const contactId of contactIds) { + const contact = await getContact(contactId); +} + +// ✅ BETTER - Parallel requests, but can hit rate limits +await Promise.all(contactIds.map(id => getContact(id))); + +// ✅ BEST - Batch fetch on server side, cache results +// Fetch all contacts for a company once, then lookup by ID +const allContacts = await getContactsByCompany(companyId); +const contactMap = {}; +allContacts.forEach(c => contactMap[c.id] = c); +``` + +**Strategy for avoiding rate limits:** +1. Fetch related data in bulk when possible (e.g., all contacts for a company) +2. Cache frequently accessed data (companies, products, picklists) +3. Use batch endpoints when available +4. Implement request throttling/queuing for parallel operations + +### 10. Configuration Item Filtering by Status +When querying configuration items, the `isActive` field behaves differently: + +```javascript +// Get only active items +const query = { + filter: [ + { op: 'eq', field: 'companyID', value: companyId }, + { op: 'eq', field: 'isActive', value: true } // Boolean true + ] +}; + +// Get only inactive items +const query = { + filter: [ + { op: 'eq', field: 'companyID', value: companyId }, + { op: 'eq', field: 'isActive', value: false } // Boolean false + ] +}; + +// Get all items (active and inactive) +const query = { + filter: [ + { op: 'eq', field: 'companyID', value: companyId } + // Don't filter by isActive + ] +}; +``` + +### 11. RMM Integration Fields +Configuration Items have special fields for RMM system integration: + +- `rmmDeviceUID` - Unique identifier from RMM system (use for matching) +- `rmmDeviceAuditIPAddress` - IP address reported by RMM +- `rmmDeviceAuditHostname` - Hostname from RMM +- `dattoSerialNumber` - Datto-specific serial number +- `dattoInternalIP` / `dattoRemoteIP` - Datto-specific IP addresses + +**Best practice for matching RMM devices to Autotask:** +1. First try matching by `rmmDeviceUID` +2. Fall back to `serialNumber` or `dattoSerialNumber` +3. Last resort: match by IP address (less reliable) + +### 12. Contact Association +Contacts can be linked to configuration items via `contactID`: + +```javascript +// Update configuration item with contact +await updateConfigurationItem(itemId, { + contactID: contactId // Links device to a specific contact +}); + +// Remove contact association +await updateConfigurationItem(itemId, { + contactID: null // Removes contact link +}); +``` + +**Note:** Contact must belong to the same company as the configuration item. + +## Testing Strategy + +1. **Start with read-only operations** (GET requests) +2. **Test with a sandbox/development Autotask instance** if available +3. **Cache aggressively** to avoid hitting rate limits during development +4. **Log all API calls** during development for debugging +5. **Implement retry logic** for transient failures + +## Security Best Practices + +1. **Never commit credentials** to version control +2. **Use environment variables** for all sensitive data +3. **Implement proper authentication** in your application +4. **Validate and sanitize** all user inputs +5. **Use HTTPS** for all API communications +6. **Rotate API credentials** regularly +7. **Implement audit logging** for API operations + +## Sample Project Structure + +``` +project/ +├── backend/ +│ ├── src/ +│ │ ├── server.js +│ │ ├── routes/ +│ │ │ ├── tickets.js +│ │ │ └── tasks.js +│ │ └── services/ +│ │ ├── autotask.js +│ │ └── cache.js +│ ├── package.json +│ └── Dockerfile +├── frontend/ +│ ├── src/ +│ │ └── components/ +│ ├── package.json +│ └── Dockerfile +├── docker-compose.yml +├── .env +└── .env.example +``` + +## Additional Resources + +- [Autotask REST API Documentation](https://ww1.autotask.net/help/DeveloperHelp/Content/APIs/REST/REST_API_Home.htm) +- [Autotask Entity Documentation](https://ww1.autotask.net/help/DeveloperHelp/Content/APIs/REST/Entities/Entities_overview.htm) +- Use browser DevTools Network tab to inspect API calls in Autotask UI for field discovery + +--- + +This guide should provide a comprehensive starting point for any AI model to help you develop Autotask API integrations. Adapt the patterns to your specific use case and requirements. \ No newline at end of file diff --git a/DATTO_RMM_OPENCLAW_SKILL.md b/DATTO_RMM_OPENCLAW_SKILL.md new file mode 100644 index 0000000..7b60e32 --- /dev/null +++ b/DATTO_RMM_OPENCLAW_SKILL.md @@ -0,0 +1,288 @@ +# Datto RMM — OpenClaw Read-Only API Skill + +> **Purpose:** Query Datto RMM data (devices, sites, alerts) from the Pulse platform via authenticated read-only OpenClaw API endpoints. All data is sourced from Pulse's local PostgreSQL DB by default, with an option to fetch live from the Datto RMM API. + +## Base URL + +``` +https://pulse.wulfconsulting.cloud +``` + +## Authentication + +Every request requires the `x-openclaw-key` header: + +```http +x-openclaw-key: +``` + +Missing or invalid key → `401 Unauthorized`. + +--- + +## Data Source Behaviour + +| Parameter | Behaviour | +|---|---| +| *(default)* | Queries Pulse's local DB — fast, no Datto rate limits, data is as fresh as the last sync | +| `?live=true` | Proxies directly to the live Datto RMM API — always current but slower | + +Every response includes `"source": "db"` or `"source": "live"` so you always know data freshness. DB responses also include `synced_at` where available. + +--- + +## Endpoints + +### 1. List Sites + +``` +GET /api/openclaw/datto-rmm/sites +GET /api/openclaw/datto-rmm/sites?live=true +``` + +Returns all Datto RMM sites linked to Autotask companies. + +**Response:** +```json +{ + "data": [ + { + "id": 376835, + "uid": "68431d2c-8327-4d86-a05b-b60d4e7f793e", + "name": "Acme Corp", + "autotask_company_id": 29683001, + "autotask_company_name": "Acme Corp", + "number_of_devices": 42, + "number_of_online_devices": 38, + "number_of_offline_devices": 4, + "portal_url": "https://concord.centrastage.net/csm/...", + "synced_at": "2026-03-21T20:00:00Z" + } + ], + "total": 87, + "source": "db" +} +``` + +**Key fields:** +- `uid` — Datto RMM site UID (use for device filtering) +- `autotask_company_id` — links to Autotask `companies.id` in Pulse DB +- `number_of_online_devices` / `number_of_offline_devices` — counts from last sync + +--- + +### 2. List Devices + +``` +GET /api/openclaw/datto-rmm/devices +``` + +**Query parameters:** + +| Param | Type | Description | +|---|---|---| +| `siteUid` | string | Filter by Datto RMM site UID | +| `online` | boolean | `true` / `false` — filter by online status | +| `deleted` | boolean | `true` / `false` — include/exclude deleted devices | +| `page` | int | Page number (default: 1) | +| `limit` | int | Items per page (default: 100, max: 500) | +| `live` | boolean | Use live Datto API instead of DB | + +**Response:** +```json +{ + "data": [ + { + "uid": "abc123-...", + "hostname": "WS-SMITH-01", + "site_uid": "68431d2c-...", + "site_name": "Acme Corp", + "device_type_category": "Desktop", + "device_type": "Windows Workstation", + "operating_system": "Windows 11 Pro", + "domain": "acme.local", + "int_ip_address": "10.1.1.50", + "ext_ip_address": "203.0.113.5", + "online": true, + "last_seen": "2026-03-21T19:45:00Z", + "last_logged_in_user": "jsmith", + "antivirus_product": "Windows Defender", + "antivirus_status": "Fully Protected", + "patch_status": "Fully Patched", + "patches_approved_pending": 0, + "reboot_required": false, + "udf": { "udf1": "...", "udf2": "..." } + } + ], + "total": 3597, + "page": 1, + "limit": 100, + "source": "db" +} +``` + +**Key fields:** +- `uid` — Datto RMM device UID (also stored as `configuration_items.reference_number` in Autotask) +- `device_type_category` — Server, Desktop, Laptop, Network Device +- `antivirus_status` — "Fully Protected", "At Risk", etc. +- `patch_status` — "Fully Patched", "Patches Available", "Reboot Required", etc. +- `udf` — JSONB object with up to 30 user-defined fields + +--- + +### 3. Get Single Device + +``` +GET /api/openclaw/datto-rmm/devices/{uid} +GET /api/openclaw/datto-rmm/devices/{uid}?live=true +``` + +Returns full device record by Datto RMM UID. + +**Response:** +```json +{ + "data": { /* same fields as list, plus: a64_bit, snmp_enabled, network_probe, software_status, warranty_date, cag_version, display_version, web_remote_url */ }, + "source": "db" +} +``` + +Returns `404` if not found. + +--- + +### 4. Get Device Audit Data + +``` +GET /api/openclaw/datto-rmm/devices/{uid}/audit +``` + +> **Always live** — fetches real-time audit data from Datto RMM API. No `?live=true` needed. + +Returns detailed hardware/software audit including: +- CPU, RAM, disk, BIOS +- Network adapters +- Installed software list +- Hardware inventory + +Returns `404` if device has no audit data. + +--- + +### 5. List Alerts + +``` +GET /api/openclaw/datto-rmm/alerts +``` + +**Query parameters:** + +| Param | Type | Description | +|---|---|---| +| `resolved` | boolean | `true` = resolved only, `false` = open only, omit = all | +| `siteUid` | string | Filter by site UID | +| `deviceUid` | string | Filter by device UID | +| `limit` | int | Max results (default: 200, max: 1000) | +| `live` | boolean | Use live Datto API | + +**Response:** +```json +{ + "data": [ + { + "alert_uid": "a1b2c3...", + "alert_category": "Patch Management", + "alert_type": "Patch Not Installed", + "alert_message_en": "Critical patches pending", + "device_uid": "abc123-...", + "device_hostname": "WS-SMITH-01", + "device_os": "Windows 11 Pro", + "site_uid": "68431d2c-...", + "site_name": "Acme Corp", + "resolved": false, + "muted": false, + "ticket_number": "T20240315.0042", + "timestamp": "2026-03-20T14:22:00Z" + } + ], + "total": 847, + "source": "db" +} +``` + +**Key fields:** +- `alert_category` — Patch Management, Antivirus, Performance, Connectivity, etc. +- `ticket_number` — Autotask ticket number if one was auto-created +- `resolved` — false = currently active alert + +--- + +### 6. List Open Alerts (Shorthand) + +``` +GET /api/openclaw/datto-rmm/alerts/open +GET /api/openclaw/datto-rmm/alerts/open?siteUid=68431d2c-... +GET /api/openclaw/datto-rmm/alerts/open?live=true +``` + +Equivalent to `GET /alerts?resolved=false`. Same parameters and response shape as `/alerts` (excluding `resolved` filter since it's always `false`). + +--- + +## Common Usage Patterns + +### Get all offline devices across all sites +``` +GET /api/openclaw/datto-rmm/devices?online=false&limit=500 +``` + +### Get open alerts for a specific client +1. `GET /api/openclaw/datto-rmm/sites` → find the site UID for the client +2. `GET /api/openclaw/datto-rmm/alerts/open?siteUid={uid}` + +### Check patch status for all devices at a site +``` +GET /api/openclaw/datto-rmm/devices?siteUid={uid}&limit=500 +``` +Then filter `data` where `patch_status != "Fully Patched"`. + +### Deep-dive a specific device +1. `GET /api/openclaw/datto-rmm/devices/{uid}` — base info from DB +2. `GET /api/openclaw/datto-rmm/devices/{uid}/audit` — live hardware/software detail + +### Force fresh data (bypass DB cache) +``` +GET /api/openclaw/datto-rmm/devices?live=true&siteUid={uid} +``` + +--- + +## Linking to Autotask / Pulse DB + +| RMM field | Pulse DB join | +|---|---| +| `datto_rmm_sites.autotask_company_id` | `companies.id` | +| `datto_rmm_devices.site_id` | `datto_rmm_sites.id` | +| `datto_rmm_alerts.device_uid` | `datto_rmm_devices.uid` | +| `datto_rmm_devices.uid` | `configuration_items.reference_number` (sometimes) | +| `datto_rmm_alerts.ticket_number` | `tickets.ticket_number` | + +--- + +## Error Responses + +| HTTP | Meaning | +|---|---| +| 401 | Missing or invalid `x-openclaw-key` | +| 404 | Device/resource not found | +| 500 | Internal error (DB or Datto API) — check `error` field | + +--- + +## Notes + +- **Sync freshness:** DB data is updated by scheduled Datto RMM syncs (typically every hour). Check `synced_at` in site/device responses for last sync time. +- **Device UIDs:** Datto `uid` is a UUID string. Do not confuse with `id` (integer DB primary key). +- **UDFs:** `udf` is a JSONB field with up to 30 user-defined fields (`udf1`–`udf30`). Field meaning varies per client configuration. +- **Alert `site_uid`** is a text field matching `datto_rmm_sites.uid` (not the integer `id`). +- **Audit endpoint** always makes a live call to Datto RMM — expect 1–3s latency. diff --git a/PULSE_DATABASE_SKILL.md b/PULSE_DATABASE_SKILL.md new file mode 100644 index 0000000..dbf516d --- /dev/null +++ b/PULSE_DATABASE_SKILL.md @@ -0,0 +1,766 @@ +# Pulse Database Skill — Query Reference + +> **Purpose:** This document describes the PostgreSQL database behind **Pulse**, an MSP operations platform built by Wulf Consulting. Use it to query Autotask PSA data, RMM alerts, security agents, backup status, IT documentation, engagement metrics, and more. + +## Connection + +- **Host:** `pulse-postgres` (Docker) or `localhost:5432` +- **Database:** `pulse_autotask` +- **User:** `pulse_user` +- **Read-only queries only** — no INSERT/UPDATE/DELETE +- **Timezone:** PostgreSQL server runs in **UTC**. All `timestamp without time zone` columns are UTC. Convert to ET with `AT TIME ZONE 'America/New_York'`. + +--- + +## Data Domains at a Glance + +| Domain | Key Tables | Approx Rows | Description | +|---|---|---|---| +| **Autotask PSA** | tickets, time_entries, companies, contacts, resources, configuration_items, contracts, projects, tasks, ticket_notes | 152K tickets, 156K time entries, 7K CIs | Service desk, billing, contracts, clients | +| **Datto RMM** | datto_rmm_alerts, datto_rmm_devices, datto_rmm_sites | 21K alerts, 3.6K devices | Remote monitoring & management | +| **SentinelOne** | s1_agents, s1_threats, s1_sites | 2.8K agents, 4.1K threats | Endpoint security | +| **Veeam** | veeam_organizations, veeam_backup_jobs, veeam_backup_agents, veeam_alarms, veeam_protected_workloads, veeam_repositories, veeam_backup_servers | ~2.5K total | Backup & disaster recovery | +| **IT Glue** | itg_organizations, itg_configurations, itg_passwords, itg_flexible_assets, itg_contacts, itg_documents, itg_expirations, itg_domains, itg_locations | 14.7K configs | IT documentation | +| **Microsoft 365** | graph_users, teams_meetings, teams_meeting_attendees, engagement_snapshots | 14.4K meetings | Teams meetings, activity reports | +| **Zoom** | zoom_users, zoom_meetings, zoom_meeting_participants, zoom_calls | 2.8K calls | Zoom calls and meetings | +| **QuickBooks Online** | qbo_invoices, qbo_payments, qbo_deposits, qbo_transactions, qbo_reports | 6.9K invoices, 12.1K txns | Accounting — invoices, payments, deposits, financial reports | +| **Zabbix NMS** | zabbix_events, zabbix_wan_hosts | 231 events, 75 hosts | Network monitoring — WAN hosts, alert events | +| **Billing** | billing_items | 96K items | Invoice line items tied to tickets/projects/tasks | +| **Authentication** | "user", session, account, verification, two_factor | 3 users | Better Auth — Entra ID SSO, RBAC, sessions | + +--- + +## 1. Autotask PSA — Core Service Desk + +### tickets (~152K rows, 68 columns) + +> **Timezone note:** `create_date`, `due_date_time`, `completed_date`, `resolved_date_time`, `first_response_date_time`, `last_activity_date` are all `timestamp without time zone` stored in **UTC**. To display in ET: `create_date AT TIME ZONE 'America/New_York'`. + +The central table. Each row is a service ticket. + +**Key columns:** +- `id` (bigint PK) — Autotask ticket ID +- `title` (varchar) — ticket subject line +- `description` (text) — full body/description +- `status` (int) — FK to `statuses.value` +- `priority` (int) — FK to `priorities.value` +- `queue_id` (int) — FK to `queues.value` +- `source` (int) — how the ticket was created (see Source Codes below) +- `company_id` (bigint) — FK to `companies.id` +- `contact_id` (bigint) — FK to `contacts.id` +- `assigned_resource_id` (bigint) — FK to `resources.id` +- `configuration_item_id` (bigint) — FK to `configuration_items.id` +- `contract_id` (bigint) — FK to `contracts.id` +- `project_id` (bigint) — FK to `projects.id` +- `issue_type` (int), `sub_issue_type` (int) — classification +- `ticket_type` (int) — 1=Incident, 2=Service Request, 5=Alert +- `create_date` (timestamp) — when opened +- `due_date_time` (timestamp) — SLA due +- `completed_date` (timestamp) — when closed +- `resolved_date_time` (timestamp) — when resolved +- `first_response_date_time` (timestamp) — first response SLA timestamp +- `last_activity_date` (timestamp) — most recent update +- `monitor_id` (bigint) — RMM monitor that created this ticket (if source=8) +- `monitor_type_id` (int) — type of monitor +- `is_deleted` (boolean) — soft delete flag + +**Source codes (tickets.source):** +| Value | Meaning | +|---|---| +| 8 | Monitoring Alert (RMM/Datto) | +| 4 | Email | +| 21 | Portal | +| 2 | Phone/Voice | +| -1 | Insourced | +| -2 | Outsourced | +| 35 | Phish Alert | +| 6 | API | +| 17 | Internal Alert | + +**Ticket types (tickets.ticket_type):** +| Value | Meaning | +|---|---| +| 1 | Incident | +| 2 | Service Request | +| 5 | Alert | +| NULL | Unclassified | + +### companies (~246 rows, 41 columns) + +Client/customer organizations. + +**Key columns:** +- `id` (bigint PK) +- `company_name` (varchar) — display name +- `company_number` (varchar) — short code +- `is_active` (boolean) +- `company_type` (int) — 1=Customer, 2=Lead, 3=Prospect, 4=Dead, 6=Cancellation, 7=Vendor, etc. +- `owner_resource_id` (bigint) — account manager, FK to `resources.id` +- `classification` (varchar) — e.g. "Platinum", "Gold", etc. +- Address fields: `address1`, `city`, `state`, `postal_code` +- `last_activity_date` (timestamp) + +### contacts (~4.2K rows, 37 columns) + +People at client companies. + +**Key columns:** +- `id` (bigint PK) +- `first_name`, `last_name`, `email_address`, `phone` (varchar) +- `company_id` (bigint) — FK to `companies.id` +- `is_active` (boolean) +- `title` (varchar) — job title + +### resources (~40 columns) + +Internal staff / technicians. + +**Key columns:** +- `id` (bigint PK) +- `first_name`, `last_name`, `email` (varchar) +- `email_address` (varchar) — primary email +- `is_active` (boolean) +- `resource_type` (varchar) +- `default_service_desk_role_id` (bigint) +- `hire_date` (date) +- `location_id` (bigint) + +### time_entries (~154K rows, 45 columns) + +Work logged against tickets, tasks, or projects. + +**Key columns:** +- `id` (bigint PK) +- `resource_id` (bigint) — who did the work, FK to `resources.id` +- `ticket_id` (bigint) — FK to `tickets.id` (NULL if task/project entry) +- `task_id` (bigint) — FK to `tasks.id` +- `project_id` (bigint) — FK to `projects.id` +- `company_id` (bigint) — FK to `companies.id` +- `entry_date` (timestamp) — date of work +- `hours_worked` (numeric) — actual hours +- `hours_to_bill` (numeric) — billable hours +- `start_date_time`, `end_date_time` (timestamp) — clock in/out +- `title` (varchar), `notes` (text), `internal_notes` (text) +- `billable` (boolean), `non_billable` (boolean) +- `billing_rate` (numeric), `cost_rate` (numeric), `revenue` (numeric) +- `contract_id` (bigint), `contract_service_id` (bigint) +- `role_id` (bigint) +- `is_deleted` (boolean) + +### ticket_notes (~41K rows, 14 columns) + +Notes/comments on tickets. + +**Key columns:** +- `id` (bigint PK) +- `ticket_id` (bigint) — FK to `tickets.id` +- `title` (varchar), `description` (text) — note content +- `note_type` (int) — internal, external, etc. +- `publish` (int) — visibility +- `creator_resource_id` (bigint) — who wrote it +- `create_date_time` (timestamptz) + +### configuration_items (~7K rows, 95 columns) + +Devices/assets tracked in Autotask. + +**Key columns:** +- `id` (bigint PK) +- `reference_title` (varchar) — device name (e.g. "DT037", "SRV-DC01") +- `reference_number` (varchar) — often a GUID from RMM +- `serial_number` (varchar) +- `company_id` (bigint) — FK to `companies.id` +- `contact_id` (bigint) — FK to `contacts.id` +- `is_active` (boolean) +- `device_type` (varchar) + +**Note:** `reference_title` follows a naming convention per client (e.g. DT037 exists at multiple companies as separate CIs). Always filter by both `reference_title` AND `company_id` when searching. + +### contracts (~44 columns) + +Service agreements with clients. + +**Key columns:** +- `id` (bigint PK) +- `company_id` (bigint) — FK to `companies.id` +- `contract_name` (varchar), `contract_number` (varchar) +- `contract_type` (int), `status` (int) +- `start_date`, `end_date` (date) +- `estimated_hours` (numeric), `estimated_revenue` (numeric) + +### contract_services (~8.2K rows) + +Line items on contracts. + +- `contract_id` → `contracts.id` +- `company_id` → `companies.id` +- `service_name` (text), `unit_price`, `quantity` + +### projects (~291 rows, 36 columns) + +**Key columns:** +- `id`, `company_id`, `project_name`, `status`, `type` +- `project_lead_resource_id` → `resources.id` +- `start_date_time`, `end_date_time`, `actual_hours`, `estimated_time` + +### tasks (~4.3K rows, 34 columns) + +Tasks on tickets or projects. + +- `ticket_id` → `tickets.id` +- `project_id` → `projects.id` +- `assigned_resource_id` → `resources.id` +- `status`, `priority`, `estimated_hours`, `remaining_hours` + +### billing_items (~96K rows) + +Invoice line items linked to tickets, tasks, or projects. + +- `ticket_id` → `tickets.id`, `task_id` → `tasks.id`, `project_id` → `projects.id` +- `company_id` → `companies.id` +- `quantity`, `rate`, `total_amount`, `unit_cost`, `unit_price` + +--- + +## 2. Lookup / Picklist Tables + +These map integer codes to human-readable labels. Join on `value`. + +### statuses (ticket statuses) + +Join: `statuses.value = tickets.status` + +| Value | Label | +|---|---| +| 1 | New | +| 5 | Complete | +| 7 | Waiting Customer | +| 8 | In Progress | +| 9 | Waiting Materials | +| 10 | Dispatched | +| 11 | Escalate | +| 12 | Waiting Vendor | +| 13 | Waiting Approval | +| 14 | Resource Assigned | +| 15 | Ready to Deploy | +| 16 | Reopened | +| 17 | Info Req | +| 19 | End User Note Added | +| 20 | Equipment Pulled | +| 21 | Waiting Verification | +| 25 | On Hold | +| 27 | Resolution Plan | +| 30 | Service Call Scheduled | +| 37 | Client Non-Responsive | +| 45 | Pending Next Site Visit | +| 46 | Burn-in In Progress | +| 47 | Resource Requested | +| 48 | Escalate to MC | +| 51 | Reconcile Billing | +| 54 | Resolved \ | +| 55 | Huddle Review | +| 56 | Tracking Shipment | +| 57 | Escalate to Wulf | +| 58 | Pre-Pick Order | +| 59 | Need to Order/Fulfill | +| 60 | Loading Customer Config | +| 61 | Escalate to CSM | +| 62 | Escalate to Customer HD | +| 64 | Waiting Trivium Employee | +| 65 | Waiting Procurement | +| 66 | Internal Note Added | +| 67 | Quote Delivered | +| 68 | Ready for Customer Config | +| 69 | Outsourced | +| 70 | Waiting Quote Acceptance | +| 71 | Waiting Project/Ticket | + +### priorities + +Join: `priorities.value = tickets.priority` + +| Value | Label | +|---|---| +| 1 | Standard | +| 2 | Medium | +| 3 | Standard | +| 4 | Critical | +| 6 | High | +| 7 | Security Event | +| 8 | Minor Service | +| 9 | Major Service | +| 10 | Installation | +| 11 | Fast Track | + +### queues (46 active) + +Join: `queues.value = tickets.queue_id` + +| Value | Label | +|---|---| +| 5 | Client Triage | +| 6 | Post Sale | +| 8 | Monitoring Alert | +| 29682833 | Level 1 Support | +| 29682969 | Level 2 Support | +| 29703428 | Level 3 Support | +| 29749490 | Client Success | +| 29793481 | App-Care | +| 29807035 | Alert II | +| 29807036 | Alert I | +| 29832283 | Operations Triage | +| 29853695 | Follow Up | +| 29853697 | Trivium Packaging - Help Desk | +| 29853698 | Project Delivery | +| 29853699 | Mission Control | +| 29853700 | Deployment | +| 29853701 | IT Operations | +| 29853702 | Recurring Tickets | +| 29853706 | Premier Automation Help Desk | +| 29853710 | TTG Help Desk | +| 29853714 | Glunt Help Desk | +| 29853719 | TNT Pizza Help Desk | +| 29853720 | TTG Machines | +| 29853721 | TTG NetOps | +| 29853723 | TTG Finance | +| 29853724 | TTG Safety | +| 29853726 | TTG Remote | +| 29853727 | Purchasing | +| 29853728 | TTG New User Access | +| 29853729 | TTG Term User Access | +| 29853731 | Trivium Packaging - MES | +| 29853738 | TTG Security | +| 29853740 | TTG Change Mgmt | +| 29853741 | Subcontractor | +| 29853750 | PER Service Desk | +| 29853751 | PER Network Operations Center | +| 29853752 | PER Security Operations Center | +| 29853753 | LEC Service Desk | +| 29853754 | LEC Network Operations Center | +| 29853755 | LEC Security Operations Center | +| 29853756 | VCF Service Desk | +| 29853757 | VCF Network Operations | +| 29853758 | VCF Security Operations Center | +| 29853766 | Sales | +| 29780304 | TaskFire | +| 29753333 | Waiting Verification | + +### issue_types + +Join: `issue_types.value = tickets.issue_type` + +| Value | Label | +|---|---| +| 4 | Upgrade | +| 6 | New Install | +| 7 | Monitoring Alert | +| 10 | Break/Fix | +| 11 | Maintenance | +| 12 | Help Desk | +| 13 | Email | +| 14 | Vendor | +| 15 | User Education | +| 16 | Provision | +| 18 | Purchase | +| 19 | Deploy | +| 20 | Reactive | +| 21 | MAC | +| 22 | Centralized Services | +| 23 | NOC Services | +| 25 | Networking | +| 27 | WiFi | +| 28 | Client Success | +| 29 | Automation | +| 30 | Server | +| 31 | Hardware | +| 32 | LOB Software | +| 33 | Workstation | +| 36 | 24-Hour Emergency Support | + +--- + +## 3. Datto RMM + +### datto_rmm_alerts (~20.7K rows, 61 columns) + +- `id` (int PK), `uid` (text) — alert identifiers +- `alert_category`, `alert_type`, `alert_message_en` — what triggered +- `priority` (text) — Critical, High, Moderate, Low, Information +- `resolved` (boolean), `resolved_on` (timestamptz) +- `muted` (boolean) +- `ticket_number` (text) — linked Autotask ticket +- `device_hostname`, `device_ip`, `device_os`, `device_id` +- `site_id` (text) — FK to `datto_rmm_sites` +- `timestamp` (timestamptz) — when alert fired + +### datto_rmm_devices (~3.6K rows, 42 columns) + +- `id` (int PK), `uid` (text), `hostname` +- `device_type_category` (text) — Server, Desktop, Laptop, Network Device +- `operating_system`, `domain`, `int_ip_address`, `ext_ip_address` +- `online` (boolean), `last_seen` (timestamptz) +- `last_logged_in_user` (text) +- `antivirus_product`, `antivirus_status`, `patch_status` +- `site_id` (int) — FK to `datto_rmm_sites.id` +- `udf` (jsonb) — custom fields + +### datto_rmm_sites (~16 columns) + +- `id` (int PK), `uid`, `name` +- `autotask_company_id` (int) — **FK to `companies.id`** (links RMM sites to Autotask clients) +- `autotask_company_name` +- `number_of_devices`, `number_of_online_devices` + +**Join pattern:** `datto_rmm_sites.autotask_company_id = companies.id` + +--- + +## 4. SentinelOne + +### s1_agents (~2.8K rows, 42 columns) + +Endpoint security agents. + +- `id` (varchar PK) — S1 agent ID +- `computer_name`, `os_name`, `os_type` +- `site_id` → `s1_sites.id`, `site_name` +- `is_active`, `is_decommissioned` +- `infected` (boolean), `active_threats` (int) +- `network_status`, `mitigation_mode`, `detection_state` +- `external_ip`, `last_active_date`, `last_logged_in_user_name` +- `firewall_enabled` (boolean) + +### s1_threats (~4.1K rows, 25 columns) + +Detected threats. + +- `id` (varchar PK) +- `threat_name`, `classification`, `confidence_level` +- `mitigation_status`, `analyst_verdict`, `incident_status` +- `agent_id` → `s1_agents.id` +- `agent_computer_name`, `agent_os_name` +- `site_id` → `s1_sites.id` + +### s1_sites (~22 columns) + +- `id` (varchar PK), `name`, `account_name` +- `health_status`, `active_licenses`, `total_licenses` + +### s1_company_mappings + +Maps S1 sites to Autotask companies for cross-referencing. + +--- + +## 5. Veeam Backup + +### veeam_organizations (~17 columns) + +- `instance_uid` (PK), `name`, `company_id` +- All other Veeam tables FK to `veeam_organizations.instance_uid` + +### veeam_backup_jobs (~234 rows) + +- `instance_uid`, `name`, `type`, `status`, `last_run`, `next_run` +- `organization_uid` → `veeam_organizations` +- `backup_server_uid` → `veeam_backup_servers` + +### veeam_backup_agents (~727 rows) + +- Backup agents installed on endpoints +- `organization_uid` → `veeam_organizations` + +### veeam_alarms (~581 rows) + +- Active alarms/alerts +- `organization_uid` → `veeam_organizations` + +### veeam_protected_workloads, veeam_repositories, veeam_backup_servers + +Supporting tables for backup infrastructure. + +--- + +## 6. IT Glue Documentation + +### itg_organizations (~330 rows) + +- `id` (bigint PK), `name`, `short_name`, `organization_type_name` +- `psa_id` (varchar) — Autotask company ID (string). Join: `itg_organizations.psa_id::bigint = companies.id` + +### itg_configurations (~14.7K rows) + +Hardware/software assets documented in IT Glue. + +- `id`, `organization_id` → `itg_organizations.id` +- `name`, `hostname`, `serial_number`, `asset_tag` +- `configuration_type_name`, `configuration_status_name` +- `primary_ip`, `mac_address`, `operating_system` +- `warranty_expires_at`, `installed_at` + +### itg_passwords (~17 columns) + +- `id`, `organization_id`, `name`, `username`, `password_category_name` +- `url`, `notes` + +### itg_flexible_assets (~3.2K rows) + +Custom documentation (e.g. Backup configs, Email configs, LAN/VLAN, Voice/PBX). + +- `id`, `organization_id`, `flexible_asset_type_id`, `flexible_asset_type_name` +- `traits` (jsonb) — all custom field values + +### itg_contacts, itg_documents, itg_expirations, itg_domains, itg_locations + +Supporting IT documentation tables. + +--- + +## 7. Engagement & Communications + +### graph_users + +Microsoft 365 users synced from Azure AD. + +- `id` (varchar PK), `display_name`, `email`, `job_title`, `department` +- `account_enabled` (boolean) + +### teams_meetings (~14.3K rows) + +Teams calendar events / meetings. + +- `id` (int PK), `user_email`, `subject` +- `start_time`, `end_time` (timestamptz), `duration_minutes` +- `attendee_count`, `client_attendee_count`, `has_client_attendees` (boolean) + +### teams_meeting_attendees (~12.6K rows) + +- `meeting_id` → `teams_meetings.id` +- `attendee_email`, `attendee_name` +- `matched_contact_id` → `contacts.id` +- `matched_company_id` → `companies.id` + +### engagement_snapshots (~1K rows) + +Weekly/monthly aggregates of M365 activity per user. + +- `user_email`, `period_type` (D7, D30, D90, D180) +- `teams_chat_messages`, `teams_calls`, `teams_meetings_attended`, `teams_meetings_organized` +- `emails_sent`, `emails_received`, `emails_read` + +### zoom_meetings, zoom_meeting_participants + +- `host_email`, `topic`, `start_time`, `end_time`, `duration_minutes` +- Participants with `matched_contact_id` → `contacts.id`, `matched_company_id` → `companies.id` +- `is_internal` (boolean) — internal vs external attendee + +### zoom_calls (~2.8K rows) + +- `resource_email`, `direction` (inbound/outbound), `call_status` +- `other_party_number`, `other_party_name` +- `matched_contact_id`, `matched_company_id` + +--- + +## 8. QuickBooks Online (Accounting) + +### qbo_invoices (~6.9K rows) + +- `id` (text PK) — QBO invoice ID +- `doc_number` (text) — invoice number (e.g. "1042") +- `txn_date` (date), `due_date` (date) +- `customer_ref_id`, `customer_ref_name` — QBO customer +- `total_amt` (numeric), `balance` (numeric) — amounts +- `status` (text) — Paid, Overdue, etc. +- `line_items` (jsonb) — invoice line detail +- `linked_txns` (jsonb) — linked payments + +### qbo_payments (~4.5K rows) + +- `id` (text PK) +- `txn_date` (date), `total_amt` (numeric) +- `customer_ref_id`, `customer_ref_name` +- `payment_method_ref` (text), `deposit_account_ref` (text) +- `unapplied_amt` (numeric) +- `linked_txns` (jsonb) — linked invoices + +### qbo_deposits (~1.8K rows) + +- `id` (text PK) +- `txn_date` (date), `total_amt` (numeric) +- `deposit_to_account_ref_id`, `deposit_to_account_ref_name` +- `line_items` (jsonb) + +### qbo_transactions (~12.1K rows) + +General ledger transactions (expenses, bills, journal entries, etc.). + +- `id` (text), `txn_type` (text) — composite PK +- `txn_date` (date), `doc_number` (text) +- `entity_ref_id`, `entity_ref_name`, `entity_type` — vendor/customer +- `account_ref_id`, `account_ref_name` — GL account +- `total_amt` (numeric) +- `line_items` (jsonb) + +**Transaction types:** Bill, BillPayment, Expense, JournalEntry, Transfer, VendorCredit, CreditMemo, SalesReceipt, Estimate, PurchaseOrder, etc. + +### qbo_reports (~36 rows) + +Periodic financial reports stored as JSON. + +- `id` (serial PK) +- `report_type` (text) — ProfitAndLoss, BalanceSheet, CashFlow +- `period_start` (date), `period_end` (date) +- `report_data` (jsonb) — full QBO report payload +- Unique on `(realm_id, report_type, period_start, period_end)` + +--- + +## 9. Zabbix NMS + +### zabbix_wan_hosts (~75 rows) + +WAN monitoring hosts auto-synced from RMM site public IPs. + +- `host_id` (text) — Zabbix host ID +- `host` (text) — hostname in Zabbix +- `name` (text) — display name +- `ip` (text) — WAN IP address +- `status` (int) — 0=enabled, 1=disabled +- `company_id` (bigint) — FK to `companies.id` +- `rmm_site_id` (int) — FK to `datto_rmm_sites.id` + +### zabbix_events (~228 rows) + +Zabbix alert events. + +- `event_id` (text PK) +- `host_id` (text), `host_name` (text) +- `trigger_id` (text), `trigger_name` (text) +- `severity` (int) — 0=Not classified, 1=Info, 2=Warning, 3=Average, 4=High, 5=Disaster +- `value` (int) — 0=OK, 1=Problem +- `clock` (timestamptz) — event time + +--- + +## 10. Authentication (Better Auth + Entra ID) + +### "user" table (~3 rows) + +**Note:** Table name is `"user"` (quoted) — must be quoted in SQL. + +- `id` (text PK) +- `name` (text), `email` (text UNIQUE) +- `"emailVerified"` (boolean), `image` (text) +- `role` (text) — `super-admin`, `admin`, `user` +- `banned` (boolean), `"bannedReason"` (text), `"banExpires"` (timestamp) +- `requires_setup` (boolean), `"twoFactorEnabled"` (boolean) +- `"createdAt"`, `"updatedAt"` (timestamp) + +**Note:** Better Auth uses **camelCase** column names — must be double-quoted in raw SQL. + +### session + +- `id` (text PK), `"userId"` → `"user".id` +- `token` (text UNIQUE), `"expiresAt"` (timestamp) +- `"ipAddress"`, `"userAgent"` (text) + +### account + +OAuth provider links (Microsoft Entra ID). + +- `id` (text PK), `"userId"` → `"user".id` +- `"providerId"` (text) — e.g. `microsoft` +- `"accountId"` (text) — provider-specific user ID +- `"accessToken"`, `"refreshToken"`, `"idToken"` (text) + +--- + +## 11. Common Join Patterns + +```sql +-- Ticket with company, resource, and status label +SELECT t.id, t.title, c.company_name, + r.first_name || ' ' || r.last_name AS technician, + s.label AS status_label, p.label AS priority_label +FROM tickets t +LEFT JOIN companies c ON c.id = t.company_id +LEFT JOIN resources r ON r.id = t.assigned_resource_id +LEFT JOIN statuses s ON s.value = t.status +LEFT JOIN priorities p ON p.value = t.priority +WHERE t.is_deleted IS NOT TRUE; + +-- Time entries for a ticket +SELECT te.entry_date, te.hours_worked, te.notes, + r.first_name || ' ' || r.last_name AS technician +FROM time_entries te +JOIN resources r ON r.id = te.resource_id +WHERE te.ticket_id = $1 AND te.is_deleted IS NOT TRUE; + +-- RMM device → Autotask company +SELECT d.hostname, d.device_type_category, d.operating_system, + s.autotask_company_name, d.online, d.last_seen +FROM datto_rmm_devices d +JOIN datto_rmm_sites s ON s.id = d.site_id; + +-- Config item lookup (always filter by company too) +SELECT ci.id, ci.reference_title, ci.serial_number, c.company_name +FROM configuration_items ci +JOIN companies c ON c.id = ci.company_id +WHERE ci.reference_title = 'DT037' AND ci.company_id = $1; + +-- IT Glue org → Autotask company +SELECT ig.name, ig.id AS itg_org_id, c.id AS autotask_company_id, c.company_name +FROM itg_organizations ig +JOIN companies c ON ig.psa_id::bigint = c.id; + +-- Meetings with client attendees +SELECT tm.subject, tm.start_time, tm.duration_minutes, + tma.attendee_name, c.company_name +FROM teams_meetings tm +JOIN teams_meeting_attendees tma ON tma.meeting_id = tm.id +LEFT JOIN companies c ON c.id = tma.matched_company_id +WHERE tm.has_client_attendees = true; + +-- QBO: Revenue by month (from invoices) +SELECT DATE_TRUNC('month', txn_date) AS month, + SUM(total_amt) AS total_invoiced, COUNT(*) AS invoice_count +FROM qbo_invoices +GROUP BY 1 ORDER BY 1 DESC; + +-- QBO: Outstanding balances by customer +SELECT customer_ref_name, SUM(balance) AS outstanding +FROM qbo_invoices WHERE balance > 0 +GROUP BY 1 ORDER BY 2 DESC; + +-- Zabbix: Active WAN problems with company +SELECT ze.host_name, ze.trigger_name, ze.severity, ze.clock, + c.company_name +FROM zabbix_events ze +JOIN zabbix_wan_hosts zwh ON zwh.host_id = ze.host_id +LEFT JOIN companies c ON c.id = zwh.company_id +WHERE ze.value = 1 ORDER BY ze.clock DESC; +``` + +--- + +## 12. Important Notes + +1. **Soft deletes:** Most Autotask tables have `is_deleted` (boolean) and `deleted_at`. Always add `WHERE is_deleted IS NOT TRUE` unless you want deleted records. + +2. **Picklist joins:** `status`, `priority`, `queue_id`, `source` on tickets are integer codes. Join to `statuses`, `priorities`, `queues` on `.value` for labels. + +3. **Configuration item names are NOT unique globally.** Names like "DT037" are a per-client naming convention. Always pair with `company_id`. + +4. **Timestamps — all UTC:** The PostgreSQL server timezone is UTC. All `timestamp without time zone` columns (Autotask) are stored in UTC. `timestamp with time zone` columns (Teams, Zoom, QBO) are also UTC-normalized. To display in Eastern Time: `column AT TIME ZONE 'America/New_York'`. Example: `create_date AT TIME ZONE 'America/New_York'`. + +5. **Monitor tickets:** `tickets.source = 8` indicates RMM-generated tickets. `monitor_id` links to the specific Datto RMM monitor. These represent ~74% of all tickets. + +6. **Cross-platform linking:** + - RMM → Autotask: `datto_rmm_sites.autotask_company_id = companies.id` + - IT Glue → Autotask: `itg_organizations.psa_id::bigint = companies.id` + - S1 → Autotask: via `s1_company_mappings` + - Zoom/Teams → Contacts: `matched_contact_id` / `matched_company_id` columns + - Config Items → RMM: `configuration_items.reference_number` sometimes matches RMM device UIDs + +7. **Row counts** (as of March 17, 2026): tickets 152K, time_entries 156K, billing_items 96K, ticket_notes 42K, teams_meetings 14.4K, itg_configurations 14.7K, configuration_items 7K, companies 246, contacts 4.2K, datto_rmm_devices 3.6K, datto_rmm_alerts 21K, s1_agents 2.8K, s1_threats 4.1K, zoom_calls 2.8K, qbo_invoices 6.9K, qbo_transactions 12.1K, qbo_payments 4.5K, qbo_deposits 1.8K, qbo_reports 36, zabbix_wan_hosts 75, zabbix_events 231.