Commit graph

10 commits

Author SHA1 Message Date
jmcqueen
c20f387081 feat(lookup): add store-based ticket search for store callers
Store associates share accounts and iPads, so the existing
findMyTickets (keyed by reporter email) misses tickets a coworker
opened for the same store earlier in the shift. This adds a
store-keyed lookup so a store caller can see everything open at
their location.

- New service function searchOpenTicketsByStoreNumber(): normalizes
  to 5 digits, validates against the stores cache (warn-only on miss),
  runs the same Grok-enriched search as the reporter path.
- New route GET /wxccai/open-tickets-by-store?storeNumber=782 —
  accepts padded or unpadded input, degrade-gracefully 200 on
  runtime failures to match the sibling reporter route.
- JQL note: the CMDB "Store Number" field only matches on the
  object *label* (the 5-digit padded string). Neither the objectId,
  ASSET-<id> objectKey, workspace-qualified id, nor raw digits
  match. Probed all variants before landing.
- WxCC AI Agent docs updated: findMyStoreTickets promoted to the
  primary lookup for store callers; findMyTickets reframed as the
  corporate-caller path. System prompt Step 1 rewritten to route
  store vs corporate callers to the right tool.

Verified end-to-end against store 782 — returns 5 open tickets
including two from other reporters (SS-20380 wireless phone,
SS-11943 Zipline training) that email-based lookup misses.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 11:40:29 -04:00
jmcqueen
e9a32e31c1 docs: WxCC AI Agent ticket-operations reference
Adds a shareable reference doc covering everything the Contact Center
AI agent needs to open, look up, comment on, and close SS tickets via
this service:

  1. Inventory of the 9 AI-facing endpoints (read / create / update / close)
  2. The 14 supported SS subTypes (copy-pasteable)
  3. A drop-in system prompt (call-flow discipline, safety rules)
  4. Tool definitions in OpenAI/JSON-schema function-calling format
  5. Two worked call examples
  6. Endpoints deliberately kept out of the AI's tool loop

Companion to the code shipped in this branch — no code changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 11:28:21 -04:00
jmcqueen
034e657fcb feat(close): programmatic SS-ticket closure for the CC agent
Extends the app so a Contact Center AI agent can close SS tickets
end-to-end (customer-confirmed fix, cancellation, duplicate) without a
human touch — the natural next step after ticket creation landed.

The SS Resolved (161) workflow validator requires four fields on the
ticket at execution time (components + 3 CMDB customfields for Business
Service / System / Cause). The transition screen itself only exposes
resolution, so the closer has to PUT these into the ticket first and
*then* fire the transition. This change wires up the whole flow with
sensible per-subType defaults, so the AI can close a typical ticket
with just { subType, comment }.

