Rebrand: rename ondeck->horizon in package.json, docker-compose, SERVER.md, docs; production stack at /opt/stacks/horizon

This commit is contained in:
lorentz 2026-04-12 23:59:20 +00:00
parent 77f23e12cd
commit bbdbe1ddc0
8 changed files with 464 additions and 46 deletions

View file

@ -29,7 +29,12 @@
"Read(//usr/lib/**)", "Read(//usr/lib/**)",
"Bash(ls /usr/bin/pg*)", "Bash(ls /usr/bin/pg*)",
"Bash(dpkg -l)", "Bash(dpkg -l)",
"Bash(docker exec dev-db:*)" "Bash(docker exec dev-db:*)",
"Skill(pangolin)",
"Bash(curl -s 'https://api.pangolin.wulfconsulting.cloud/v1/org/seubert-and-associates/domains?limit=1000' -H 'Authorization: Bearer uqfgsbiou7j9fvt.fcznfl7wtfi7ap3267asgqxgsjzwyzej5tdiun62')",
"Bash(curl -s https://api.pangolin.wulfconsulting.cloud/v1/target/48 -H 'Authorization: Bearer uqfgsbiou7j9fvt.fcznfl7wtfi7ap3267asgqxgsjzwyzej5tdiun62')",
"Bash(curl -s https://api.pangolin.wulfconsulting.cloud/v1/resource/42/rules -H 'Authorization: Bearer uqfgsbiou7j9fvt.fcznfl7wtfi7ap3267asgqxgsjzwyzej5tdiun62')",
"Bash(curl -s -X PUT https://api.pangolin.wulfconsulting.cloud/v1/resource/42/rule -H 'Authorization: Bearer uqfgsbiou7j9fvt.fcznfl7wtfi7ap3267asgqxgsjzwyzej5tdiun62' -H 'Content-Type: application/json' -d '{:*)"
] ]
} }
} }

View file

