servchan/README.md
jmcqueen 0fc3eb38fa Add /createWO, WO file upload, and suppress SC upload webhook echo.
Enable ServiceChannel work order creation from DM, upload attachments from
WO spaces via /attach and /upload, and skip note/attachment echo for
bot-originated uploads. Also fixes duplicate FedEx notifications and adds
multi-instance command dedup.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-26 17:22:21 -04:00

15 KiB
Raw Blame History

ServChan

Service Channel + Webex Bot for retail facilities coordination.

ServChan automatically creates dedicated Webex collaboration spaces for every ServiceChannel work order, posts rich updates (including AI-summarized descriptions), and provides useful slash commands for technicians and support teams.

The goal of this document is to help you understand how the system actually works — both architecturally (how data and actions flow) and operationally (how to run, monitor, and debug it).

How the System Works (High-Level Flow)

1. Webhook Path (ServiceChannel → Webex Spaces)

This is the core automated behavior:

  1. ServiceChannel sends a webhook to POST /webhook (handled in src/server/app.js).

  2. The route immediately calls webhookProcessor.processWebhook(payload).

  3. webhookProcessor (src/services/webhookProcessor.js) does the following:

    • Uses a per-workOrderId Mutex + queue to safely handle multiple events for the same work order arriving at the same time.
    • Checks the SQLite DB (src/db/mappings.js) to see if a Webex room already exists for this work order.
    • If no room exists:
      • Calls webexService.createWorkOrderRoom(...) to create a new group space under the configured team.
      • Adds the default list of members.
      • Stores the workOrderId ↔ roomId mapping in the database.
    • Builds a message based on the EventType (WorkOrderCreated, WorkOrderNoteAdded, etc.).
    • For WorkOrderCreated, it calls summarizeTicketDescription() from src/integrations/xai/client.js to turn the messy ServiceChannel description into something readable.
    • Posts the message using webexService.sendMarkdown(...).
  4. webexService (src/services/webexService.js) is a thin wrapper. It currently delegates to botClient but gives us a place to add ServChan-specific logic or change the underlying client later without touching the processor.

2. Command Path (User → Bot)

Users interact with the bot via mentions in Webex:

  • The Webex Framework is initialized in src/bot/index.js.
  • All commands are routed through a single Framework.hears(...) handler.
  • Commands are dispatched to handlers in src/commands/ (help.js, avStatus.js, woSummary.js, woAttach.js, etc.).
  • Many of these handlers call the external CollabSupport service (configured via CS_API_BASE) rather than doing heavy work locally.

Key Classes / Modules and Their Responsibilities