Design (see Forgejo #10 for the full write-up):

- Generalized services/jira/storesCache into a reusable factory
  createAssetsObjectCache. Same in-memory Map + on-disk JSON snapshot +
  startAt/maxResults pagination + personal-PAT auth. Store cache becomes
  one instance; three new caches join it for the CMDB validator fields
  (businessServices=100, systems=103, causes=107 — all in schema 68, so
  the same permission workaround from Forgejo #8 keeps working).

- Boot-time init warms all four caches in parallel; a new
  services/jira/caches.js barrel exposes them for lifecycle (init) and
  by-name lookup (admin routes). Full sync takes ~5s in the tenant
  (2661 stores dominate — the three CMDB caches together hold ~653
  entries and finish in ~1s).

- Admin surface rebuilt around a plural /admin/caches path:
    GET  /admin/caches                    — status of all four
    GET  /admin/caches/:name/status       — one cache
    POST /admin/caches/:name/refresh      — force resync of one
    POST /admin/caches/refreshAll         — parallel refresh
  Old /admin/storesCache/{status,refresh} are kept as aliases.

- Config renamed: config.storesCache -> config.caches (dir instead of
  path; filenames auto-derived per cache). Env vars renamed to
  CACHES_* (dir/refresh-hours/stale-hours/page-size/max-pages).
  .env.example updated.

- ssCloseDefaults.js maps every supported SS subType to a
  {component, businessService, system, cause} tuple. Values verified
  against the live caches so they resolve at runtime. Falls back to a
  safe __default__ tuple (Help Desk / Store Technology / "I can't find
  my option - Misc" / Unknown) for un-mapped subTypes — those catch-all
  values are the tenant's designed "I don't know" escape valves.

- issues.js closeTicket completely reworked:
    * setSSValidatorFields helper: reads current ticket state, resolves
      each of the four fields via caller > existing-on-ticket > default,
      PUTs them using the Cloud CMDB shape [{id: "<ws>:<objectId>"}]
      (same fix as Forgejo #8 for Store Number).
    * Only writes fields that need writing; preserves human triage by
      default (preserveExistingClassification=true).
    * Fixes Forgejo #9: only sends the resolution field when the
      chosen transition targets statusCategory=done. Non-done
      transitions (e.g. "Waiting for customer") no longer 400 on
      "Field 'resolution' cannot be set."

- closeHelpers.js adds three CC-agent intent wrappers:
    confirmFixed(key, {subType, ...})       → resolution=Done
    customerCancelled(key, {subType, ...})  → resolution=Won't Do
    markDuplicate(key, {primaryKey, ...})   → resolution=Duplicate +
                                              formal Duplicate issueLink
                                              to the primary
  Each posts a standardized internal audit-trail comment ("Closed via
  WxCC AI agent: …") that documents the automated action for the
  humans who inherit the ticket.

- New routes: POST /ticket/:key/{confirmFixed,customerCancelled,
  duplicate} plus an expanded body on the existing /close route.

Verified end-to-end: POST /ticket/SS-20948/confirmFixed with just
subType=Report a Technology issue transitioned the ticket to Resolved
with all four validator fields populated from defaults (Help Desk /
Store Technology / I can't find my option - Misc / Unknown), plus
resolution=Done. Direct Jira REST GET confirms every field persisted.

Closes Forgejo #9. Refs Forgejo #10.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 11:20:41 -04:00
jmcqueen
b4bc6462a3 Fix Assets sync: correct AQL pagination + Jira Cloud CMDB field format
Two bugs found during end-to-end smoke test on the stores cache
workaround (Forgejo #8):

1. Wrong pagination params on POST /object/aql. The endpoint uses
   startAt/maxResults as URL query params, not page/resultPerPage in the
   body. Passing the wrong param names caused the API to silently fall
   back to the default maxResults=25, so the first sync only cached 25
   stores. After fix: 2,661 stores paginated across 6 pages in 5.3s.

2. Wrong request-field shape for CMDB-object custom fields on Jira
   Cloud. resolveStoreAssetReference was returning
     [{ objectId: "75974" }]
   which is the legacy Data Center / Server shape. Cloud requires
     [{ id: "<workspaceId>:<objectId>" }]
   The old shape is silently accepted (HTTP 204) by REST and by
   POST /rest/servicedeskapi/request, but the field is never actually
   persisted \u2014 verified via direct REST GET showing customfield_10261:[].
   After fix: SS-20948 shows the store correctly populated.

- assetsSyncClient.js: use URLSearchParams to pass
  startAt/maxResults/includeAttributes; drop the old page/resultPerPage
  opts; jsdoc updated with pointer to Atlassian Assets API v1 docs.
- storesCache.js: pagination loop now advances by startAt +=
  values.length and trusts isLast (Atlassian caps `total` at 1000 as a
  hint on this endpoint, so we can't rely on it).
- assets.js: new buildStoreFieldRef(objectId) helper produces the
  correct Cloud shape from config.jira.assetsWorkspaceId; all three
  resolution paths (cache / live PAT / service-account fallback) route
  through it. Falls back to legacy shape with a loud error log if
  workspaceId is unset (misconfig).

End-to-end verified: SS-20948 was created for store 00782 (objectId
75974) via cache-only lookup and shows the field populated on both the
JSM request echo and a direct REST GET.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 10:38:28 -04:00
jmcqueen
66255a7b0c Docs: prefer .env for ASSETS_SYNC_TOKEN; Keychain is optional dev path
The bot runs on a Linux host where macOS Keychain isn't available, so .env
is the default supported storage for the personal PAT. Both paths land in
the same process.env slot, but the previous README framing implied Keychain
was mandatory.

- .env.example: promote ASSETS_SYNC_TOKEN from a comment to a real
  REPLACE_ME field; note chmod 600 and rotation guidance
- README: split the setup section into "Setup A - production/Linux (.env)"
  and "Setup B - local dev on macOS (Keychain)"; clarify that the wrapper
  is a no-op if ASSETS_SYNC_TOKEN is already exported
- bin/load-assets-sync-secret.sh: soften the header comment to match

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 10:22:40 -04:00
jmcqueen
fa06538aa4 Workaround #1: cache Assets store lookups via personal PAT sync
The service account is silently filtered out of Object Type 109 (Store
Address / Hierarchy) despite having schema-level read on schema 68, so
every AQL against the store type returns total=0. Until that permission
is granted, resolve store numbers from a local cache populated by a
personal PAT (different auth path, different account, has the role).

- new: src/services/jira/assetsSyncClient.js — Basic-auth axios against
  api.atlassian.com/jsm/assets/workspace/{ws}/v1, credentials sourced
  from ASSETS_SYNC_EMAIL / ASSETS_SYNC_TOKEN (loaded from Keychain by
  bin/load-assets-sync-secret.sh so the PAT never touches .env)
- new: src/services/jira/storesCache.js — in-memory Map + on-disk JSON
  at data/stores.json (gitignored), atomic write, paginated full sync
  via AQL (objectTypeId=N), boot-time load + background refresh if
  stale, periodic setInterval every STORES_CACHE_REFRESH_HOURS
- new: bin/load-assets-sync-secret.sh — Keychain -> env var wrapper
  (security find-generic-password -s jira-assets-sync -a <email>)
- change: resolveStoreAssetReference now tries cache -> live PAT -> the
  existing service-account AQL, in that order; the fallback path is
  preserved so this cleanly deactivates once the permission on #1 is
  fixed. Error message names all three routes and points at the refresh
  endpoint.
- new admin routes: GET /api/wxccai/admin/storesCache/status,
  POST /api/wxccai/admin/storesCache/refresh
- app.js kicks off storesCache.init() after listen()
- config: STORES_CACHE_ENABLED / _PATH / _REFRESH_HOURS /
  _STALE_AFTER_HOURS / _PAGE_SIZE / _MAX_PAGES, plus intFromEnv /
  boolFromEnv helpers
- .gitignore adds data/; .env.example documents the new vars; README
  adds an "Admin" endpoints section and a "Stores cache" setup guide

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 10:18:28 -04:00
jmcqueen
b8f2eab4ab Refactor #6: split jiraService.js into services/jira/{client,issues,comments,attachments,jsmRequests,assets}
Pure move + one-way rewire, no logic changes. The old 1467-line
monolithic services/jiraService.js becomes a thin barrel that re-exports
the same public surface so both current consumers keep working unchanged:
  - src/routes/wxccRoutes.js: `import * as jiraService`
  - src/services/healthService.js: `import { jiraClient }`

New module layout (deps flow one-way, no cycles):
  client.js       — jiraClient, downloadClient, plainTextToAdf (foundational)
  issues.js       — fetch/search/status/update/transitions/close
  comments.js     — fetchPublicComments, addComment, postWebexSummaryComment
  attachments.js  — attachFileToJira, attachReadableTranscript
  assets.js       — AQL, resolveStoreAssetReference, probeAssetsForStore
  jsmRequests.js  — REQUEST_TYPE_MAP, createSSRequest, subtype helpers

Verified: barrel re-exports every original name (23 named + default with
same 17 members), REQUEST_TYPE_MAP still has 14 entries, jiraClient still
instantiates against the configured baseURL, and both consumers import
without errors under the real ES module loader.

New code should import from services/jira/* directly; the barrel is only
for backward compatibility with existing callers.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 16:36:21 -04:00
jmcqueen
312448f597 Fix #5: createSSRequest fails fast when storeNumber is missing
- New SUBTYPES_REQUIRING_STORE_NUMBER set (seeded with every current
  subType, all of which mark Store Number required in JSM). Adding a
  future subType that does NOT require Store Number is a one-line omit.
- createSSRequest now trims storeNumber (rejects whitespace-only) and
  throws a 400 with a clear message ("storeNumber is required for
  subType ...") before making any Jira API call.
- Validation errors now carry err.status = 400 so future non-route
  callers can distinguish client errors from Jira failures.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 16:30:13 -04:00
jmcqueen
c4a0a6934e Fix #4: remove global axios-retry side-effect; add dedicated downloadClient
- wxccRoutes.js no longer mutates the default axios instance, which was
  causing every bare axios call in the codebase (S3 downloads, Assets
  diagnostics) to inherit retries as a side-effect of route file load order.
- jiraService.js now defines a private downloadClient (own timeout, own
  retry policy) used by attachFileToJira and fetchAndConvertTranscript for
  fetching pre-signed S3 URLs.
- Assets AQL/GET remain bare axios calls; they're one-shot diagnostics
  and should not auto-retry.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 16:28:47 -04:00
jmcqueen
1070967870 Initial commit: Webex CC + Jira + xAI service
Core capabilities:
- Jira ticket lifecycle: status, update, comment, transitions, close
- JSM Store Support request creation with Assets object resolution
- Assets AQL diagnostic probe endpoint with schema/type introspection
- Webex transcript ingestion (audio + JSON + human-readable) with
  restricted-visibility summary comments
- Grok-powered single-ticket and open-tickets-by-reporter summaries

Repo hygiene:
- .gitignore covering .env, node_modules, logs, IDE dirs
- .env.example documenting every env var
- discover-ss-*.js scripts refactored to read credentials from .env
- README covering setup, endpoints, and the Assets scope-vs-role gotcha

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 15:23:20 -04:00