@ -0,0 +1,387 @@
---
name: pangolin
description: Manage Pangolin reverse proxy — sites, resources (HTTP/TCP services), targets (backends), domains, roles, users, clients, and access control via the Pangolin Integration API. Use when the user asks about their Pangolin tunnels, reverse proxy resources, exposed services, or site connectivity.
argument-hint: [action] [resource] [options]
allowed-tools: Bash(curl *) Read
---
# Pangolin API Skill
Manage a self-hosted Pangolin tunneled reverse proxy via the Integration API.
## Authentication
The API key is stored in `PANGOLIN_API_KEY` and base URL in `PANGOLIN_URL`.
```
Authorization: Bearer $PANGOLIN_API_KEY
```
The key format is `{apiKeyId}.{apiKeySecret}`.
## Base URL
```
https://api.$PANGOLIN_URL/v1
```
The Integration API lives at `api.` prefixed to the Pangolin domain, path prefix `/v1`.
## Standard Headers
```bash
-H "Authorization: Bearer $PANGOLIN_API_KEY" \
-H "Content-Type: application/json"
```
## How to Make Requests
```bash
curl -s "https://api.$PANGOLIN_URL/v1/org/$PANGOLIN_ORG/sites" \
-H "Authorization: Bearer $PANGOLIN_API_KEY" \
-H "Content-Type: application/json" | jq .
```
## Organization
The org ID for this instance is: **`wulf-consulting`**
Always use `PANGOLIN_ORG="wulf-consulting"` in requests.
## Response Format
```json
{
"data": {
"resources": [ ... ],
"pagination": {
"total": 42,
"limit": 25,
"offset": 0
}
},
"success": true,
"error": false,
"message": "Human-readable message",
"status": 200
}
```
Always check the `success` field.
## Pagination
List endpoints return a `pagination` object inside `data` with `total`, `limit`, and `offset` fields. The default limit is typically 25 or 1000 depending on the endpoint.
To paginate, add `?limit=N&offset=N` query parameters:
```bash
# First page
curl -s "$BASE/org/$ORG/resources?limit=25&offset=0" ...
# Second page
curl -s "$BASE/org/$ORG/resources?limit=25&offset=25" ...
```
**Always check `pagination.total` against the number of items returned.** If `total` exceeds the current page size, fetch additional pages until all items are retrieved. Example pattern:
```bash
# Fetch all pages of a resource list
OFFSET=0
LIMIT=100
while true; do
RESP=$(curl -s "$BASE/org/$ORG/resources?limit=$LIMIT&offset=$OFFSET" \
-H "Authorization: Bearer $PANGOLIN_API_KEY")
# Process items from this page...
TOTAL=$(echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['pagination']['total'])")
OFFSET=$((OFFSET + LIMIT))
if [ $OFFSET -ge $TOTAL ]; then break; fi
done
```
When listing resources, sites, targets, users, etc., always use `?limit=1000` to minimize round trips, and paginate if `total` exceeds the returned count.
## Important: HTTP Method Conventions
Pangolin uses non-standard HTTP method semantics:
- **PUT** = Create a new resource
- **POST** = Update an existing resource
- **DELETE** = Delete
- **GET** = Read/list
This is the opposite of typical REST conventions. Be careful!
## API Endpoints Reference
### Organizations
| Action | Method | Endpoint |
|--------|--------|----------|
| Check org ID | GET | `/org/checkId` |
| Create org | PUT | `/org` |
| List orgs | GET | `/orgs` (root key only) |
| Get org | GET | `/org/:orgId` |
| Update org | POST | `/org/:orgId` |
| Delete org | DELETE | `/org/:orgId` |
### Sites (Tunnel Endpoints)
| Action | Method | Endpoint |
|--------|--------|----------|
| List sites | GET | `/org/:orgId/sites` |
| Create site | PUT | `/org/:orgId/site` |
| Get site defaults | GET | `/org/:orgId/pick-site-defaults` |
| Get site by niceId | GET | `/org/:orgId/site/:niceId` |
| Get site by ID | GET | `/site/:siteId` |
| Update site | POST | `/site/:siteId` |
| Delete site | DELETE | `/site/:siteId` |
Site types: `"newt"` (tunnel client), `"wireguard"`, `"local"`
### Resources (Public HTTP/TCP Services)
| Action | Method | Endpoint |
|--------|--------|----------|
| List resources | GET | `/org/:orgId/resources` |
| List resource names | GET | `/org/:orgId/resource-names` |
| Create resource | PUT | `/org/:orgId/resource` |
| Create for site | PUT | `/org/:orgId/site/:siteId/resource` |
| Get resource | GET | `/resource/:resourceId` |
| Get by niceId | GET | `/org/:orgId/resource/:niceId` |
| Update resource | POST | `/resource/:resourceId` |
| Delete resource | DELETE | `/resource/:resourceId` |
| List site resources | GET | `/site/:siteId/resources` |
Create HTTP resource body:
```json
{
"name": "My App",
"subdomain": "app",
"http": true,
"protocol": "tcp",
"domainId": "domain-id",
"stickySession": false
}
```
Update resource fields: `name`, `subdomain`, `ssl`, `sso`, `blockAccess`, `enabled`, `emailWhitelistEnabled`, `domainId`, `stickySession`, `tlsServerName`, `setHostHeader`
### Targets (Backends/Upstreams)
| Action | Method | Endpoint |
|--------|--------|----------|
| List targets | GET | `/resource/:resourceId/targets` |
| Create target | PUT | `/resource/:resourceId/target` |
| Get target | GET | `/target/:targetId` |
| Update target | POST | `/target/:targetId` |
| Delete target | DELETE | `/target/:targetId` |
Target body:
```json
{
"siteId": 1,
"ip": "192.168.1.100",
"port": 8080,
"method": "round-robin",
"enabled": true
}
```
Health check fields: `hcEnabled`, `hcPath`, `hcScheme`, `hcInterval` (min 5s), `hcTimeout` (min 1s), `hcStatus`
### Resource Rules
| Action | Method | Endpoint |
|--------|--------|----------|
| List rules | GET | `/resource/:resourceId/rules` |
| Create rule | PUT | `/resource/:resourceId/rule` |
| Update rule | POST | `/resource/:resourceId/rule/:ruleId` |
| Delete rule | DELETE | `/resource/:resourceId/rule/:ruleId` |
### Resource Access Control
| Action | Method | Endpoint |
|--------|--------|----------|
| List roles | GET | `/resource/:resourceId/roles` |
| List users | GET | `/resource/:resourceId/users` |
| Set roles | POST | `/resource/:resourceId/roles` |
| Set users | POST | `/resource/:resourceId/users` |
| Add role | POST | `/resource/:resourceId/roles/add` |
| Remove role | POST | `/resource/:resourceId/roles/remove` |
| Add user | POST | `/resource/:resourceId/users/add` |
| Remove user | POST | `/resource/:resourceId/users/remove` |
| Set password auth | POST | `/resource/:resourceId/password` |
| Set pincode auth | POST | `/resource/:resourceId/pincode` |
| Set header auth | POST | `/resource/:resourceId/header-auth` |
| Set email whitelist | POST | `/resource/:resourceId/whitelist` |
| Get email whitelist | GET | `/resource/:resourceId/whitelist` |
| Add to whitelist | POST | `/resource/:resourceId/whitelist/add` |
| Remove from whitelist | POST | `/resource/:resourceId/whitelist/remove` |
### Access Tokens
| Action | Method | Endpoint |
|--------|--------|----------|
| Generate token | POST | `/resource/:resourceId/access-token` |
| Delete token | DELETE | `/access-token/:accessTokenId` |
| List org tokens | GET | `/org/:orgId/access-tokens` |
| List resource tokens | GET | `/resource/:resourceId/access-tokens` |
### Domains
| Action | Method | Endpoint |
|--------|--------|----------|
| List domains | GET | `/org/:orgId/domains` |
| Create domain | PUT | `/org/:orgId/domain` |
| Get domain | GET | `/org/:orgId/domain/:domainId` |
| Update domain | PATCH | `/org/:orgId/domain/:domainId` |
| Delete domain | DELETE | `/org/:orgId/domain/:domainId` |
| Get DNS records | GET | `/org/:orgId/domain/:domainId/dns-records` |
| Restart cert | POST | `/org/:orgId/domain/:domainId/restart` |
Domain types: `"ns"`, `"cname"`, `"wildcard"`
### Users
| Action | Method | Endpoint |
|--------|--------|----------|
| Create user | PUT | `/org/:orgId/user` |
| List users | GET | `/org/:orgId/users` |
| Get user | GET | `/org/:orgId/user/:userId` |
| Get by username | GET | `/org/:orgId/user-by-username` |
| Update user | POST | `/org/:orgId/user/:userId` |
| Remove user | DELETE | `/org/:orgId/user/:userId` |
### Roles
| Action | Method | Endpoint |
|--------|--------|----------|
| List roles | GET | `/org/:orgId/roles` |
| Create role | PUT | `/org/:orgId/role` |
| Get role | GET | `/role/:roleId` |
| Update role | POST | `/role/:roleId` |
| Delete role | DELETE | `/role/:roleId` |
### Clients (VPN / Olm Devices)
| Action | Method | Endpoint |
|--------|--------|----------|
| List clients | GET | `/org/:orgId/clients` |
| Create client | PUT | `/org/:orgId/client` |
| Get client | GET | `/client/:clientId` |
| Get by niceId | GET | `/org/:orgId/client/:niceId` |
| Update client | POST | `/client/:clientId` |
| Delete client | DELETE | `/client/:clientId` |
| Archive | POST | `/client/:clientId/archive` |
| Unarchive | POST | `/client/:clientId/unarchive` |
| Block | POST | `/client/:clientId/block` |
| Unblock | POST | `/client/:clientId/unblock` |
### Site Resources (Private / VPN-accessible)
| Action | Method | Endpoint |
|--------|--------|----------|
| List all | GET | `/org/:orgId/site-resources` |
| List for site | GET | `/org/:orgId/site/:siteId/resources` |
| Create | PUT | `/org/:orgId/site-resource` |
| Get | GET | `/site-resource/:id` |
| Update | POST | `/site-resource/:id` |
| Delete | DELETE | `/site-resource/:id` |
### Invitations
| Action | Method | Endpoint |
|--------|--------|----------|
| List invitations | GET | `/org/:orgId/invitations` |
| Create invite | POST | `/org/:orgId/create-invite` |
| Remove invite | DELETE | `/org/:orgId/invitations/:inviteId` |
### Blueprints (IaC)
| Action | Method | Endpoint |
|--------|--------|----------|
| Apply blueprint | PUT | `/org/:orgId/blueprint` |
| List blueprints | GET | `/org/:orgId/blueprints` |
| Get blueprint | GET | `/org/:orgId/blueprint/:blueprintId` |
### Audit Logs
| Action | Method | Endpoint |
|--------|--------|----------|
| Query request logs | GET | `/org/:orgId/logs/request` |
| Export request logs | GET | `/org/:orgId/logs/request/export` |
| Query analytics | GET | `/org/:orgId/logs/analytics` |
### API Keys (Org-scoped)
| Action | Method | Endpoint |
|--------|--------|----------|
| List keys | GET | `/org/:orgId/api-keys` |
| Create key | PUT | `/org/:orgId/api-key` |
| Get key | GET | `/org/:orgId/api-key/:apiKeyId` |
| Delete key | DELETE | `/org/:orgId/api-key/:apiKeyId` |
| List actions | GET | `/org/:orgId/api-key/:apiKeyId/actions` |
| Set actions | POST | `/org/:orgId/api-key/:apiKeyId/actions` |
### Identity Providers
| Action | Method | Endpoint |
|--------|--------|----------|
| List IdPs | GET | `/idp` |
| Create OIDC IdP | PUT | `/idp/oidc` |
| Update IdP | POST | `/idp/:idpId/oidc` |
| Delete IdP | DELETE | `/idp/:idpId` |
### Health & Info
| Action | Method | Endpoint |
|--------|--------|----------|
| Health check | GET | `/` |
| Server info | GET | `/server-info` |
## Known Configuration
### Sites (10 total)
| ID | Name | Type | NiceId |
|----|------|------|--------|
| 1 | 215-node01 | newt | infamous-common-box-turtle |
| 2 | hq-dev01 | newt | great-scytodes-thoracica |
| 3 | Seubert-DC | newt | well-groomed-texas-coral-snake |
| 7 | kaercher5630 | newt | rapid-texas-lined-snake |
| 8 | WulfHQDev | newt | edible-forficula-auricularia |
| 9 | WulfHQProd | newt | yawning-campbells-dwarf-hamster |
| 10 | WulfExp | newt | flawed-indian-desert-jird |
| 11 | Seubert-HQ | newt | productive-chilean-rock-rat |
| 13 | wulfhqclaw | newt | flickering-common-box-turtle |
| 14 | robot01 | newt | impartial-savannah-forest-tree-frog |
### Domains
| ID | Domain | Type | Verified |
|----|--------|------|----------|
| domain1 | wulfconsulting.cloud | wildcard | yes |
| rkkbfx2qxp8bvi5 | wulf.cloud | wildcard | yes |
| 4zl519ktyb1acra | kaercherfamily.com | wildcard | yes |
## Common Workflows
### Check what's exposed publicly
1. List all resources: `GET /org/wulf-consulting/resources`
2. For each resource, list targets: `GET /resource/:resourceId/targets`
### Add a new service
1. Create resource: `PUT /org/wulf-consulting/resource` with subdomain, domain, name
2. Add target: `PUT /resource/:resourceId/target` with site, IP, port
3. Optionally configure SSO, whitelist, or password auth
### Check site connectivity
1. List sites: `GET /org/wulf-consulting/sites` — check `online` field
2. Get specific site: `GET /site/:siteId` for detailed status
### Manage access
1. Get resource: `GET /resource/:resourceId`
2. Set SSO: `POST /resource/:resourceId` with `{"sso": true}`
3. Add email whitelist: `POST /resource/:resourceId/whitelist/add` with `{"email": "..."}`
## Argument Handling
- `$ARGUMENTS` contains the full user request
- If no specific action given, show: sites (with online status), resources, and domains
- Parse natural language: "what's exposed", "list sites", "add service", "check tunnels"

