# IT Glue Data Sync

Full backup of all IT Glue data into local PostgreSQL tables prefixed `itg_`.

## Overview

The IT Glue sync pulls all data from the IT Glue REST API and upserts it into Postgres. This gives Pulse a local, queryable copy of all IT documentation for use in pipelines, AI context, reporting, and cross-referencing with RMM/PSA data.

- **40,000+ records** synced across 23 entity types
- Full sync takes ~7–10 minutes
- All tables use `ON CONFLICT DO UPDATE` — fully idempotent
- Sync history tracked in `itg_sync_history`

---

## Environment Variables

| Variable | Description |
|---|---|
| `ITGLUE_API_KEY` | IT Glue API key (from IT Glue → Account → API Keys) |

Set in `/opt/stacks/pulse/.env` and `docker-compose.yml` under the `app` service environment.

---

## API Details

- **Base URL:** `https://api.itglue.com`
- **Auth:** `x-api-key: <key>` header
- **Content-Type:** `application/vnd.api+json` (JSON:API format)
- **Pagination:** `page[size]` + `page[number]`, `meta.total-pages` for total
- **Attribute keys:** hyphenated (`organization-type-id`, `created-at`, etc.)

---

## Files

| File | Purpose |
|---|---|
| `lib/services/itglue-client.ts` | IT Glue API client — typed methods + `getRaw`/`getRawAllPages` for sync |
| `lib/services/itglue-sync-service.ts` | Full sync service — iterates all entities, upserts to Postgres |
| `app/api/itglue/status/route.ts` | `GET /api/itglue/status` — connection test |
| `app/api/itglue/sync/route.ts` | `POST /api/itglue/sync` — trigger sync; `GET` — status + history + counts |
| `app/admin/sync/itglue/page.tsx` | Admin UI page for IT Glue sync |
| `migrations/037_create_itglue_tables.sql` | Creates all `itg_*` tables |

---

## Database Tables

### Reference / Lookup Tables
| Table | Description |
|---|---|
| `itg_organization_types` | Org type definitions |
| `itg_organization_statuses` | Org status definitions |
| `itg_configuration_types` | Config type definitions (Server, Workstation, etc.) |
| `itg_configuration_statuses` | Config status definitions (Active, Inactive, etc.) |
| `itg_contact_types` | Contact type definitions |
| `itg_password_categories` | Password category definitions |
| `itg_manufacturers` | Hardware manufacturers |
| `itg_models` | Hardware models (linked to manufacturer) |
| `itg_operating_systems` | OS definitions |
| `itg_platforms` | Platform definitions |
| `itg_countries` | Country list with ISO codes |

### Core Tables
| Table | Key Columns | Notes |
|---|---|---|
| `itg_organizations` | `id`, `name`, `short_name`, `organization_type_id/name`, `organization_status_id/name`, `psa_integration`, `psa_id`, `parent_id` | 330 orgs |
| `itg_locations` | `id`, `organization_id`, `name`, `primary_location`, `address_*`, `city`, `region_name`, `postal_code`, `country_name`, `phone` | 751 locations |
| `itg_contacts` | `id`, `organization_id`, `first_name`, `last_name`, `title`, `contact_type_id/name`, `location_id`, `emails` (JSONB), `phones` (JSONB) | 7,113 contacts |
| `itg_configurations` | `id`, `organization_id`, `name`, `hostname`, `primary_ip`, `mac_address`, `serial_number`, `asset_tag`, `configuration_type_id/name`, `configuration_status_id/name`, `manufacturer_id/name`, `model_id/name`, `operating_system_id/name`, `rmm_id`, `rmm_integration_type` | 14,712 configs |
| `itg_flexible_asset_types` | `id`, `name`, `description`, `icon`, `enabled`, `builtin` | 41 types |
| `itg_flexible_asset_fields` | `id`, `flexible_asset_type_id`, `name`, `kind`, `required`, `use_for_title` | 1,140 fields |
| `itg_flexible_assets` | `id`, `organization_id`, `flexible_asset_type_id/name`, `name`, `traits` (JSONB), `archived` | 3,161 assets |
| `itg_password_folders` | `id`, `organization_id`, `name`, `inherited` | Per-org |
| `itg_passwords` | `id`, `organization_id`, `name`, `username`, `password`, `url`, `password_category_id/name`, `password_folder_id`, `otp_enabled`, `archived` | 270 passwords |
| `itg_documents` | `id`, `organization_id`, `name`, `content`, `draft`, `archived` | 537 documents |
| `itg_domains` | `id`, `organization_id`, `name`, `expires_at`, `registrar_name`, `whois_updated_at` | 212 domains |
| `itg_expirations` | `id`, `organization_id`, `resource_id`, `resource_type`, `resource_name`, `expiration_type`, `expiration_date`, `notify` | 9,770 expirations |
| `itg_sync_history` | `id`, `sync_type`, `status`, `triggered_by`, `started_at`, `completed_at`, `duration_ms`, `entities` (JSONB), `total_upserted` | Sync audit log |

All tables include `synced_at TIMESTAMPTZ` updated on every upsert.

---

## API Quirks & Workarounds

### Flexible Assets — require per-type filter
The `/flexible_assets` endpoint **requires** `filter[flexible-asset-type-id]`. Without it, the API returns 422. The sync iterates over all 41 flexible asset types and fetches assets per type.

### Password Folders, Documents, Expirations — no flat endpoint
These endpoints only exist as nested routes:
- `/organizations/:id/relationships/password_folders`
- `/organizations/:id/relationships/documents`
- `/organizations/:id/relationships/expirations`

The sync iterates over all 330 organizations for each of these.

### Configuration Interfaces — skipped
`/configuration_interfaces` has no flat endpoint. Per-config calls across 14,712 configurations would require 14,712+ API requests and take hours. This entity is intentionally excluded from the sync.

---

## Triggering a Sync

### Via Admin UI
Navigate to **Admin → IT Glue Sync** (or **Admin → Integrations & Sync → IT Glue**) and click **Full Sync**.

### Via API
```bash
# Trigger sync
curl -X POST http://localhost:3100/api/itglue/sync \
  -H "Content-Type: application/json" \
  -d '{"triggeredBy": "manual"}'

# Check status / history
curl http://localhost:3100/api/itglue/sync

# Test connection
curl http://localhost:3100/api/itglue/status
```

---

## Querying the Data

```sql
-- All organizations
SELECT id, name, organization_type_name, organization_status_name FROM itg_organizations;

-- Configurations for a specific org
SELECT name, hostname, primary_ip, configuration_type_name, configuration_status_name
FROM itg_configurations
WHERE organization_name ILIKE '%acme%';

-- Flexible assets by type
SELECT o.name AS org, fa.name, fa.traits
FROM itg_flexible_assets fa
JOIN itg_organizations o ON o.id = fa.organization_id
WHERE fa.flexible_asset_type_name = 'Servers';

-- Expiring domains (next 90 days)
SELECT organization_name, name, expires_at, registrar_name
FROM itg_domains
WHERE expires_at BETWEEN NOW() AND NOW() + INTERVAL '90 days'
ORDER BY expires_at;

-- Upcoming expirations
SELECT organization_name, resource_name, resource_type, expiration_type, expiration_date
FROM itg_expirations
WHERE expiration_date > NOW()
ORDER BY expiration_date
LIMIT 50;

-- Last sync summary
SELECT status, total_upserted, duration_ms,
       started_at, completed_at
FROM itg_sync_history
ORDER BY started_at DESC
LIMIT 5;
```

---

## Sync Performance (Initial Run)

| Entity | Records | Notes |
|---|---|---|
| organization_types | 10 | |
| organization_statuses | 2 | |
| configuration_types | 51 | |
| configuration_statuses | 2 | |
| contact_types | 9 | |
| password_categories | 11 | |
| manufacturers | 183 | |
| operating_systems | 365 | |
| platforms | 22 | |
| countries | 243 | |
| models | 1,803 | Per-manufacturer iteration |
| flexible_asset_types | 41 | |
| flexible_asset_fields | 1,140 | Per-type iteration |
| organizations | 330 | |
| locations | 751 | |
| contacts | 7,113 | |
| configurations | 14,712 | Largest entity |
| flexible_assets | 3,161 | Per-type iteration (41 API calls) |
| password_folders | 1 | Per-org iteration |
| passwords | 270 | |
| documents | 537 | Per-org iteration |
| domains | 212 | |
| expirations | 9,770 | Per-org iteration |
| **Total** | **40,739** | ~7.7 minutes |