Module / Class Role
index.js Thin bootstrap. Creates DB connection, instantiates webhookProcessor + webexService, starts the bot and Express server.
webhookProcessor.js The heart of automation. Owns concurrency control (mutex/queue), room lifecycle decisions, and message construction.
webexService.js Thin service layer on top of the Webex client. Preferred interface for anything that needs to talk to Webex.
botClient.js (in integrations/webex/) Low-level Webex bot operations using the bot token (create rooms, post messages, add members).
adminClient.js (in integrations/webex/) Separate client for privileged operations (DECT, phone lookups, etc.) that require different scopes.
src/integrations/xai/client.js All Grok/xAI calls. Contains both the short initial-description summarizer and the more detailed ticket summarizer.
src/db/mappings.js + path.js Database access for work order ↔ room mappings. Path resolution is centralized here for safety.
src/server/app.js Express routes (/health, /webhook, /cleanup-test, etc.).
src/commands/*.js Individual slash command implementations.

Data & Action Flow Summary

  • Incoming webhookserverwebhookProcessor → (DB + webexService + xai) → Webex room
  • User command → Webex Framework (bot/) → specific command handler → (often external CollabSupport API) → response back to Webex
  • Space cleanupcleanup-test or scheduled job → spaceCleanupService → reads DB + ServiceChannel status → acts via botClient

All persistent state for work order rooms lives in the SQLite database (see "Database Constraint" below).

What Happens When a Work Order Is Created (End-to-End Walkthrough)

Here is a concrete trace of what occurs when ServiceChannel sends a WorkOrderCreated webhook:

  1. Webhook arrives
    ServiceChannel calls POST /webhook with a payload containing EventType: "WorkOrderCreated" and the full work order object.

  2. Route hands off immediately
    src/server/app.js receives the request, logs it, returns 200 OK quickly, and calls:

    webhookProcessor.processWebhook(payload)
    
  3. Concurrency protection
    src/services/webhookProcessor.js looks up (or creates) a Mutex for this specific workOrderId. It also maintains a small queue so multiple rapid events for the same work order are processed in order.

  4. Room lookup
    It queries the database (src/db/mappings.js) to see if a Webex room already exists for this work order.

  5. Room creation (first time only)
    If no room exists:

    • Calls webexService.createWorkOrderRoom(workOrder, teamId)
    • webexService calls botClient.createRoom(...) with the standard title format: ServChan WO-123456 | Store 2477 | Store Name
    • Adds the default members (the list currently hardcoded in index.js)
    • Saves the mapping (workOrderId → roomId) to the database
  6. Description summarization
    Because this is a WorkOrderCreated event, it calls:

    summarizeTicketDescription(description, xaiToken)
    

    from src/integrations/xai/client.js. This sends a focused prompt to Grok to turn the often messy ServiceChannel description into a clean 26 sentence summary.

  7. Message construction
    The processor builds a rich Markdown message containing:

    • Link to the work order in ServiceChannel
    • Store and trade information
    • Priority, category, problem code
    • Status
    • The AI-generated description summary
  8. Message posted
    Calls webexService.sendMarkdown(roomId, text), which ultimately calls botClient.sendMarkdown(...).

  9. Cleanup
    The mutex is released. If the queue for this work order is now empty, the mutex and queue objects are removed from memory.

The result is a new, populated Webex space ready for the team, usually within a few seconds of the work order being created in ServiceChannel.

Directory Structure

ServChan/
├── index.js                 # Thin bootstrap / orchestrator
├── src/
│   ├── bot/                 # Webex Framework + command routing
│   ├── server/              # Express app (routes + handlers)
│   ├── services/            # Core business logic (webhookProcessor, webexService, etc.)
│   ├── integrations/        # External system clients (webex, serviceChannel, xai)
│   ├── db/                  # Database access + safe path resolution
│   ├── commands/            # Individual slash command implementations
│   ├── legacy/              # Old code kept during transition (avoid editing)
│   ├── experimental/        # Superseded device code (mostly unused now)
│   └── utils/
├── tools/                   # Standalone operational scripts
├── config/                  # Non-secret config (secrets still here too)
├── data/                    # SQLite database (handle with care)
├── Dockerfile
├── docker-compose.yml
├── REFACTOR-LOG.md          # History of major changes (reference only)
└── README.md

Operational Considerations

The Database Constraint (Very Important)

The SQLite database file used by the live production bot must never be moved, renamed, or have a second copy accidentally created.

  • All code resolves the DB path through src/db/path.js, which respects DB_PATH from your environment.
  • The main application creates one connection in index.js and passes that instance around.
  • When debugging, always verify which database file is actually being used (check logs at startup — it prints the path).

If you ever see two different .db files being touched, stop and investigate immediately.

Remote Service Dependency

Many user-facing commands (/avStatus, /woSummary, /woAttachments, /woHistory) do not do the heavy lifting locally. They call an external service via the CS_API_BASE environment variable (currently pointing at a CollabSupport instance).

If those commands start failing but the webhook automation still works, the problem is usually with the remote service or network connectivity to it.

Webex Token Separation

  • Normal bot actions (creating rooms for work orders, posting messages, adding members) use the bot token via botClient.
  • Advanced lookups (DECT networks, phones assigned to stores, etc.) require a different privileged token and go through adminClient.

Keep these two concerns separate. Do not mix the tokens.

Logging & Observability

  • Application logs are written daily to ./logs/YYYYMMDD.log.
  • The good structured logger lives in src/utils/logger.js.
  • Some older legacy paths still use simpler console + file logging.
  • The /health endpoint is your best friend for runtime state (DB connectivity, memory, uptime).

Space Cleanup

Automated (or manual via /cleanup-test) removal of old completed work order spaces is handled by spaceCleanupService.js.

It reads work order status from ServiceChannel and then acts on the Webex rooms via the bot client. It is intentionally conservative.

Running the Application

Local

npm run dev

Production (default — optimized image, no source mount, npm start):

docker compose up -d --build

After the first build you can usually omit --build:

docker compose up -d

Development (live reload via node --watch — restarts on file changes; dev only):

docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build

See docker-compose.yml and docker-compose.dev.yml for details.

Important for production images:

  • All secrets must come from .env (via env_file). The application now enforces this via src/config/secrets.js.
  • config/config.json must not contain real credentials (it can still hold non-secret values like server name/port or legacy non-token webex fields for compatibility).
  • The production stage runs as the non-root node user (uid 1000). On Linux hosts ensure your ./data and ./logs host directories are writable by uid 1000 if you see permission errors.

Production Considerations

  • Use the production compose files for any long-lived or customer-facing deployment.
  • Logs and the SQLite DB are the only things that need to persist across container restarts/recreates. Everything else is in the image.
  • The /health and /healthz endpoints are suitable for container orchestrators (Kubernetes liveness/readiness, etc.).
  • Consider adding resource limits, secrets management (Docker secrets / Vault), and log rotation outside the container for full prod ops.
  • Always run docker compose ... config to validate before deploying.

Health Checks

  • GET /healthz → Simple liveness (used by Docker healthcheck)
  • GET /health → Detailed JSON report including database connectivity and memory usage

Common Debugging Commands

  • Check which DB file is in use → Look at the very first logs after startup.
  • Test space cleanup logic → GET /cleanup-test?dryRun=true
  • Force a fresh xAI summary on a new work order → Create a test work order in ServiceChannel.

Bot Commands

Command Context Notes
/help Anywhere Context-aware
/avStatus In WO space or with store number Often calls remote service
/woSummary In WO space or with WO number Often calls remote service
/woAttachments In WO space or with WO number Often calls remote service
/woHistory With store number Often calls remote service
/woApprove In WO space (or with WO#) Manually posts proposal approval Adaptive Card (when status is IN PROGRESS | WAITING FOR APPROVAL). Auto-triggered on relevant webhooks too.
/attach / /upload WO space only Upload a photo or file to the linked ServiceChannel work order; optional caption becomes an SC note.
/createWO 1:1 DM only Adaptive Card to create a new work order in ServiceChannel; Webex group space is created automatically via webhook.

Create WO (new): In a direct message with ServChan, run /createWO to open a form (store, description, category, priority, optional problem code/NTE). Defaults come from env (SC_CREATE_DEFAULT_*). Admin discovery: GET /create-wo-discovery?trade=Audio (requires ADMIN_TOKEN).

Approval Cards (new): When a webhook arrives with IN PROGRESS | WAITING FOR APPROVAL (typically with a "Proposal created" note), ServChan automatically posts an Adaptive Card v1.3 in the room. The card shows proposal details/costs, prepopulates the suggested new NTE (current + proposal sums), and allows submit to PATCH the NTE in ServiceChannel + record approver attribution from the Webex user. Use /woApprove to re-trigger.

Development Guidelines

When making changes:

  • Prefer dependency injection (especially passing the db instance) over creating new connections.
  • If you need to talk to Webex for normal bot operations, go through webexService rather than calling botClient directly.
  • All AI summarization should live in or be called from src/integrations/xai/.
  • Respect the production database path resolution — never hardcode a .db path.
  • If you're touching webhook behavior, be extremely careful with concurrency (the mutex/queue pattern exists for a reason).

Environment Variables

Variable Purpose Notes
DB_PATH SQLite database location Critical — production uses this
WEBEX_BOT_TOKEN Token for normal bot operations Used by botClient
CS_API_BASE Base URL for remote CollabSupport service Powers many commands
WEBEX_BOT_PERSON_ID Used to identify the bot during cleanup Prevents removing itself
WEBEX_ADMIN_TOKEN (or similar) Token for privileged Webex calls Used by adminClient (if configured)

Secrets must be provided exclusively via environment variables (loaded by src/config/secrets.js and required at startup for core functionality). config/config.json should contain only non-secret values. The code no longer depends on secrets living in the JSON file.


This document is meant to be living. Update it when you add significant new behavior or change how major flows work.