View file

@ -17,22 +17,39 @@ tail -f /var/log/horizon.log
``` ```
## Details ## Details
- **Port:** 3000 (proxied via Pangolin/Nginx to https://horizon.seubert.cloud) - **Port:** 3000 (proxied via Pangolin to https://horizon.seubert.cloud)
- **Working directory:** `/opt/projects/OnDeck/ondeck` - **Stack directory:** `/opt/stacks/horizon`
- **Mode:** `npm run dev` (Next.js with Turbopack) - **Mode:** Docker Compose (production Next.js standalone build)
- **Service file:** `/etc/systemd/system/horizon.service` - **Container names:** `horizon-app`, `horizon-db`
## After code changes ## Managing the container stack
Changes hot-reload automatically. If the server gets into a bad state:
```bash ```bash
systemctl restart horizon cd /opt/stacks/horizon
docker compose pull && docker compose up -d --build # rebuild & restart
docker compose restart horizon-app # restart app only
docker compose down # stop all
docker compose logs -f horizon-app # app logs
docker compose logs -f horizon-db # db logs
```
## After code changes (rebuild image)
```bash
cd /opt/stacks/horizon
docker compose up -d --build horizon-app
``` ```
## After schema changes ## After schema changes
Run these before restarting:
```bash ```bash
cd /opt/projects/OnDeck/ondeck cd /opt/stacks/horizon
npx prisma db push docker compose exec horizon-app npx prisma db push
npx prisma generate docker compose restart horizon-app
systemctl restart horizon ```
## Dev server (local development only)
The systemd dev service still runs at `/opt/projects/OnDeck/ondeck`:
```bash
systemctl start horizon # start dev
systemctl stop horizon # stop dev
systemctl restart horizon # restart dev
systemctl status horizon # check status
``` ```

View file

@ -1,16 +1,17 @@
version: '3.8' version: '3.8'
services: services:
app: horizon-app:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: horizon-app
ports: ports:
- "3000:3000" - "3000:3000"
environment: environment:
- NODE_ENV=production - NODE_ENV=production
- DATABASE_URL=postgresql://ondeck_user:ondeck_password_2026!@db:5432/ondeck - DATABASE_URL=postgresql://horizon_user:${DB_PASSWORD}@horizon-db:5432/horizon
- NEXTAUTH_URL=http://localhost:3000 - NEXTAUTH_URL=${NEXTAUTH_URL}
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET} - NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
- AZURE_AD_CLIENT_ID=${AZURE_AD_CLIENT_ID} - AZURE_AD_CLIENT_ID=${AZURE_AD_CLIENT_ID}
- AZURE_AD_CLIENT_SECRET=${AZURE_AD_CLIENT_SECRET} - AZURE_AD_CLIENT_SECRET=${AZURE_AD_CLIENT_SECRET}
@ -19,35 +20,43 @@ services:
- AFW_DATABASE=${AFW_DATABASE} - AFW_DATABASE=${AFW_DATABASE}
- AFW_USER=${AFW_USER} - AFW_USER=${AFW_USER}
- AFW_PASSWORD=${AFW_PASSWORD} - AFW_PASSWORD=${AFW_PASSWORD}
- CRON_SECRET=${CRON_SECRET}
- METRICS_SECRET=${METRICS_SECRET}
- GRAPH_CLIENT_ID=${GRAPH_CLIENT_ID}
- GRAPH_CLIENT_SECRET=${GRAPH_CLIENT_SECRET}
- GRAPH_TENANT_ID=${GRAPH_TENANT_ID}
depends_on: depends_on:
- db horizon-db:
condition: service_healthy
restart: unless-stopped restart: unless-stopped
networks: networks:
- ondeck-network - horizon-internal
- pangolin
db: horizon-db:
image: postgres:17 image: postgres:17
container_name: horizon-db
environment: environment:
- POSTGRES_DB=ondeck - POSTGRES_DB=horizon
- POSTGRES_USER=ondeck_user - POSTGRES_USER=horizon_user
- POSTGRES_PASSWORD=ondeck_password_2026! - POSTGRES_PASSWORD=${DB_PASSWORD}
ports:
- "5432:5432"
volumes: volumes:
- ondeck-pgdata:/var/lib/postgresql/data - horizon-pgdata:/var/lib/postgresql/data
restart: unless-stopped restart: unless-stopped
networks: networks:
- ondeck-network - horizon-internal
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U ondeck_user -d ondeck"] test: ["CMD-SHELL", "pg_isready -U horizon_user -d horizon"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
volumes: volumes:
ondeck-pgdata: horizon-pgdata:
driver: local driver: local
networks: networks:
ondeck-network: horizon-internal:
driver: bridge driver: bridge
pangolin:
external: true

