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>
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>
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>
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>
- 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>
- 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>