diff --git a/.claude/settings.local.json b/.claude/settings.local.json index fe47756..51b461a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -29,7 +29,12 @@ "Read(//usr/lib/**)", "Bash(ls /usr/bin/pg*)", "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 '{:*)" ] } } diff --git a/.claude/skills/pangolin/SKILL.md b/.claude/skills/pangolin/SKILL.md new file mode 100644 index 0000000..84d5045 --- /dev/null +++ b/.claude/skills/pangolin/SKILL.md @@ -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" diff --git a/ondeck/SERVER.md b/ondeck/SERVER.md index f6cf345..b43ee6a 100644 --- a/ondeck/SERVER.md +++ b/ondeck/SERVER.md @@ -17,22 +17,39 @@ tail -f /var/log/horizon.log ``` ## Details -- **Port:** 3000 (proxied via Pangolin/Nginx to https://horizon.seubert.cloud) -- **Working directory:** `/opt/projects/OnDeck/ondeck` -- **Mode:** `npm run dev` (Next.js with Turbopack) -- **Service file:** `/etc/systemd/system/horizon.service` +- **Port:** 3000 (proxied via Pangolin to https://horizon.seubert.cloud) +- **Stack directory:** `/opt/stacks/horizon` +- **Mode:** Docker Compose (production Next.js standalone build) +- **Container names:** `horizon-app`, `horizon-db` -## After code changes -Changes hot-reload automatically. If the server gets into a bad state: +## Managing the container stack ```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 -Run these before restarting: ```bash -cd /opt/projects/OnDeck/ondeck -npx prisma db push -npx prisma generate -systemctl restart horizon +cd /opt/stacks/horizon +docker compose exec horizon-app npx prisma db push +docker compose restart horizon-app +``` + +## 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 ``` diff --git a/ondeck/docker-compose.yml b/ondeck/docker-compose.yml index 46333a1..7e5611d 100644 --- a/ondeck/docker-compose.yml +++ b/ondeck/docker-compose.yml @@ -1,16 +1,17 @@ version: '3.8' services: - app: + horizon-app: build: context: . dockerfile: Dockerfile + container_name: horizon-app ports: - "3000:3000" environment: - NODE_ENV=production - - DATABASE_URL=postgresql://ondeck_user:ondeck_password_2026!@db:5432/ondeck - - NEXTAUTH_URL=http://localhost:3000 + - DATABASE_URL=postgresql://horizon_user:${DB_PASSWORD}@horizon-db:5432/horizon + - NEXTAUTH_URL=${NEXTAUTH_URL} - NEXTAUTH_SECRET=${NEXTAUTH_SECRET} - AZURE_AD_CLIENT_ID=${AZURE_AD_CLIENT_ID} - AZURE_AD_CLIENT_SECRET=${AZURE_AD_CLIENT_SECRET} @@ -19,35 +20,43 @@ services: - AFW_DATABASE=${AFW_DATABASE} - AFW_USER=${AFW_USER} - 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: - - db + horizon-db: + condition: service_healthy restart: unless-stopped networks: - - ondeck-network + - horizon-internal + - pangolin - db: + horizon-db: image: postgres:17 + container_name: horizon-db environment: - - POSTGRES_DB=ondeck - - POSTGRES_USER=ondeck_user - - POSTGRES_PASSWORD=ondeck_password_2026! - ports: - - "5432:5432" + - POSTGRES_DB=horizon + - POSTGRES_USER=horizon_user + - POSTGRES_PASSWORD=${DB_PASSWORD} volumes: - - ondeck-pgdata:/var/lib/postgresql/data + - horizon-pgdata:/var/lib/postgresql/data restart: unless-stopped networks: - - ondeck-network + - horizon-internal healthcheck: - test: ["CMD-SHELL", "pg_isready -U ondeck_user -d ondeck"] + test: ["CMD-SHELL", "pg_isready -U horizon_user -d horizon"] interval: 10s timeout: 5s retries: 5 volumes: - ondeck-pgdata: + horizon-pgdata: driver: local networks: - ondeck-network: + horizon-internal: driver: bridge + pangolin: + external: true diff --git a/ondeck/docs/designation-admin-guide.md b/ondeck/docs/designation-admin-guide.md index 160af2b..a800908 100644 --- a/ondeck/docs/designation-admin-guide.md +++ b/ondeck/docs/designation-admin-guide.md @@ -6,11 +6,11 @@ The Designation Admin Interface allows administrators to create and manage clien ## 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) 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: - This is a GUID from AMS360/AFW (e.g., `13CF7DCB-F6AF-42C2-A7AB-26641A216A81`) - 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 ``` -1. Admin creates "Shape" designation in OnDeck +1. Admin creates "Shape" designation in Horizon - Name: Shape - Color: Indigo - 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 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 - Updates their designationId to "Shape" - Result: 145 clients updated, 5 skipped (already had Shape) diff --git a/ondeck/docs/shape-historical-import.md b/ondeck/docs/shape-historical-import.md index f8ac874..2b8a0e7 100644 --- a/ondeck/docs/shape-historical-import.md +++ b/ondeck/docs/shape-historical-import.md @@ -9,14 +9,14 @@ ### 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: ``` 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 @@ -52,12 +52,12 @@ After running, the report shows: | Stat | Meaning | |---|---| | **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 | | **Fuzzy matches** | Clients matched by approximate name — review for correctness | | **Tasks updated** | Existing tasks whose status was set to Completed or N/A | | **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 | | **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 | @@ -65,7 +65,7 @@ After running, the report shows: ### What gets skipped 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 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: -1. Check if the client exists in OnDeck under a different spelling -2. If yes — either rename the client in OnDeck to match, or rename the Excel file -3. If no — create the client in OnDeck first, then re-run the import +1. Check if the client exists in Horizon under a different spelling +2. If yes — either rename the client in Horizon to match, or rename the Excel file +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: └── parseExcelFile() Parse XLSX without external libraries (custom ZIP+XML) └── 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: └── findMatchingTasks() Look up existing task in DB (±5-day window) └── deduplication Delete extras if >1 match diff --git a/ondeck/package.json b/ondeck/package.json index 8e91524..0b29003 100644 --- a/ondeck/package.json +++ b/ondeck/package.json @@ -1,5 +1,5 @@ { - "name": "ondeck", + "name": "horizon", "version": "0.1.0", "private": true, "scripts": { diff --git a/ondeck/scripts/import-shape-historical.ts b/ondeck/scripts/import-shape-historical.ts index c461272..c0ac147 100644 --- a/ondeck/scripts/import-shape-historical.ts +++ b/ondeck/scripts/import-shape-historical.ts @@ -1,7 +1,7 @@ /** * 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 * * Usage: