Commit graph

7 commits

Author SHA1 Message Date
224e853368 Add a cancel button to the compose page
Users can now abandon a draft they don't want to send. Two entry
points, one shared server action.

Backend
- New action on the existing POST /CollabCentral/:app/jobs/:action
  route: /jobs/cancel. Deletes jobs.building[<personId>::<appName>]
  if present, saves jobs.json, returns 200 with { cancelled: true }.
- Idempotent: if there was no building entry (client fires this on
  every cancel click regardless of state), the endpoint no-ops with
  { cancelled: false } and never touches jobs.json. That keeps disk
  writes tied to real state changes instead of every click.
- Wrapped saveConfig in try/catch so a disk-full or permission
  failure returns a clean 500 instead of crashing the request.

Frontend
- New "Cancel" button in the compose form's action row, sits next
  to "Review & send" as a secondary action.
- cancelMessage() checks whether the form has anything in it (text,
  image, CSV, groups, schedule) and only prompts window.confirm() if
  there's actually a draft to lose. Blank-form clicks skip the
  prompt so the button behaves as expected on first load.
- New "Discard" action on the review modal for "wait, actually no"
  decisions after clicking Review. Positioned on the far left of the
  modal footer with margin-right: auto so it's visually separated
  from the affirmative "Keep editing" / "Send" pair — a stray click
  on the way to Send lands on Keep editing, not Discard.
- Extended clearForm() to also clear both VirtualSelect group
  pickers via setValue([]) (previously only text/files/schedule got
  reset, leaving stale group selections after a cancel or send).
- New .btn-danger style: muted red text on a transparent background,
  filled-red hover state. Used for the modal Discard button and
  available for future destructive UI.

Not covered (follow-up if wanted): the preview DM the bot sends to
the sender during /jobs/edit isn't tracked in the building job, so
Cancel doesn't retract that Webex message. Adding server-side
tracking of the preview message id would let cancel call
DELETE /v1/messages/<id> to clean it up.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 20:32:50 -04:00
524b443dce Make favorite groups editable from the compose page
Selecting a group in "Additional groups" now auto-saves it to the
caller's favorites, and each favorite gets a × affordance for one-click
removal. Favorites still live where they always have —
config.webex.bot[app].authorized[personId].groups — so nothing changes
for existing installations.

Backend
- New POST /CollabCentral/:app/user/groups/add. Body { id }. Validates
  the caller is authorized for :app, validates :id resolves to a real
  Webex group by looking it up in the cached org-wide group list
  (blocks arbitrary strings from being stuffed into config.json),
  dedups against the existing favorites array, writes config.json via
  saveConfig, and returns the updated array.
- New POST /CollabCentral/:app/user/groups/remove. Body { id }.
  Filters that id out of the caller's favorites, writes only when
  something actually changed (a remove of an unknown id no-ops instead
  of rewriting config.json), and returns the updated array.
- Both endpoints are safe against concurrent writes: index.js is
  single-process and node is single-threaded, so read/mutate/write
  runs atomically per request.
- Both log a compact audit line (last-8 of personId + group name) so
  operators can see who is curating what.

Frontend (sendMessage.html + .js + app.css)
- New "Manage favorites" chip strip renders directly under the
  Favorite Groups picker. Each favorite becomes a pill (uses .alias
  when set, otherwise .name; long labels truncate with ellipsis).
  Clicking × on a pill removes that favorite server-side and updates
  the strip locally.
- Additional Groups picker wires a 'change' listener that diffs the
  current selection against the previous one and only fires
  /user/groups/add for newly-picked ids (never re-fires on the
  reselect side of a deselect+reselect, never spams the API with the
  full selection on every keystroke).
- Client keeps favoriteGroups mirrored to every API response so the
  "already a favorite?" dedup check is a pure in-memory lookup — no
  wasted round trips when the user re-picks a group they already
  favorited.
