Commit graph

5 commits

Author SHA1 Message Date
44568044e8 Phase R1: split per-user data into gitignored authorized.json
Every add/remove of a favorite group or authorized user was rewriting
config.json — the same file that carries structural bot metadata and
was committed to git. This split ends the git-noise and lets ops
deploy fresh installs without a pre-populated user list.

Split
- config.json (committed) stays structural: server, per-bot labels,
  integration + service-account ids, languages.
- config/authorized.json (gitignored) is the new mutable source of
  truth: { admins: [personId...], bot: { <appName>: { <personId>:
  { id, displayName, email, avatar, groups: [...] } } } }.
- Seeded authorized.json with the current admins list and all
  authorized users (3 on novi, 4 on techupdates) so this commit is
  a pure move — no data lost, no downtime.

Helpers (lib/helpers.js)
- New getAuthorizedEntry(authorized, app, id) as the single lookup
  point every consumer goes through, so nullability is uniform.
- isAuthorized() gains an authorized-doc arg (pure signature stays
  testable): fails closed when the doc is missing / partially
  loaded, so a broken deploy grants no access.
- isAdmin() now reads authorized.admins instead of config.admins.

Runtime (index.js)
- loadAuthorized() with an ENOENT fallback to { admins: [], bot: {} }
  so a fresh deploy can bootstrap via the admin page instead of
  requiring a hand-crafted authorized.json.
- All 8 previous config.webex.bot[app].authorized sites (favorites
  read/add/remove, admin list/add/delete, isAuthorized) now go
  through the authorized doc.
- Every mutation writes to config/authorized.json instead of
  config/config.json.

Latent-bug fixup (uncovered while smoke-testing this refactor)
- The /user/:scope/:action fallthroughs used res.status(4xx)
  without .send(...), so unknown scopes / unauthorized callers got
  a hung request instead of a response. Added ".send(...)" bodies
  so the response actually completes.

Docs + tests
- README updated: new "Authorized users" step in "Adding a new bot",
  updated file-layout section, docker mount list adds
  authorized.json.
- Test suite expanded from 48 → 53 with a new getAuthorizedEntry
  group and the existing isAuthorized/isAdmin cases reshaped for
  the new signatures.

Smoke tested the auth matrix end-to-end (admin + non-admin + signed-
out across /info, /admin/users, /user/groups/list): every path
returns the expected code and body.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 21:06:44 -04:00
bd6bfedb70 Persist authorized-user list changes from the admin UI
Three users added to techupdates.authorized via the admin page:
Samantha Matthews, Rachel Grattan, and Chloe Piccola. Committed so
the on-disk config stays in sync with origin after admin actions.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 20:52:26 -04:00
a793c9f39b Add admin UI for managing per-bot authorized users + retract cancelled preview DMs
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>
2026-07-01 20:47:39 -04:00
2984e8e851 Phase 9: rebuild frontend on a shared layout + fix Monitor Jobs race
The nav link race:
- Every page's header used `<a id="jobLink" href=''>` and the real
  destination was only assigned later, after /info returned. An empty
  href resolves to the current document URL, so clicking "Monitor Jobs"
  on sendMessage before /info completed silently reloaded sendMessage.
- Fixed structurally: nav links now use static relative hrefs baked
  into the HTML ("./sendMessage.html", "./monitorJobs.html"), so the
  destination is correct the moment the DOM parses.

Shared UI:
- New html/css/app.css: design tokens (palette, radius, shadow, font),
  sticky compact top header (bot avatar + label on the left, nav pills
  in the middle, current user on the right, active-page highlight via
  aria-current), card containers, form styling with focus rings,
  DataTables theme overrides, status pills, modal, and responsive
  breakpoints.
- New html/js/app.js: shared browser bootstrap. Parses appName from
  the URL, redirects to OAuth if the id cookie is missing, fetches
  /info once, populates the header, applies aria-current to the
  active nav link, and invokes a per-page onReady callback with
  { info, appName }. Also exports getCookie, escapeHtml, and formatDate
  helpers so each page stops shipping its own copy.

Per-page rewrites:
- sendMessage.html/.js: form now lives in a card, image preview only
  shows when a file is attached, EasyMDE + VirtualSelect styled to
  match the theme, submit is a primary button, confirmation modal
  redesigned. All bootstrap code deleted (delegated to app.js).
- monitorJobs.html/.js: three cards (Running / Scheduled / Completed)
  with themed DataTables. Completed table sorts by start time desc,
  paginates, and searches; message column truncates HTML previews to
  ~120 chars. Empty-state text per table. `jobId` and running-row
  "view" links go to jobDetail via safe relative URLs.
- jobDetail.html/.js: same shared header + card layout; summary grid,
  message preview, and recipient table styled to match the new
  palette.

Sanity checks:
- All 43 helper tests still pass.
- Server boots cleanly on port 3001.
- Curl of sendMessage/monitorJobs/jobDetail all return 200 with the
  shared header markup.
- /CollabCentral/:app/css/app.css and /CollabCentral/:app/js/app.js
  both serve 200 (shared static mount is per-bot as expected).
- No local href in any page is empty; every nav target resolves at
  parse time.
- /info and requireBot 404 gate unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 19:36:36 -04:00
e604c7e9c9 Initial commit: multi-bot CollabCentral
Extends the single-Novi codebase into a multi-bot mass-messenger where
each bot has its own token, avatar, label, and per-user authorization.

- Secrets moved out of config.json: per-bot tokens in gitignored
  config/botTokens.json (with enabled flag), service-account OAuth in
  gitignored config/token.json (rewritten by refresh cron), integration
  and Google keys in .env.
- Single Webex integration handles OAuth for all bots via a per-app
  redirect URI derived from OAUTH_CALLBACK_URL_TEMPLATE.
- New requireBot middleware and getBotConfig helper reject requests for
  unknown or disabled bots at the /CollabCentral/:app boundary.
- New /info endpoint plus dynamic frontend loading (sendMessage,
  monitorJobs) so pages self-describe per bot, including bot avatar
  fetched from Webex /people/me at startup.
- Job draft state keyed by cookieId + appName so each bot has its own
  building queue; job list/detail endpoints filter by appName so users
  only see jobs from bots they are authorized on.
- New jobDetail page for a readable per-job view; completed jobs are
  retained for 30 days by the cleanup cron.
- Completion adaptive cards use per-bot avatar and label.
- Miscellaneous fixes: off-by-two in the send loop, removed three dead
  send/process variants, added defensive init for jobs.* on load,
  dropped the deprecated crypto npm shim, and cleaned up stray logger
  labels and typos.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 17:53:07 -04:00