View file

@ -6,11 +6,11 @@ The Designation Admin Interface allows administrators to create and manage clien
## Key Concepts ## Key Concepts
### Designations are Created in OnDeck ### Designations are Created in Horizon
**All designations are created and managed within the OnDeck application.** They are NOT imported from AFW. Instead: **All designations are created and managed within the Horizon application.** They are NOT imported from AFW. Instead:
1. **Create a designation** in OnDeck (e.g., "Shape", "Premium", "VIP") 1. **Create a designation** in Horizon (e.g., "Shape", "Premium", "VIP")
2. **Optionally map it to an AFW ANotId** (a GUID that identifies customers in AMS360) 2. **Optionally map it to an AFW ANotId** (a GUID that identifies customers in AMS360)
3. **Sync to automatically assign** clients based on their AFW data 3. **Sync to automatically assign** clients based on their AFW data
@ -19,12 +19,12 @@ The Designation Admin Interface allows administrators to create and manage clien
Each designation can have an **optional AFW ANotId** field: Each designation can have an **optional AFW ANotId** field:
- This is a GUID from AMS360/AFW (e.g., `13CF7DCB-F6AF-42C2-A7AB-26641A216A81`) - This is a GUID from AMS360/AFW (e.g., `13CF7DCB-F6AF-42C2-A7AB-26641A216A81`)
- It identifies a group of customers in the AFW database - It identifies a group of customers in the AFW database
- When you sync, clients with this ANotId in AFW get assigned to this designation in OnDeck - When you sync, clients with this ANotId in AFW get assigned to this designation in Horizon
### Example Workflow ### Example Workflow
``` ```
1. Admin creates "Shape" designation in OnDeck 1. Admin creates "Shape" designation in Horizon
- Name: Shape - Name: Shape
- Color: Indigo - Color: Indigo
- AFW ANotId: 13CF7DCB-F6AF-42C2-A7AB-26641A216A81 - AFW ANotId: 13CF7DCB-F6AF-42C2-A7AB-26641A216A81
@ -35,7 +35,7 @@ Each designation can have an **optional AFW ANotId** field:
3. Admin saves the designation 3. Admin saves the designation
4. Admin clicks "Sync" on the Shape designation 4. Admin clicks "Sync" on the Shape designation
- OnDeck queries AFW for customers with ANotId 13CF7DCB-... - Horizon queries AFW for customers with ANotId 13CF7DCB-...
- Finds matching clients in local database - Finds matching clients in local database
- Updates their designationId to "Shape" - Updates their designationId to "Shape"
- Result: 145 clients updated, 5 skipped (already had Shape) - Result: 145 clients updated, 5 skipped (already had Shape)

