Two features stitched together because they touch the same building-
job data path.
--- 1. Retract preview DM on cancel -----------------------------------
The /jobs/edit flow DMs a preview of the composed message to the
sender's own Webex space. Until now that DM lingered even if the
sender then hit Cancel or Discard.
- /jobs/edit now stores the returned message id as
jobs.building[key].previewMessageId.
- /jobs/cancel captures that id before deleting the building entry,
saves the cancel first, then fires a best-effort
DELETE /v1/messages/<id> against Webex.
- New deleteWebexMessage(messageId, appName) helper wraps the DELETE.
Uses the bot token (bots own their messages) and never throws — a
Webex hiccup logs but doesn't fail the cancel that already
succeeded on our side. Called fire-and-forget so the HTTP response
isn't blocked on a slow Webex round trip.
--- 2. Admin: manage authorized users -------------------------------
Admin authority lives in a new top-level config.admins array of
personIds (seeded with Joe's id). Admin actions are cross-bot in
concept but the routes are :app-scoped because the resource being
edited is per-bot and it lets admin reuse the existing OAuth session
without a separate auth surface.
Helpers
- lib/helpers.js: new pure isAdmin(config, personId) that fails
closed when admins is missing, not-an-array, or config is null.
- test/helpers.test.js: 5 new assertions covering the happy path,
the "not in list" case, missing personId, non-array admins, and
missing config. Total suite is now 48 assertions across 11 groups.
Server-side (index.js)
- New findPersonByEmail(email) helper hits Webex /v1/people?email=
using the service account token, returns
{ id, displayName, email, avatar } or null.
- /info now returns isAdmin so the client can decide whether to
render the admin dropdown.
- New requireAdmin(req, res) gate returns 401 for signed-out and
403 for signed-in-but-not-admin (distinct codes so the frontend
can render distinct panels).
- GET /CollabCentral/:app/admin/users → list users
- POST /CollabCentral/:app/admin/users → lookup + add
- DELETE /CollabCentral/:app/admin/users/:id → remove
- Shared adminUserRow / adminUsersList shape so every response is an
authoritative snapshot the client can render without merging.
- DELETE of an unknown id is idempotent — returns 200 removed:false
without rewriting config.json.
Frontend
- New html/admin.html + html/admin.js on the shared layout. Panels
swap between not-signed-in / not-admin / admin. Add-user form
takes an email; user list renders as rows with Webex avatar
(fallback initials), name, email, favorite-group count, and a
Remove button that confirm()s before firing DELETE.
- html/js/app.js: renderUserChip() replaces the plain-text top-right
user label with a proper button + dropdown menu when the caller
is an admin. Menu is keyboard-friendly (Escape to close), closes
on outside-click, and currently exposes one item ("Admin" →
admin.html). Non-admins get the plain-text label unchanged, so
the existing pages are visually identical for them.
- html/css/app.css: new .appHeader__userBtn / .appHeader__userMenu
dropdown, .inlineFieldRow for the email-plus-button pattern, and
.userRow* rules for the admin user list.
Config
- Add config.admins array seeded with Joe McQueen's personId.
- config.json also picks up an in-app state change from the running
instance (an "AV Team" favorite removed from Joe's techupdates
authorized entry via the favorites UI). Rolling that into this
commit so the file stops drifting from origin.
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|---|---|---|
| config | ||
| html | ||
| lib | ||
| test | ||
| uploads | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| Dockerfile | ||
| index.js | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
CollabCentral
A multi-bot mass-messaging service for Webex. Each bot has its own token, avatar, label, and per-user access list, and users can only see the bots and jobs they're authorized on.
Under one server you can host any number of bots (Novi, TechUpdates, …) — each one exposes the same UI at:
/CollabCentral/<botName>/sendMessage.html— compose and send./CollabCentral/<botName>/monitorJobs.html— track running / scheduled / completed jobs./CollabCentral/<botName>/jobDetail.html?jobId=…— detailed per-job view.
Requirements
- Node.js 20+.
- A single Webex integration used for OAuth on all bots.
- A Webex service account (used server-side for group/people lookups; its
refresh token is stored in
config/token.jsonand rotated automatically). - One Webex bot per app you want to host.
Quick start (local)
-
Copy
.env.exampleto.envand fill in the values:SERVER_PORT=1450 CRON_TIMEZONE=America/New_York OAUTH_CALLBACK_URL_TEMPLATE=https://bot.example.com/CollabCentral/:app/oauth WEBEX_INTEGRATION_CLIENT_ID=... WEBEX_INTEGRATION_CLIENT_SECRET=... WEBEX_SERVICE_ACCOUNT_CLIENT_ID=... WEBEX_SERVICE_ACCOUNT_CLIENT_SECRET=... GOOGLE_TRANSLATE_API_KEY=... -
Copy
config/botTokens.example.jsontoconfig/botTokens.jsonand put in the bot tokens for each app:{ "novi": { "token": "…", "enabled": true }, "techupdates": { "token": "…", "enabled": true } } -
Create
config/token.jsonwith the initial service-account OAuth tokens (access + refresh). After the first refresh, the cron job rewrites this file automatically. -
Install dependencies and start:
npm install npm start
npm start uses node --env-file=.env, so no external process manager is
required to load env vars during local development.
Adding a new bot
Say you want to add a bot called alerts.
- Webex bot — create the bot at developer.webex.com. Copy its access token.
- Bot token — add an entry to
config/botTokens.json:
Setting"alerts": { "token": "<bot access token>", "enabled": true }enabled: falsewill keep the bot listed but return 404 for all/CollabCentral/alerts/*routes. - Bot metadata + authorized users — edit
config/config.jsonand add an entry underwebex.bot:
Only person IDs listed under"alerts": { "label": "Ops Alerts", "authorized": { "<webexPersonId>": { "id": "<webexPersonId>", "displayName": "…", "email": "…", "avatar": "…", "groups": [ { "name": "…", "alias": "…", "id": "<webexGroupId>" } ] } } }authorizedcan log into that bot's UI. - Icons — drop
alerts.pngandalerts.icointohtml/(they're served by the per-bot static mount). - OAuth redirect URI — in the Webex integration used for OAuth, register
https://<your-host>/CollabCentral/alerts/oauthas an allowed redirect URI. Without this step, users get an OAuth error on login. - Restart the server (or the container). The bot's avatar is fetched once
at startup via Webex
/people/meusing the token you supplied in step 2.
Docker
Dockerfile builds a stateless image. Config, tokens, uploads, and jobs
data are expected to be mounted at runtime.
docker build -t collabcentral .
docker run -d \
--name collabcentral \
--env-file /path/to/.env \
-v /path/to/config:/usr/src/app/config \
-v /path/to/uploads:/usr/src/app/uploads \
-p 1450:1450 \
collabcentral
The config/ mount should contain at minimum config.json, botTokens.json,
token.json, languages.json, and (if you want to preserve state)
jobs.json and userPrefs.json.
File layout
Committed to the repo:
index.js— Express server, OAuth, cron jobs, Webex API integration, and the send queue.html/— frontend (sendMessage / monitorJobs / jobDetail pages, per-bot icons, shared JS libraries).config/config.json— server settings, per-bot labels, and the authorized-user table for each bot.config/languages.json— supported translation languages.config/botTokens.example.json— template forbotTokens.json..env.example— template for.env.Dockerfile,.dockerignore,.gitignore,package.json,package-lock.json.
Runtime state (gitignored, not committed):
.env— secrets and deployment-specific URLs.config/botTokens.json— per-bot access tokens.config/token.json— service-account OAuth tokens (rotated by the refresh cron).config/jobs.json— building / running / scheduled / completed jobs.config/userPrefs.json— per-user preferences (language, etc.).uploads/— CSVs of recipient IDs and any attached images.
Testing
Unit tests for the pure helpers extracted into lib/helpers.js run under
Node's built-in test runner — no additional dev dependencies required.
npm test
The suite covers bot enable/disable gating (getBotToken, isBotEnabled,
getBotConfig), per-bot draft isolation (buildingKey, jobsForApp), the
authorization check (isAuthorized), OAuth URL construction (buildAuthUrl,
getOAuthRedirectUri), the retention filter (cleanCompletedJobs), and the
duration formatter (msToTime). Adding a new helper? Add it to lib/helpers.js
and cover it in test/helpers.test.js — keeping the state-carrying wrappers
in index.js thin means each helper can be tested without booting the server.
Behavior notes
- Completed-job retention: the daily cleanup cron (default
01:10inCRON_TIMEZONE) drops entries fromjobs.completedolder than 30 days. ChangeCOMPLETED_RETENTION_DAYSinindex.jsto adjust. - Send concurrency: a shared
p-queuelimits outbound Webex sends to 10 in-flight requests. - Rate limiting:
fetchWithRateLimittransparently retries on Webex 429 responses honoringRetry-After. - Per-bot job isolation: draft jobs are keyed by
cookieId + appName, and job list / detail endpoints filter byappName, so authorized users of one bot never see another bot's jobs.