Files
kandev/apps/backend/internal
João Salavisa 5693c10d8c feat(jira): poll JQL queries to auto-create tasks (issue watchers) (#746)
* feat(jira): poll JQL queries to auto-create tasks (issue watchers)

* fix(jira): address review feedback on issue watcher

- Poller: gate per-watch by `LastPolledAt` + `PollIntervalSeconds` so the
  user-configured cadence actually applies (was silently ignored, every
  watch polled every 5 min). Tick interval reduced to 60s to match the
  smallest valid per-watch interval.
- Service: validate `pollIntervalSeconds` against the documented 60-3600s
  bounds on create/update so the UI's range check is enforced server-side.
- Service: stamp `last_polled_at` via a fresh `context.Background()` with a
  short write deadline; the poller's ctx may be cancelled at shutdown,
  which would otherwise drop the timestamp on the floor.
- Service/store: replace per-ticket `HasIssueWatchTask` round-trips with a
  single `ListSeenIssueKeys` set lookup (up to 50× fewer DB queries per
  tick on broad JQL queries).
- Hook: reset cached watch list on workspace switch — the store's `loaded`
  flag is global, so without this the table renders the previous
  workspace's watchers stale.
- Section component: pull the CRUD callbacks from a single
  `useJiraIssueWatches` call and pass them down, instead of invoking the
  hook twice on the same workspace.
- Dialog: add the three placeholders (`{{issue.type}}`, `{{issue.reporter}}`,
  `{{issue.project}}`) supported by the backend interpolator but missing
  from the help text.

* fix(jira): repair workspace-switch refetch + drop double trigger toast

The previous a9bd9fa attempt at the workspace-switch reset was incomplete:
`setJiraIssueWatches([])` flips `loaded=true`, so the fetch effect's
`if (!workspaceId || loaded || loading) return` guard still short-circuited
and the new workspace's data was never fetched. Switching workspaces left
the table empty with no recovery path.

- Add a dedicated `resetJiraIssueWatches` action that zeroes both `items`
  and `loaded`, and use it from the hook's workspace-change effect.
- Drop the inline "Checking JIRA…" toast in the watcher table — the
  parent's `wrappedTrigger` already fires a result toast on completion,
  so the click was double-toasting.

* fix(jira): reject empty workflowId/workflowStepId on issue-watch PATCH

`applyIssueWatchPatch` writes the field whenever the patch pointer is
non-nil, but Go's JSON decoder returns a non-nil pointer to "" for an
explicit `{"workflowId": ""}` body. The post-patch JQL check guarded
against the same hole for `jql`; this extends it to the workflow
fields. Without it, a stripped row would silently drop every event the
orchestrator tries to handle (the watcher looks active but produces no
tasks).

* fix(jira): require workspace ownership on issue-watch mutate/trigger

The PATCH/DELETE/trigger handlers looked up watches by the path :id with
no workspace check, while the list/create handlers correctly required
workspace_id. A user authenticated to workspace A who learned a watch
UUID from workspace B could mutate it, delete it, or trigger it
(running JQL with workspace B's stored credentials and creating tasks
there). Closes the IDOR by:

- backend: PATCH/DELETE/trigger now require ?workspace_id=... and 404
  on mismatch (404 not 403, so probing can't enumerate IDs across
  workspaces).
- frontend: api client + hook thread workspaceId through all mutation
  calls.
- store: drop the no-longer-used HasIssueWatchTask method (replaced by
  ListSeenIssueKeys after the N+1 fix); test now asserts via the
  bulk-fetched set.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-05-01 11:01:02 +01:00
..