- Transient status message ("Added to favorites." / "Removed from
  favorites." / error variants) fades under the field label; sticks
  around ~4s.
- New shared styles: .fieldLabelRow (label + inline status),
  .fieldStatus (with --error variant), .chipStrip, .chip,
  .chip__label, .chip__close (with hover/focus states).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 20:25:15 -04:00
452673b9be Cache Webex group list in-process + fix log typo
The /user/groups/find endpoint used to hit Webex on every request,
paginating through ~10k groups 500 at a time (roughly 20 sequential
round trips per compose-page load). Now the whole list lives in a
process-local cache that refreshes on startup and once a day at 03:00
CRON_TIMEZONE.

- New groupsCache = { data, lastRefreshed, refreshing }. refreshing is
  a shared in-flight Promise so a startup refresh and the daily cron
  can't stampede if their timing overlaps.
- refreshGroupsCache() wraps findWebexGroup() with load timing +
  error logging.
- getGroupsFromCache() returns the cached list immediately; only the
  very first request after startup waits (and only if the startup
  refresh hasn't completed yet).
- Warm the cache in the background right after loadBotProfiles() in
  the app.listen callback.
- Third cron ('0 0 3 * * *') refreshes the cache daily, offset from
  the 01:10 jobs cleanup and per-minute token/scheduled-jobs tick so
  they don't fight for the event loop.

Bug fixes rolled in while I was in findWebexGroup:
- Return reject(...) on a non-ok Webex response instead of also
  continuing the while loop, which used to race resolve/reject.
- Return reject(error) from the try/catch so a Webex hiccup no longer
  hangs the paginator forever; previously it caught+logged and let
  the loop spin.
- Drop the stray `memberSize = 0` assignment (memberSize was never
  declared in scope).
- Drop the per-page console.log noise; failures now go through the
  timestamped logger under the findWebexGroup tag.

Route cleanup:
- /user/groups/find now serves getGroupsFromCache() and returns 502
  with a logged reason if the cache is empty AND the refresh failed.
- Dropped the stray saveConfig(response, "./testData.json") that
  used to persist the entire fetched group list to a scratch file on
  every request.

Also: fix "identify" -> "identity" in the whoAmI startup log line.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 20:13:58 -04:00
04fb4f6442 Fix OAuth infinite-redirect loop caused by httpOnly session cookies
The bug: SESSION_COOKIE_OPTIONS had httpOnly: true, so js/app.js could
not see the `id` cookie set by /oauth. Every page load thought the user
was signed out, redirected to Webex, minted a fresh token, set another
invisible cookie, redirected back, and looped -- until Webex's Common
Token Store hit its per-user limit and returned
error=tokenlimit_reached on the next callback.

Fix:
- httpOnly is now explicitly false with a comment explaining why: the
  app's design has always relied on the client reading the id and
  displayName cookies via document.cookie.
- sameSite tightened change from 'strict' to 'lax' so the cookie
  reliably survives the webex.com -> /oauth -> /sendMessage.html
  redirect chain across all browsers (some treat continuations of a
  cross-site navigation as cross-site for strict cookies).
- Stop setting access_token, refresh_token, avatar, email, orgId on
  the response. `req.cookies.*` grep confirms the server never reads
  any of them, and the client uses id/displayName only. Removing the
  token cookies also eliminates a would-be XSS foothold.

Existing broken sessions: setting a new cookie with the same name and
path replaces the old one regardless of httpOnly flag, so a single
completed OAuth after this deploy repairs the browser state.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 19:57:33 -04:00
2b37c4b24f Phase 8: extract pure helpers into lib/ and cover with node:test
- Move buildingKey, jobsForApp, getBotToken, isBotEnabled, getBotConfig,
  isAuthorized, getOAuthRedirectUri, buildAuthUrl, cleanCompletedJobs,
  and msToTime into lib/helpers.js as state-free functions that accept
  config, botTokens, or env as parameters. COMPLETED_RETENTION_DAYS also
  lives there so callers and tests share the constant.
- Replace the bodies in index.js with thin wrappers that pass the module-
  level state into the pure helpers. Call sites and behavior are
  unchanged; index.js shrinks by ~60 lines.
- Move the cleanCompletedJobs logging into the cron caller so the pure
  helper returns a result object (jobs, removed, cutoff) that tests can
  assert on without capturing stdout.
- Add test/helpers.test.js with 43 assertions across 10 suites covering
  the enable/disable gating, per-bot draft isolation, authorization,
  OAuth URL construction, retention filter (including endTime -> startTime
  -> created fallback and the safety default for jobs missing a
  timestamp), and the duration formatter.
- Wire `npm test` to `node --test test/*.test.js` (no new deps, uses the
  built-in node:test runner) and document it in the README.

Smoke test confirms unchanged HTTP behavior for /info (known + unknown
bots), the requireBot 404 gate, and the 401 path on jobs/list/completed.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 18:21:02 -04:00
b4e5ca3f33 Phase 7: polish for production readiness
- Add README with local setup, add-a-bot walkthrough, Docker run
  recipe, and a rundown of committed vs. runtime state.
- Wrap the cron schedules with an explicit timezone (default
  America/New_York, override via CRON_TIMEZONE) so cadence is
  independent of the host/container clock.
- Route saveServiceAccountToken through a try/catch so a disk hiccup
  no longer bubbles up as an unhandled exception during token refresh,
  and keep the in-memory copy usable even on write failure.
- Silence per-message queue.on('active') / on('completed') logs that
  also registered a new listener on every job invocation (accumulating
  on the shared queue over time).
- Fix logIt reference (undefined; would have thrown if
  checkScheduledJobs ever rejected) and a broken JSON.stringify.result
  debug log that always evaluated to undefined.
- Dockerfile: switch to node:20-slim, install from the lockfile via
  npm ci --omit=dev, set NODE_ENV=production, and document that env
  and volumes are supplied at runtime.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 18:00:59 -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