View file

@ -9,14 +9,14 @@
### What this script does ### What this script does
This script imports historical SHAPE task completion data from the team's SharePoint Excel files into OnDeck. It is a **one-time migration tool** — not part of the ongoing sync. This script imports historical SHAPE task completion data from the team's SharePoint Excel files into Horizon. It is a **one-time migration tool** — not part of the ongoing sync.
Each team member has a folder on SharePoint at: Each team member has a folder on SharePoint at:
``` ```
Claims/SHAPE Accounts/{MEMBER_FOLDER}/{Client}.xlsx Claims/SHAPE Accounts/{MEMBER_FOLDER}/{Client}.xlsx
``` ```
The script reads every client Excel file for each advocate, matches it to a client in OnDeck, and records whether each SHAPE task was completed, marked N/A, or left blank. The script reads every client Excel file for each advocate, matches it to a client in Horizon, and records whether each SHAPE task was completed, marked N/A, or left blank.
### Team member folders ### Team member folders
@ -52,12 +52,12 @@ After running, the report shows:
| Stat | Meaning | | Stat | Meaning |
|---|---| |---|---|
| **Files processed** | Excel files successfully downloaded and parsed | | **Files processed** | Excel files successfully downloaded and parsed |
| **Clients matched** | Clients in the Excel that were found in OnDeck | | **Clients matched** | Clients in the Excel that were found in Horizon |
| **Clients unmatched** | Clients that could not be matched — need manual review | | **Clients unmatched** | Clients that could not be matched — need manual review |
| **Fuzzy matches** | Clients matched by approximate name — review for correctness | | **Fuzzy matches** | Clients matched by approximate name — review for correctness |
| **Tasks updated** | Existing tasks whose status was set to Completed or N/A | | **Tasks updated** | Existing tasks whose status was set to Completed or N/A |
| **Tasks assigned** | Tasks that had the advocate linked to them | | **Tasks assigned** | Tasks that had the advocate linked to them |
| **Tasks created** | Tasks from the Excel that didn't exist in OnDeck yet | | **Tasks created** | Tasks from the Excel that didn't exist in Horizon yet |
| **Duplicates deleted** | Extra copies of the same task removed from the DB | | **Duplicates deleted** | Extra copies of the same task removed from the DB |
| **Ad-hoc created** | "Additional Services" rows and unmatched tasks saved as free-form tasks | | **Ad-hoc created** | "Additional Services" rows and unmatched tasks saved as free-form tasks |
| **Advocates assigned** | Clients that had their Claims Advocate set from this import | | **Advocates assigned** | Clients that had their Claims Advocate set from this import |
@ -65,7 +65,7 @@ After running, the report shows:
### What gets skipped ### What gets skipped
The script will **not** import a row if: The script will **not** import a row if:
- The client name in the Excel cannot be matched to any OnDeck client (logged as "unmatched") - The client name in the Excel cannot be matched to any Horizon client (logged as "unmatched")
- The file is named `archive*`, contains `template`, `master workbook`, or starts with `all shape` — these are intentionally excluded - The file is named `archive*`, contains `template`, `master workbook`, or starts with `all shape` — these are intentionally excluded
Nothing else is filtered. Every task row in every matched client file is processed. Nothing else is filtered. Every task row in every matched client file is processed.
@ -80,9 +80,9 @@ Specifically: if the database already has two identical tasks for the same clien
After running the dry run, check the **"Unmatched Clients"** section of the report. For each one: After running the dry run, check the **"Unmatched Clients"** section of the report. For each one:
1. Check if the client exists in OnDeck under a different spelling 1. Check if the client exists in Horizon under a different spelling
2. If yes — either rename the client in OnDeck to match, or rename the Excel file 2. If yes — either rename the client in Horizon to match, or rename the Excel file
3. If no — create the client in OnDeck first, then re-run the import 3. If no — create the client in Horizon first, then re-run the import
--- ---
@ -116,7 +116,7 @@ Each .xlsx file:
4. processFile() × N For each file: 4. processFile() × N For each file:
└── parseExcelFile() Parse XLSX without external libraries (custom ZIP+XML) └── parseExcelFile() Parse XLSX without external libraries (custom ZIP+XML)
└── parseExcelBlocks() Extract client blocks and task rows └── parseExcelBlocks() Extract client blocks and task rows
└── matchClient() Match Excel client name → OnDeck client └── matchClient() Match Excel client name → Horizon client
└── processTaskRow() × N For each task row: └── processTaskRow() × N For each task row:
└── findMatchingTasks() Look up existing task in DB (±5-day window) └── findMatchingTasks() Look up existing task in DB (±5-day window)
└── deduplication Delete extras if >1 match └── deduplication Delete extras if >1 match

View file

@ -1,5 +1,5 @@
{ {
"name": "ondeck", "name": "horizon",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {

View file

@ -1,7 +1,7 @@
/** /**
* SHAPE Historical Data Import Script * SHAPE Historical Data Import Script
* *
* Imports historical task completion data from SharePoint Excel files into OnDeck. * Imports historical task completion data from SharePoint Excel files into Horizon.
* Each team member has a folder in Claims/SHAPE Accounts/{MEMBER_FOLDER}/{Client}.xlsx * Each team member has a folder in Claims/SHAPE Accounts/{MEMBER_FOLDER}/{Client}.xlsx
* *
* Usage: * Usage: