@cat-factory/app 0.271.0 → 0.272.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,83 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { TaskSourceState } from '@cat-factory/contracts'
3
+ import { boardFromService, huntRequest } from './BugHuntModal.logic'
4
+
5
+ // What scopes a hunt. The rule is small and the failures are silent in both directions: a
6
+ // repo-backed tracker that sends a board would be refused on submit, and one that sent an empty
7
+ // string instead of an explicit null would read as a board named as blank.
8
+
9
+ function state(overrides: Partial<TaskSourceState> = {}): TaskSourceState {
10
+ return {
11
+ source: 'github',
12
+ label: 'GitHub Issues',
13
+ icon: 'i-lucide-github',
14
+ credentialFields: [],
15
+ refLabel: 'Issue URL',
16
+ refPlaceholder: 'acme/web#123',
17
+ available: true,
18
+ enabled: true,
19
+ ridesVcsProvider: 'github',
20
+ supportsIntake: true,
21
+ ignoredIntakePredicates: [],
22
+ repoBacked: true,
23
+ ...overrides,
24
+ }
25
+ }
26
+
27
+ const FORM = { containerId: 'blk-1', board: '', issueType: '', labels: '' }
28
+
29
+ describe('boardFromService', () => {
30
+ it('follows what the SOURCE declares, not which source it is', () => {
31
+ expect(boardFromService(state({ source: 'acme:forge' }))).toBe(true)
32
+ expect(boardFromService(state({ source: 'jira', repoBacked: false }))).toBe(false)
33
+ })
34
+
35
+ it('treats an unresolved source as having a board, so a control is still rendered', () => {
36
+ expect(boardFromService(undefined)).toBe(false)
37
+ })
38
+ })
39
+
40
+ describe('huntRequest', () => {
41
+ it('sends an explicit null board for a repo-backed tracker, whatever was typed before', () => {
42
+ const request = huntRequest({ ...FORM, source: state(), board: 'someone-else/web' })
43
+
44
+ // Null, never '' and never the stale text: the backend REFUSES a board named for such a
45
+ // source, so a hunt that carried one would be rejected rather than scoped.
46
+ expect(request).toEqual({ containerId: 'blk-1', board: null })
47
+ })
48
+
49
+ it('sends the trimmed board a repo-less tracker names', () => {
50
+ const source = state({ source: 'jira', repoBacked: false })
51
+
52
+ expect(huntRequest({ ...FORM, source, board: ' PROJ ' })).toEqual({
53
+ containerId: 'blk-1',
54
+ board: 'PROJ',
55
+ })
56
+ })
57
+
58
+ it('describes no scan until a repo-less tracker has a board', () => {
59
+ const source = state({ source: 'jira', repoBacked: false })
60
+
61
+ expect(huntRequest({ ...FORM, source, board: ' ' })).toBeNull()
62
+ })
63
+
64
+ it('describes no scan without a container, which decides the repository too', () => {
65
+ expect(huntRequest({ ...FORM, source: state(), containerId: undefined })).toBeNull()
66
+ })
67
+
68
+ it('carries only the predicates that were actually filled in', () => {
69
+ const request = huntRequest({
70
+ ...FORM,
71
+ source: state(),
72
+ issueType: ' defect ',
73
+ labels: 'regression, , checkout ',
74
+ })
75
+
76
+ expect(request).toEqual({
77
+ containerId: 'blk-1',
78
+ board: null,
79
+ issueType: 'defect',
80
+ labels: ['regression', 'checkout'],
81
+ })
82
+ })
83
+ })
@@ -0,0 +1,54 @@
1
+ import type { RunBugHuntInput, TaskSourceState } from '@cat-factory/contracts'
2
+
3
+ // The pure half of BugHuntModal: what SCOPES a hunt. Extracted for the reason every `*.logic.ts`
4
+ // here is (a decision worth a test should not need a mounted component to reach), and this one
5
+ // carries the whole rule the surface exists to enforce: a repo-backed tracker hunts the
6
+ // repository of the service the bug will land in, and names no board of its own.
7
+
8
+ /**
9
+ * Whether this tracker's board is the chosen service's repository rather than a choice.
10
+ *
11
+ * Read off the source's declared `repoBacked`, never its id: a deployment that registers its own
12
+ * repo-backed source, or one running GitLab instead of GitHub, must behave identically, and a
13
+ * source list compared here would be a second authority that drifts from the backend's.
14
+ * An unresolved source (still loading, or one this workspace no longer offers) is NOT repo-backed:
15
+ * the answer decides which control to render, and rendering none is the option a user cannot
16
+ * correct.
17
+ */
18
+ export function boardFromService(source: TaskSourceState | undefined): boolean {
19
+ return source?.repoBacked === true
20
+ }
21
+
22
+ /**
23
+ * The scan request, or null when the form does not yet name one.
24
+ *
25
+ * `board` is explicitly `null` for a repo-backed tracker rather than an empty string: the backend
26
+ * REFUSES a board named for such a source instead of ignoring it, so the difference between "no
27
+ * board to name" and "a board named as blank" has to survive this far. The container is required
28
+ * either way: it is where an adopted bug lands, and on a repo-backed tracker it is also what
29
+ * decides which repository is read at all.
30
+ */
31
+ export function huntRequest(input: {
32
+ source: TaskSourceState | undefined
33
+ containerId: string | undefined
34
+ board: string
35
+ issueType: string
36
+ labels: string
37
+ }): RunBugHuntInput | null {
38
+ const { containerId } = input
39
+ if (!input.source || !containerId) return null
40
+ const fromService = boardFromService(input.source)
41
+ const board = input.board.trim()
42
+ if (!fromService && !board) return null
43
+ const issueType = input.issueType.trim()
44
+ const labels = input.labels
45
+ .split(',')
46
+ .map((label) => label.trim())
47
+ .filter((label) => label.length > 0)
48
+ return {
49
+ containerId,
50
+ board: fromService ? null : board,
51
+ ...(issueType ? { issueType } : {}),
52
+ ...(labels.length ? { labels } : {}),
53
+ }
54
+ }
@@ -1,7 +1,14 @@
1
1
  <script setup lang="ts">
2
- // Bug hunt: pick a connected tracker, pick one of its boards, and let the platform rank that
3
- // board's open, UNASSIGNED bugs by impact against implementation complexity. Confirming a
4
- // candidate adopts it as a bug task in the chosen container and starts the bug-fix pipeline.
2
+ // Bug hunt: pick a connected tracker, scope the scan, and let the platform rank that board's
3
+ // open, UNASSIGNED bugs by impact against implementation complexity. Confirming a candidate
4
+ // adopts it as a bug task in the chosen container and starts the bug-fix pipeline.
5
+ //
6
+ // What SCOPES the scan depends on the tracker, and the source states which (`repoBacked`): a
7
+ // repo-backed one (GitHub Issues, GitLab Issues) hunts the repository the chosen service is
8
+ // linked to and offers NO board control, because its issues live in one repo per service and the
9
+ // only honest answer is the one the backend resolves. Every other tracker names a board of its
10
+ // own. Never a picker either way that could aim a hunt at a repository this board holds no
11
+ // service for.
5
12
  //
6
13
  // The interactive dual of the recurring bug-triage schedule: same reading and same pipeline,
7
14
  // but a human picks the bug instead of the oldest match being claimed unattended.
@@ -37,11 +44,14 @@ import {
37
44
  } from '~/utils/sourcePicker'
38
45
  import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
39
46
  import { appliesIntakePredicate } from '~/utils/intakePredicates'
47
+ import { boardFromService as isBoardFromService, huntRequest } from './BugHuntModal.logic'
40
48
 
41
49
  const { t, d, n } = useI18n()
42
50
  const ui = useUiStore()
43
51
  const tasks = useTasksStore()
44
52
  const hunt = useBugHuntStore()
53
+ const board = useBoardStore()
54
+ const github = useGitHubStore()
45
55
  const toast = useToast()
46
56
  const { present } = usePipelineErrorToast()
47
57
 
@@ -130,6 +140,36 @@ const boardItems = computed(() =>
130
140
  })),
131
141
  )
132
142
 
143
+ /** This tracker's board is the chosen service's own repository (see the logic module). */
144
+ const boardFromService = computed(() => isBoardFromService(descriptor.value))
145
+
146
+ /**
147
+ * WHICH repository a repo-backed hunt will read, named before it runs.
148
+ *
149
+ * The premise of this whole branch is that the platform picks the board on the user's behalf, so
150
+ * withholding the value until the results block (`scannedBoard`) states it only after a billable
151
+ * scan has already been paid for. Resolved the way the backend resolves it — walk the chosen
152
+ * container up to its service frame, read that frame's repo link — so the two cannot name
153
+ * different repositories. Null while the projection is still loading or the service holds no
154
+ * link; the field then says what it always said, and the not-linked case stays the backend's to
155
+ * refuse (`boardNeedsRepo`), since an unloaded projection and an unlinked service look identical
156
+ * from here.
157
+ */
158
+ const scopedRepo = computed(() => {
159
+ const container = containerId.value ? board.getBlock(containerId.value) : undefined
160
+ const frame = container ? board.serviceOf(container) : undefined
161
+ const repo = frame ? github.repoForBlock(frame.id) : undefined
162
+ return repo ? `${repo.owner}/${repo.name}` : null
163
+ })
164
+
165
+ /**
166
+ * The service this hunt is scoped to has no repository linked, so it has no issues to read. The
167
+ * one scan failure worded here instead of in a toast: it names something to fix on this board,
168
+ * and it belongs beside the scope it invalidates.
169
+ */
170
+ const REPO_NOT_LINKED: TaskSourceReadReason = 'repo_not_linked'
171
+ const huntNeedsRepo = computed(() => hunt.huntErrorReason === REPO_NOT_LINKED)
172
+
133
173
  /**
134
174
  * The tracker CANNOT enumerate boards, so the user types the scope in themselves. Keyed on the
135
175
  * backend's reason code, not on "any error": an unreachable tracker or an expired token would
@@ -140,10 +180,30 @@ const boardIsFreeText = computed(
140
180
  () => !hunt.boardsLoading && hunt.boardsErrorReason === BOARDS_UNSUPPORTED,
141
181
  )
142
182
 
183
+ /**
184
+ * The refusals this surface words ITSELF, keyed on the backend's reason.
185
+ *
186
+ * The backend does not localize prose, so a reason with no entry here renders the server's
187
+ * untranslated English — the honest last resort for a cause this modal was not built to explain,
188
+ * and the wrong answer for one it was. Both entries are reachable only from a client that
189
+ * disagrees with the backend about which sources are repo-backed (a stale SPA build, a raced
190
+ * `repoBacked` read), which is exactly when a user is least served by raw backend prose.
191
+ * `repo_not_linked` is deliberately absent: it is not a message but a warning rendered beside the
192
+ * scope it invalidates.
193
+ */
194
+ const REFUSAL_KEYS: Partial<Record<TaskSourceReadReason, string>> = {
195
+ board_from_service: 'bugHunt.refusal.boardFromService',
196
+ missing_board: 'bugHunt.refusal.missingBoard',
197
+ }
198
+ function refusalText(reason: string | null, fallback: string | null): string | null {
199
+ const key = reason ? REFUSAL_KEYS[reason as TaskSourceReadReason] : undefined
200
+ return key ? t(key) : fallback
201
+ }
202
+
143
203
  /** A board read that failed for a reason the user has to fix — shown, never silently swallowed. */
144
204
  const boardsFailure = computed(() =>
145
205
  !hunt.boardsLoading && hunt.boardsError !== null && !boardIsFreeText.value
146
- ? hunt.boardsError
206
+ ? refusalText(hunt.boardsErrorReason, hunt.boardsError)
147
207
  : null,
148
208
  )
149
209
 
@@ -157,7 +217,17 @@ function createdAtDate(createdAt: string): Date | null {
157
217
  return Number.isNaN(parsed.getTime()) ? null : parsed
158
218
  }
159
219
 
160
- const canHunt = computed(() => !!source.value && boardId.value.trim().length > 0)
220
+ /** The scan this form currently describes, or null while it does not describe one. */
221
+ const request = computed(() =>
222
+ huntRequest({
223
+ source: descriptor.value,
224
+ containerId: containerId.value,
225
+ board: boardId.value,
226
+ issueType: issueType.value,
227
+ labels: labels.value,
228
+ }),
229
+ )
230
+ const canHunt = computed(() => request.value !== null)
161
231
 
162
232
  watch(open, (isOpen) => {
163
233
  if (!isOpen) return
@@ -168,7 +238,7 @@ watch(open, (isOpen) => {
168
238
  awaitingConnect.value = null
169
239
  source.value = ui.bugHunt?.source ?? tasks.offeredSources[0]?.source ?? undefined
170
240
  resetContainer()
171
- if (source.value) hunt.loadBoards(source.value)
241
+ loadBoardsFor(source.value)
172
242
  })
173
243
 
174
244
  // Switching tracker invalidates both the board list and any ranking already on screen: the
@@ -176,24 +246,40 @@ watch(open, (isOpen) => {
176
246
  watch(source, (next) => {
177
247
  boardId.value = ''
178
248
  hunt.reset()
179
- if (next) hunt.loadBoards(next)
249
+ loadBoardsFor(next)
180
250
  })
181
251
 
252
+ // For a repo-backed tracker the service IS the board, so moving the hunt to another service moves
253
+ // it to another repository: the shortlist on screen belongs to the old one and must go with it.
254
+ // A repo-less tracker keeps its results, since the container only decides where an adopted bug
255
+ // lands and the scan is still of the board that was asked for.
256
+ watch(containerId, () => {
257
+ if (boardFromService.value) hunt.reset()
258
+ })
259
+
260
+ /**
261
+ * Boards are listed only for a tracker that HAS a board to choose; asking otherwise is refused
262
+ * server-side. The other branch is not a no-op: the previous tracker's list (or the warning its
263
+ * failed listing left behind) has to go, or it renders under a tracker with no board field.
264
+ */
265
+ function loadBoardsFor(next: TaskSourceKind | undefined) {
266
+ if (!next) return
267
+ if (isBoardFromService(tasks.descriptorFor(next))) {
268
+ hunt.dropBoards(next)
269
+ // The repo projection is lazy and nothing on the board opens it, so the field that names the
270
+ // repository this hunt will read asks for it here — only on the branch that has one.
271
+ void github.ensureLoaded().catch(() => {})
272
+ } else hunt.loadBoards(next)
273
+ }
274
+
182
275
  async function runHunt() {
183
- if (!source.value || !canHunt.value) return
184
- const parsedLabels = labels.value
185
- .split(',')
186
- .map((l) => l.trim())
187
- .filter((l) => l.length > 0)
188
- const ok = await hunt.hunt(source.value, {
189
- board: boardId.value.trim(),
190
- ...(issueType.value.trim() ? { issueType: issueType.value.trim() } : {}),
191
- ...(parsedLabels.length ? { labels: parsedLabels } : {}),
192
- })
193
- if (!ok) {
276
+ const input = request.value
277
+ if (!source.value || !input) return
278
+ const ok = await hunt.hunt(source.value, input)
279
+ if (!ok && !huntNeedsRepo.value) {
194
280
  toast.add({
195
281
  title: t('bugHunt.huntFailed'),
196
- description: hunt.huntError ?? undefined,
282
+ description: refusalText(hunt.huntErrorReason, hunt.huntError) ?? undefined,
197
283
  icon: 'i-lucide-triangle-alert',
198
284
  color: 'error',
199
285
  })
@@ -306,10 +392,21 @@ const STATUS_KEYS: Record<BugHuntAnalysisStatus, string> = {
306
392
  </UFormField>
307
393
 
308
394
  <UFormField :label="t('bugHunt.board')">
395
+ <!-- This tracker's issues belong to one repository per service, so the board is
396
+ STATED rather than asked: the repository the service below is linked to. No
397
+ control at all, because every value one could offer here is either that repo
398
+ (nothing to choose) or another one this board holds no service for. -->
399
+ <p
400
+ v-if="boardFromService"
401
+ class="flex items-center gap-1.5 py-1 text-sm text-slate-300"
402
+ >
403
+ <UIcon name="i-lucide-folder-git-2" class="h-4 w-4 shrink-0" />
404
+ <span class="truncate">{{ scopedRepo ?? t('bugHunt.boardFromService') }}</span>
405
+ </p>
309
406
  <!-- A tracker that can't enumerate its boards gets a free-text field rather than
310
407
  an empty picker, so the hunt is still usable. -->
311
408
  <UInput
312
- v-if="boardIsFreeText"
409
+ v-else-if="boardIsFreeText"
313
410
  v-model="boardId"
314
411
  :placeholder="t('bugHunt.boardPlaceholder')"
315
412
  class="w-full"
@@ -322,9 +419,14 @@ const STATUS_KEYS: Record<BugHuntAnalysisStatus, string> = {
322
419
  :placeholder="t('bugHunt.pickBoard')"
323
420
  class="w-full"
324
421
  />
422
+ <!-- The service holds no repository, so there are no issues to read. Said here
423
+ rather than in a toast: it invalidates the scope named right above it. -->
424
+ <p v-if="huntNeedsRepo" class="mt-1 text-xs text-amber-400">
425
+ {{ t('bugHunt.boardNeedsRepo') }}
426
+ </p>
325
427
  <!-- A board read that failed for a fixable reason (unreachable site, expired
326
428
  token): named, so the user isn't left with an empty picker and no cause. -->
327
- <p v-if="boardsFailure" class="mt-1 text-xs text-amber-400">
429
+ <p v-else-if="boardsFailure" class="mt-1 text-xs text-amber-400">
328
430
  {{ t('bugHunt.boardsFailed', { reason: boardsFailure }) }}
329
431
  </p>
330
432
  </UFormField>
@@ -347,16 +449,32 @@ const STATUS_KEYS: Record<BugHuntAnalysisStatus, string> = {
347
449
  </UFormField>
348
450
  </div>
349
451
 
350
- <!-- Where an adopted bug lands. Stated when the frame this hunt was opened from is the
351
- only legal target; a choice (scoped to that frame) when it has modules, or over the
352
- whole board when the hunt was opened standalone. -->
452
+ <!-- Where an adopted bug lands, and on a repo-backed tracker WHICH REPOSITORY is
453
+ scanned, so the wording says both rather than leaving the scope unexplained. Stated
454
+ when the frame this hunt was opened from is the only legal target; a choice (scoped
455
+ to that frame) when it has modules, or over the whole board when the hunt was opened
456
+ standalone. -->
457
+ <!-- Two blocks rather than one with a computed `keypath`: the i18n extractor reads a
458
+ bound keypath as the key itself, so a dynamic one is a key missing from every
459
+ locale. Every other `<i18n-t>` in the SPA names its key statically for that reason. -->
353
460
  <p v-if="containerStated" class="text-xs text-slate-400">
354
- <i18n-t keypath="bugHunt.adoptingInto" tag="span" scope="global">
461
+ <i18n-t v-if="boardFromService" keypath="bugHunt.huntingIn" tag="span" scope="global">
462
+ <template #container>
463
+ <span class="font-medium text-slate-200">{{ pinnedContainer!.title }}</span>
464
+ </template>
465
+ </i18n-t>
466
+ <i18n-t v-else keypath="bugHunt.adoptingInto" tag="span" scope="global">
355
467
  <template #container>
356
468
  <span class="font-medium text-slate-200">{{ pinnedContainer!.title }}</span>
357
469
  </template>
358
470
  </i18n-t>
359
471
  </p>
472
+ <!-- Two fields rather than one with a computed key, for the reason the two blocks above
473
+ are two: the i18n extractor reads a bound key as the key itself, so a dynamic one
474
+ leaves BOTH real keys unreferenced and a dead-key sweep prunes them. -->
475
+ <UFormField v-else-if="boardFromService" :label="t('bugHunt.huntIn')">
476
+ <USelect v-model="containerId" :items="containerItems" class="w-full" />
477
+ </UFormField>
360
478
  <UFormField v-else :label="t('bugHunt.adoptInto')">
361
479
  <USelect v-model="containerId" :items="containerItems" class="w-full" />
362
480
  </UFormField>
@@ -379,6 +497,12 @@ const STATUS_KEYS: Record<BugHuntAnalysisStatus, string> = {
379
497
  <!-- Results -->
380
498
  <div v-if="hunt.hasResult" class="space-y-3 border-t border-slate-800 pt-3">
381
499
  <p class="text-xs text-slate-400">
500
+ <!-- The board the scan actually ran against, named because on a repo-backed tracker
501
+ the platform resolved it: the person reading the shortlist should not have to
502
+ infer which repository it came out of. -->
503
+ <span class="text-slate-500">
504
+ {{ t('bugHunt.scannedBoard', { board: hunt.result!.board }) }}
505
+ </span>
382
506
  {{ t(STATUS_KEYS[hunt.result!.analysisStatus]) }}
383
507
  <span v-if="hunt.result!.model" class="text-slate-500">
384
508
  {{ t('bugHunt.viaModel', { model: hunt.result!.model }) }}
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'
2
2
  import { useBugHuntStore } from '~/stores/bugHunt'
3
3
  import { useWorkspaceStore } from '~/stores/workspace'
4
4
  import { ApiError } from '~/composables/api/errors'
5
- import type { TaskSourceKind, TrackerBoardsView } from '~/types/domain'
5
+ import type { BugHuntResult, TaskSourceKind, TrackerBoardsView } from '~/types/domain'
6
6
 
7
7
  // What the board picker does with a FAILED board read. Only one failure means "this tracker
8
8
  // cannot enumerate boards, so type one in"; every other failure has to stay visible as an error,
@@ -23,20 +23,43 @@ const boardsUnsupported = () =>
23
23
  function stubApi(): {
24
24
  store: ReturnType<typeof useBugHuntStore>
25
25
  serve: (fn: () => Promise<unknown>) => void
26
+ serveHunt: (fn: () => Promise<unknown>) => void
26
27
  } {
27
28
  let handler: () => Promise<unknown> = () => Promise.resolve({ source: 'jira', boards: [] })
29
+ let huntHandler: () => Promise<unknown> = () => Promise.resolve(huntResult())
28
30
  vi.stubGlobal('useApi', () => ({
29
31
  listTrackerBoards: (_ws: string, _source: TaskSourceKind) =>
30
32
  handler() as Promise<TrackerBoardsView>,
33
+ runBugHunt: (_ws: string, _source: TaskSourceKind, _input: unknown) =>
34
+ huntHandler() as Promise<BugHuntResult>,
31
35
  }))
32
36
  return {
33
37
  store: useBugHuntStore(),
34
38
  serve: (fn) => {
35
39
  handler = fn
36
40
  },
41
+ serveHunt: (fn) => {
42
+ huntHandler = fn
43
+ },
37
44
  }
38
45
  }
39
46
 
47
+ /** An empty but well-formed scan result, so a success case asserts on the store, not the shape. */
48
+ function huntResult(): BugHuntResult {
49
+ return {
50
+ source: 'github',
51
+ board: 'acme/web',
52
+ analysisStatus: 'empty',
53
+ model: null,
54
+ candidates: [],
55
+ scanned: 0,
56
+ truncated: false,
57
+ }
58
+ }
59
+
60
+ /** The one scan input shape the modal builds; the store only passes it through. */
61
+ const SCAN = { containerId: 'blk_auth', board: null }
62
+
40
63
  describe('bug hunt store — board listing failures', () => {
41
64
  beforeEach(() => {
42
65
  useWorkspaceStore().workspaceId = 'ws1'
@@ -79,6 +102,22 @@ describe('bug hunt store — board listing failures', () => {
79
102
  expect(store.boards.map((b) => b.id)).toEqual(['t1'])
80
103
  })
81
104
 
105
+ it('drops the previous tracker failure when the next one has no board to list', async () => {
106
+ // A repo-backed tracker renders no board field at all, so a stale "boards could not be
107
+ // loaded" warning would sit under a control that is not there, blaming this tracker for the
108
+ // last one's outage.
109
+ const { store, serve } = stubApi()
110
+ serve(() => Promise.reject(apiError(502, 'upstream')))
111
+ await store.loadBoards('jira')
112
+
113
+ store.dropBoards('github')
114
+
115
+ expect(store.boardsSource).toBe('github')
116
+ expect(store.boards).toEqual([])
117
+ expect(store.boardsError).toBeNull()
118
+ expect(store.boardsErrorReason).toBeNull()
119
+ })
120
+
82
121
  it('a source switch mid-flight never lands the older tracker failure on the newer one', async () => {
83
122
  const { store, serve } = stubApi()
84
123
  let rejectJira!: (e: unknown) => void
@@ -100,4 +139,86 @@ describe('bug hunt store — board listing failures', () => {
100
139
  expect(store.boardsErrorReason).toBeNull()
101
140
  expect(store.boardsError).toBeNull()
102
141
  })
142
+
143
+ it('leaves nothing loading when the next tracker has no board to list', async () => {
144
+ // The abandoned listing's own `finally` will not run until it settles, which for a hanging
145
+ // tracker is never. A picker (or a Hunt button) gated on the flag would wait on a request
146
+ // nobody is waiting for.
147
+ const { store, serve } = stubApi()
148
+ serve(() => new Promise(() => {}))
149
+ void store.loadBoards('jira')
150
+ expect(store.boardsLoading).toBe(true)
151
+
152
+ store.dropBoards('github')
153
+
154
+ expect(store.boardsLoading).toBe(false)
155
+ })
156
+
157
+ it('a superseded listing never reports the tracker now loading as done', async () => {
158
+ const { store, serve } = stubApi()
159
+ let settleJira!: (v: unknown) => void
160
+ serve(() => new Promise((res) => (settleJira = res)))
161
+ const inFlight = store.loadBoards('jira')
162
+
163
+ serve(() => new Promise(() => {}))
164
+ void store.loadBoards('linear')
165
+ settleJira({ source: 'jira', boards: [] })
166
+ await inFlight
167
+
168
+ expect(store.boardsSource).toBe('linear')
169
+ expect(store.boardsLoading).toBe(true)
170
+ })
171
+ })
172
+
173
+ describe('bug hunt store — scan failures', () => {
174
+ beforeEach(() => {
175
+ useWorkspaceStore().workspaceId = 'ws1'
176
+ })
177
+
178
+ it('keeps the backend reason for the one failure the surface words itself', async () => {
179
+ // `repo_not_linked` names something fixable on this board, so the modal states it beside the
180
+ // scope it invalidates instead of raising a toast. That routing reads ONLY this field.
181
+ const { store, serveHunt } = stubApi()
182
+ serveHunt(() => Promise.reject(apiError(422, 'validation', { reason: 'repo_not_linked' })))
183
+
184
+ expect(await store.hunt('github', SCAN)).toBe(false)
185
+
186
+ expect(store.huntErrorReason).toBe('repo_not_linked')
187
+ expect(store.huntError).toBeTruthy()
188
+ expect(store.result).toBeNull()
189
+ })
190
+
191
+ it('records NO reason for a scan that simply failed, so it stays a toast', async () => {
192
+ const { store, serveHunt } = stubApi()
193
+ serveHunt(() => Promise.reject(apiError(502, 'upstream')))
194
+
195
+ expect(await store.hunt('jira', { containerId: 'blk_auth', board: 'PROJ' })).toBe(false)
196
+
197
+ expect(store.huntErrorReason).toBeNull()
198
+ expect(store.huntError).toBeTruthy()
199
+ })
200
+
201
+ it('clears a previous scan failure once a later scan succeeds', async () => {
202
+ const { store, serveHunt } = stubApi()
203
+ serveHunt(() => Promise.reject(apiError(422, 'validation', { reason: 'repo_not_linked' })))
204
+ await store.hunt('github', SCAN)
205
+
206
+ serveHunt(() => Promise.resolve(huntResult()))
207
+ expect(await store.hunt('github', SCAN)).toBe(true)
208
+
209
+ expect(store.huntError).toBeNull()
210
+ expect(store.huntErrorReason).toBeNull()
211
+ expect(store.result?.board).toBe('acme/web')
212
+ })
213
+
214
+ it('drops the reason on reset, so a re-opened hunt never re-states the old refusal', async () => {
215
+ const { store, serveHunt } = stubApi()
216
+ serveHunt(() => Promise.reject(apiError(422, 'validation', { reason: 'repo_not_linked' })))
217
+ await store.hunt('github', SCAN)
218
+
219
+ store.reset()
220
+
221
+ expect(store.huntErrorReason).toBeNull()
222
+ expect(store.huntError).toBeNull()
223
+ })
103
224
  })
@@ -38,6 +38,13 @@ export const useBugHuntStore = defineStore('bugHunt', () => {
38
38
  const result = ref<BugHuntResult | null>(null)
39
39
  const hunting = ref(false)
40
40
  const huntError = ref<string | null>(null)
41
+ /**
42
+ * The backend's machine-readable reason for a failed scan, kept for the same reason
43
+ * {@link boardsErrorReason} is: `repo_not_linked` says the service this hunt is scoped to has
44
+ * no repository to read issues from, which is a state the person can fix on the board and the
45
+ * only one the surface words itself. Every other failure stays a toast.
46
+ */
47
+ const huntErrorReason = ref<string | null>(null)
41
48
  /** The candidate currently being adopted, so only its own row shows a spinner. */
42
49
  const adopting = ref<string | null>(null)
43
50
 
@@ -62,20 +69,44 @@ export const useBugHuntStore = defineStore('bugHunt', () => {
62
69
  boardsError.value = e instanceof Error ? e.message : String(e)
63
70
  boardsErrorReason.value = apiErrorReason(e)
64
71
  } finally {
65
- boardsLoading.value = false
72
+ // Only the listing the picker is still showing owns the flag. A superseded one clearing it
73
+ // unconditionally would report the tracker now being loaded as done, which is the same
74
+ // mistake in the opposite direction from the one `dropBoards` avoids.
75
+ if (boardsSource.value === source) boardsLoading.value = false
66
76
  }
67
77
  }
68
78
 
79
+ /**
80
+ * Forget the board listing, because the tracker now in the picker has none to offer: its board
81
+ * is the chosen service's repository, resolved server-side. Called INSTEAD of `loadBoards`, so a
82
+ * previous tracker's list (or its failure, which the surface renders as a warning) cannot sit
83
+ * under a tracker that has no board field at all. `boardsSource` moves with it, so a listing
84
+ * still in flight for that previous tracker lands on nothing rather than reviving the picker.
85
+ */
86
+ function dropBoards(source: TaskSourceKind): void {
87
+ boardsSource.value = source
88
+ boards.value = []
89
+ boardsError.value = null
90
+ boardsErrorReason.value = null
91
+ // The listing in flight belongs to the tracker being left, and its `finally` will not run
92
+ // until it settles — indefinitely, if that tracker hangs. Clearing the flag with the rest of
93
+ // the state is what makes "land on nothing" true of the WHOLE listing rather than of four of
94
+ // its five fields, so a reader gating on it cannot wait on a request nobody is waiting for.
95
+ boardsLoading.value = false
96
+ }
97
+
69
98
  /** Run a hunt and keep its ranked result. Returns false when the scan itself failed. */
70
99
  async function hunt(source: TaskSourceKind, input: RunBugHuntInput): Promise<boolean> {
71
100
  hunting.value = true
72
101
  huntError.value = null
102
+ huntErrorReason.value = null
73
103
  try {
74
104
  result.value = await api.runBugHunt(workspace.requireId(), source, input)
75
105
  return true
76
106
  } catch (e) {
77
107
  result.value = null
78
108
  huntError.value = e instanceof Error ? e.message : String(e)
109
+ huntErrorReason.value = apiErrorReason(e)
79
110
  return false
80
111
  } finally {
81
112
  hunting.value = false
@@ -117,6 +148,7 @@ export const useBugHuntStore = defineStore('bugHunt', () => {
117
148
  function reset(): void {
118
149
  result.value = null
119
150
  huntError.value = null
151
+ huntErrorReason.value = null
120
152
  adopting.value = null
121
153
  }
122
154
 
@@ -131,8 +163,10 @@ export const useBugHuntStore = defineStore('bugHunt', () => {
131
163
  hasResult,
132
164
  hunting,
133
165
  huntError,
166
+ huntErrorReason,
134
167
  adopting,
135
168
  loadBoards,
169
+ dropBoards,
136
170
  hunt,
137
171
  adopt,
138
172
  reset,
@@ -34,6 +34,8 @@ const jiraDescriptor: TaskSourceState = {
34
34
  enabled: true,
35
35
  supportsIntake: true,
36
36
  ignoredIntakePredicates: [],
37
+ // Jira issues belong to a project, not a repository, so a hunt on it picks a board.
38
+ repoBacked: false,
37
39
  // Jira carries its own credentials, so it rides no VCS connection.
38
40
  ridesVcsProvider: null,
39
41
  }
@@ -14,6 +14,7 @@ function state(ignored: TaskSourceState['ignoredIntakePredicates']): TaskSourceS
14
14
  enabled: true,
15
15
  ridesVcsProvider: 'gitlab',
16
16
  supportsIntake: true,
17
+ repoBacked: true,
17
18
  ignoredIntakePredicates: ignored,
18
19
  }
19
20
  }
@@ -4296,7 +4296,9 @@
4296
4296
  "tracker": "Tracker",
4297
4297
  "board": "Board",
4298
4298
  "pickBoard": "Board auswählen",
4299
- "boardPlaceholder": "Projektschlüssel, Team-ID oder owner/repo",
4299
+ "boardPlaceholder": "Projektschlüssel oder Team-ID",
4300
+ "boardFromService": "Das mit diesem Service verknüpfte Repository",
4301
+ "boardNeedsRepo": "Mit diesem Service ist kein Repository verknüpft, es gibt also keine Tickets zu durchsuchen. Verknüpfe eines im Service-Panel.",
4300
4302
  "boardsFailed": "Boards konnten nicht geladen werden: {reason}",
4301
4303
  "issueType": "Vorgangstyp",
4302
4304
  "issueTypeHelp": "Standard ist bug. Wird von Trackern ohne Vorgangstypen ignoriert.",
@@ -4305,6 +4307,8 @@
4305
4307
  "labelsHelp": "Durch Komma getrennt. Alle müssen vorhanden sein.",
4306
4308
  "adoptInto": "Ausgewählten Fehler hinzufügen zu",
4307
4309
  "adoptingInto": "Der ausgewählte Fehler landet in {container}",
4310
+ "huntIn": "Zu durchsuchender Service",
4311
+ "huntingIn": "Durchsucht wird das mit {container} verknüpfte Repository; dort landet auch der ausgewählte Fehler.",
4308
4312
  "run": "Jagen",
4309
4313
  "running": "Board wird gelesen und die Funde werden bewertet…",
4310
4314
  "huntFailed": "Die Jagd ist fehlgeschlagen",
@@ -4317,6 +4321,7 @@
4317
4321
  "noCandidates": "Auf diesem Board gab es keine offenen, nicht zugewiesenen Fehler.",
4318
4322
  "ratings": "Auswirkung {impact}/5, Aufwand {complexity}/5, Konfidenz {confidence}",
4319
4323
  "viaModel": "Bewertet von {model}.",
4324
+ "scannedBoard": "{board} durchsucht.",
4320
4325
  "truncated": "Es wurden nur die ersten {count} passenden Fehler geprüft; dieses Board enthält mehr.",
4321
4326
  "comments": "{count} Kommentar | {count} Kommentare",
4322
4327
  "confidence": {
@@ -4330,6 +4335,10 @@
4330
4335
  "failed": "Die Bewertung konnte nicht abgeschlossen werden, daher sind diese unbewertet.",
4331
4336
  "over_budget": "Dieser Arbeitsbereich hat sein Ausgabenbudget überschritten, daher wurden diese nicht bewertet.",
4332
4337
  "empty": "Nichts zu bewerten."
4338
+ },
4339
+ "refusal": {
4340
+ "boardFromService": "Dieser Tracker durchsucht das Repository, mit dem der gewählte Service verknüpft ist, und hat daher kein eigenes Board.",
4341
+ "missingBoard": "Wähle das Board aus, auf dem gesucht werden soll."
4333
4342
  }
4334
4343
  },
4335
4344
  "pipeline": {
@@ -4897,7 +4897,9 @@
4897
4897
  "tracker": "Tracker",
4898
4898
  "board": "Board",
4899
4899
  "pickBoard": "Pick a board",
4900
- "boardPlaceholder": "Project key, team id or owner/repo",
4900
+ "boardPlaceholder": "Project key or team id",
4901
+ "boardFromService": "The repository this service is linked to",
4902
+ "boardNeedsRepo": "This service has no repository linked, so it has no issues to hunt. Link one from the service panel.",
4901
4903
  "boardsFailed": "Boards could not be loaded: {reason}",
4902
4904
  "issueType": "Issue type",
4903
4905
  "issueTypeHelp": "Defaults to bug. Ignored by trackers with no issue types.",
@@ -4906,6 +4908,8 @@
4906
4908
  "labelsHelp": "Comma separated. All of them must be present.",
4907
4909
  "adoptInto": "Add the picked bug to",
4908
4910
  "adoptingInto": "The picked bug lands in {container}",
4911
+ "huntIn": "Service to hunt",
4912
+ "huntingIn": "Hunting the repository linked to {container}; the picked bug lands there too.",
4909
4913
  "run": "Hunt",
4910
4914
  "running": "Reading the board and rating what it finds…",
4911
4915
  "huntFailed": "The hunt failed",
@@ -4918,6 +4922,7 @@
4918
4922
  "noCandidates": "No open, unassigned bugs matched on this board.",
4919
4923
  "ratings": "Impact {impact}/5, complexity {complexity}/5, {confidence} confidence",
4920
4924
  "viaModel": "Rated by {model}.",
4925
+ "scannedBoard": "Scanned {board}.",
4921
4926
  "truncated": "Only the first {count} matching bugs were scanned; this board holds more.",
4922
4927
  "comments": "{count} comment | {count} comments",
4923
4928
  "@comments": {
@@ -4934,6 +4939,10 @@
4934
4939
  "failed": "The rating could not be completed, so these are unrated.",
4935
4940
  "over_budget": "This workspace is over its spend budget, so these were not rated.",
4936
4941
  "empty": "Nothing to rate."
4942
+ },
4943
+ "refusal": {
4944
+ "boardFromService": "This tracker scans the repository the chosen service is linked to, so it has no board of its own.",
4945
+ "missingBoard": "Pick the board to hunt on."
4937
4946
  }
4938
4947
  },
4939
4948
  "pipeline": {
@@ -4739,7 +4739,9 @@
4739
4739
  "tracker": "Gestor de incidencias",
4740
4740
  "board": "Tablero",
4741
4741
  "pickBoard": "Elige un tablero",
4742
- "boardPlaceholder": "Clave de proyecto, id de equipo o owner/repo",
4742
+ "boardPlaceholder": "Clave de proyecto o id de equipo",
4743
+ "boardFromService": "El repositorio vinculado a este servicio",
4744
+ "boardNeedsRepo": "Este servicio no tiene ningún repositorio vinculado, así que no hay incidencias que explorar. Vincula uno desde el panel del servicio.",
4743
4745
  "boardsFailed": "No se pudieron cargar los tableros: {reason}",
4744
4746
  "issueType": "Tipo de incidencia",
4745
4747
  "issueTypeHelp": "Por defecto bug. Se ignora en gestores sin tipos de incidencia.",
@@ -4748,6 +4750,8 @@
4748
4750
  "labelsHelp": "Separadas por comas. Todas deben estar presentes.",
4749
4751
  "adoptInto": "Añadir el error elegido a",
4750
4752
  "adoptingInto": "El error elegido se añade a {container}",
4753
+ "huntIn": "Servicio a explorar",
4754
+ "huntingIn": "Se explora el repositorio vinculado a {container}; el error elegido también se añade ahí.",
4751
4755
  "run": "Cazar",
4752
4756
  "running": "Leyendo el tablero y valorando lo que encuentra…",
4753
4757
  "huntFailed": "La caza ha fallado",
@@ -4760,6 +4764,7 @@
4760
4764
  "noCandidates": "No hay errores abiertos y sin asignar que coincidan en este tablero.",
4761
4765
  "ratings": "Impacto {impact}/5, complejidad {complexity}/5, confianza {confidence}",
4762
4766
  "viaModel": "Valorado por {model}.",
4767
+ "scannedBoard": "Explorado {board}.",
4763
4768
  "truncated": "Solo se han explorado los primeros {count} errores coincidentes; este tablero contiene más.",
4764
4769
  "comments": "{count} comentario | {count} comentarios",
4765
4770
  "confidence": {
@@ -4773,6 +4778,10 @@
4773
4778
  "failed": "No se ha podido completar la valoración, así que están sin valorar.",
4774
4779
  "over_budget": "Este espacio de trabajo ha superado su presupuesto de gasto, así que no se han valorado.",
4775
4780
  "empty": "Nada que valorar."
4781
+ },
4782
+ "refusal": {
4783
+ "boardFromService": "Este rastreador analiza el repositorio al que está vinculado el servicio elegido, así que no tiene un tablero propio.",
4784
+ "missingBoard": "Elige el tablero en el que buscar."
4776
4785
  }
4777
4786
  },
4778
4787
  "pipeline": {
@@ -4739,7 +4739,9 @@
4739
4739
  "tracker": "Gestionnaire de tickets",
4740
4740
  "board": "Tableau",
4741
4741
  "pickBoard": "Choisir un tableau",
4742
- "boardPlaceholder": "Clé de projet, id d'équipe ou owner/repo",
4742
+ "boardPlaceholder": "Clé de projet ou id d'équipe",
4743
+ "boardFromService": "Le dépôt lié à ce service",
4744
+ "boardNeedsRepo": "Aucun dépôt n'est lié à ce service : il n'y a donc pas de tickets à explorer. Liez-en un depuis le panneau du service.",
4743
4745
  "boardsFailed": "Impossible de charger les tableaux : {reason}",
4744
4746
  "issueType": "Type de ticket",
4745
4747
  "issueTypeHelp": "Par défaut bug. Ignoré par les gestionnaires sans types de tickets.",
@@ -4748,6 +4750,8 @@
4748
4750
  "labelsHelp": "Séparées par des virgules. Toutes doivent être présentes.",
4749
4751
  "adoptInto": "Ajouter le bug retenu à",
4750
4752
  "adoptingInto": "Le bug retenu est ajouté à {container}",
4753
+ "huntIn": "Service à explorer",
4754
+ "huntingIn": "Exploration du dépôt lié à {container} ; le bug retenu y est ajouté.",
4751
4755
  "run": "Chasser",
4752
4756
  "running": "Lecture du tableau et évaluation des résultats…",
4753
4757
  "huntFailed": "La chasse a échoué",
@@ -4760,6 +4764,7 @@
4760
4764
  "noCandidates": "Aucun bug ouvert et non assigné ne correspond sur ce tableau.",
4761
4765
  "ratings": "Impact {impact}/5, complexité {complexity}/5, confiance {confidence}",
4762
4766
  "viaModel": "Évalué par {model}.",
4767
+ "scannedBoard": "{board} exploré.",
4763
4768
  "truncated": "Seuls les {count} premiers bugs correspondants ont été parcourus ; ce tableau en contient davantage.",
4764
4769
  "comments": "{count} commentaire | {count} commentaires",
4765
4770
  "confidence": {
@@ -4773,6 +4778,10 @@
4773
4778
  "failed": "L'évaluation n'a pas pu aboutir, ils sont donc non évalués.",
4774
4779
  "over_budget": "Cet espace de travail a dépassé son budget de dépenses, ces bogues n'ont donc pas été évalués.",
4775
4780
  "empty": "Rien à évaluer."
4781
+ },
4782
+ "refusal": {
4783
+ "boardFromService": "Ce traqueur analyse le dépôt auquel le service choisi est lié : il n'a donc pas de tableau propre.",
4784
+ "missingBoard": "Choisissez le tableau sur lequel chercher."
4776
4785
  }
4777
4786
  },
4778
4787
  "pipeline": {
@@ -4739,7 +4739,9 @@
4739
4739
  "tracker": "מערכת מעקב",
4740
4740
  "board": "לוח",
4741
4741
  "pickBoard": "בחר לוח",
4742
- "boardPlaceholder": "מפתח פרויקט, מזהה צוות או owner/repo",
4742
+ "boardPlaceholder": "מפתח פרויקט או מזהה צוות",
4743
+ "boardFromService": "המאגר המקושר לשירות הזה",
4744
+ "boardNeedsRepo": "לשירות הזה לא מקושר מאגר, ולכן אין בו תקלות לסרוק. קשר מאגר מתוך פאנל השירות.",
4743
4745
  "boardsFailed": "לא ניתן היה לטעון את הלוחות: {reason}",
4744
4746
  "issueType": "סוג הפנייה",
4745
4747
  "issueTypeHelp": "ברירת המחדל היא bug. מתעלמים ממנו במערכות ללא סוגי פניות.",
@@ -4748,6 +4750,8 @@
4748
4750
  "labelsHelp": "מופרדות בפסיקים. כולן חייבות להופיע.",
4749
4751
  "adoptInto": "הוסף את הבאג הנבחר אל",
4750
4752
  "adoptingInto": "הבאג הנבחר יתווסף ל-{container}",
4753
+ "huntIn": "השירות לסריקה",
4754
+ "huntingIn": "נסרק המאגר המקושר ל-{container}; הבאג הנבחר יתווסף לשם.",
4751
4755
  "run": "צוד",
4752
4756
  "running": "קורא את הלוח ומעריך את מה שנמצא…",
4753
4757
  "huntFailed": "הציד נכשל",
@@ -4760,6 +4764,7 @@
4760
4764
  "noCandidates": "אין בלוח הזה באגים פתוחים ולא משויכים שתואמים.",
4761
4765
  "ratings": "השפעה {impact}/5, מורכבות {complexity}/5, רמת ודאות {confidence}",
4762
4766
  "viaModel": "הוערך על ידי {model}.",
4767
+ "scannedBoard": "נסרק {board}.",
4763
4768
  "truncated": "נסרקו רק {count} הבאגים התואמים הראשונים; בלוח הזה יש עוד.",
4764
4769
  "comments": "תגובה אחת | שתי תגובות | {count} תגובות",
4765
4770
  "confidence": {
@@ -4773,6 +4778,10 @@
4773
4778
  "failed": "לא ניתן היה להשלים את ההערכה, ולכן אלה אינם מדורגים.",
4774
4779
  "over_budget": "סביבת העבודה חרגה מתקציב ההוצאות, ולכן הבאגים האלה לא דורגו.",
4775
4780
  "empty": "אין מה להעריך."
4781
+ },
4782
+ "refusal": {
4783
+ "boardFromService": "הגששן סורק את המאגר שאליו מקושר השירות שנבחר, ולכן אין לו לוח משלו.",
4784
+ "missingBoard": "בחרו את הלוח שבו לחפש."
4776
4785
  }
4777
4786
  },
4778
4787
  "pipeline": {
@@ -4296,7 +4296,9 @@
4296
4296
  "tracker": "Tracker",
4297
4297
  "board": "Bacheca",
4298
4298
  "pickBoard": "Scegli una bacheca",
4299
- "boardPlaceholder": "Chiave progetto, id team oppure owner/repo",
4299
+ "boardPlaceholder": "Chiave progetto o id team",
4300
+ "boardFromService": "Il repository collegato a questo servizio",
4301
+ "boardNeedsRepo": "A questo servizio non è collegato alcun repository, quindi non ci sono issue da cercare. Collegane uno dal pannello del servizio.",
4300
4302
  "boardsFailed": "Impossibile caricare le board: {reason}",
4301
4303
  "issueType": "Tipo di ticket",
4302
4304
  "issueTypeHelp": "Per impostazione predefinita bug. Ignorato dai tracker senza tipi di ticket.",
@@ -4305,6 +4307,8 @@
4305
4307
  "labelsHelp": "Separate da virgole. Devono essere tutte presenti.",
4306
4308
  "adoptInto": "Aggiungi il bug scelto a",
4307
4309
  "adoptingInto": "Il bug scelto viene aggiunto a {container}",
4310
+ "huntIn": "Servizio da esaminare",
4311
+ "huntingIn": "Viene esaminato il repository collegato a {container}; il bug scelto viene aggiunto lì.",
4308
4312
  "run": "Cerca",
4309
4313
  "running": "Lettura della bacheca e valutazione di quanto trovato…",
4310
4314
  "huntFailed": "La caccia non è riuscita",
@@ -4317,6 +4321,7 @@
4317
4321
  "noCandidates": "Nessun bug aperto e non assegnato corrisponde su questa bacheca.",
4318
4322
  "ratings": "Impatto {impact}/5, complessità {complexity}/5, confidenza {confidence}",
4319
4323
  "viaModel": "Valutato da {model}.",
4324
+ "scannedBoard": "Esaminato {board}.",
4320
4325
  "truncated": "Sono stati esaminati solo i primi {count} bug corrispondenti; questa bacheca ne contiene altri.",
4321
4326
  "comments": "{count} commento | {count} commenti",
4322
4327
  "confidence": {
@@ -4330,6 +4335,10 @@
4330
4335
  "failed": "La valutazione non è stata completata, quindi non sono valutati.",
4331
4336
  "over_budget": "Questo spazio di lavoro ha superato il budget di spesa, quindi non sono stati valutati.",
4332
4337
  "empty": "Nulla da valutare."
4338
+ },
4339
+ "refusal": {
4340
+ "boardFromService": "Questo tracker analizza il repository a cui è collegato il servizio scelto, quindi non ha una board propria.",
4341
+ "missingBoard": "Scegli la board su cui cercare."
4333
4342
  }
4334
4343
  },
4335
4344
  "pipeline": {
@@ -4739,7 +4739,9 @@
4739
4739
  "tracker": "トラッカー",
4740
4740
  "board": "ボード",
4741
4741
  "pickBoard": "ボードを選択",
4742
- "boardPlaceholder": "プロジェクトキー、チーム ID または owner/repo",
4742
+ "boardPlaceholder": "プロジェクトキーまたはチーム ID",
4743
+ "boardFromService": "このサービスにリンクされたリポジトリ",
4744
+ "boardNeedsRepo": "このサービスにはリポジトリがリンクされていないため、探索できる課題がありません。サービスパネルからリンクしてください。",
4743
4745
  "boardsFailed": "ボードを読み込めませんでした: {reason}",
4744
4746
  "issueType": "課題タイプ",
4745
4747
  "issueTypeHelp": "既定は bug です。課題タイプを持たないトラッカーでは無視されます。",
@@ -4748,6 +4750,8 @@
4748
4750
  "labelsHelp": "カンマ区切り。すべて付いている必要があります。",
4749
4751
  "adoptInto": "選んだバグの追加先",
4750
4752
  "adoptingInto": "選んだバグは {container} に追加されます",
4753
+ "huntIn": "探索するサービス",
4754
+ "huntingIn": "{container} にリンクされたリポジトリを探索します。選んだバグもそこに追加されます。",
4751
4755
  "run": "ハント",
4752
4756
  "running": "ボードを読み取り、見つかったものを評価しています…",
4753
4757
  "huntFailed": "ハントに失敗しました",
@@ -4760,6 +4764,7 @@
4760
4764
  "noCandidates": "このボードには条件に合う未割り当ての未解決バグがありません。",
4761
4765
  "ratings": "影響度 {impact}/5、複雑さ {complexity}/5、確信度 {confidence}",
4762
4766
  "viaModel": "{model} による評価です。",
4767
+ "scannedBoard": "{board} を走査しました。",
4763
4768
  "truncated": "一致したバグのうち最初の {count} 件のみを走査しました。このボードにはさらにあります。",
4764
4769
  "comments": "コメント {count} 件 | コメント {count} 件",
4765
4770
  "confidence": {
@@ -4773,6 +4778,10 @@
4773
4778
  "failed": "評価を完了できなかったため、未評価のまま表示しています。",
4774
4779
  "over_budget": "このワークスペースは利用予算を超えているため、評価は行われませんでした。",
4775
4780
  "empty": "評価する対象がありません。"
4781
+ },
4782
+ "refusal": {
4783
+ "boardFromService": "このトラッカーは選択したサービスにリンクされたリポジトリを走査するため、独自のボードはありません。",
4784
+ "missingBoard": "調査するボードを選択してください。"
4776
4785
  }
4777
4786
  },
4778
4787
  "pipeline": {
@@ -4739,7 +4739,9 @@
4739
4739
  "tracker": "System zgłoszeń",
4740
4740
  "board": "Tablica",
4741
4741
  "pickBoard": "Wybierz tablicę",
4742
- "boardPlaceholder": "Klucz projektu, identyfikator zespołu lub owner/repo",
4742
+ "boardPlaceholder": "Klucz projektu lub identyfikator zespołu",
4743
+ "boardFromService": "Repozytorium powiązane z tą usługą",
4744
+ "boardNeedsRepo": "Ta usługa nie ma powiązanego repozytorium, więc nie ma w niej zgłoszeń do przeszukania. Powiąż je w panelu usługi.",
4743
4745
  "boardsFailed": "Nie udało się wczytać tablic: {reason}",
4744
4746
  "issueType": "Typ zgłoszenia",
4745
4747
  "issueTypeHelp": "Domyślnie bug. Ignorowane przez systemy bez typów zgłoszeń.",
@@ -4748,6 +4750,8 @@
4748
4750
  "labelsHelp": "Oddzielone przecinkami. Wszystkie muszą występować.",
4749
4751
  "adoptInto": "Dodaj wybrany błąd do",
4750
4752
  "adoptingInto": "Wybrany błąd trafi do {container}",
4753
+ "huntIn": "Usługa do przeszukania",
4754
+ "huntingIn": "Przeszukiwane jest repozytorium powiązane z {container}; tam też trafi wybrany błąd.",
4751
4755
  "run": "Poluj",
4752
4756
  "running": "Odczytywanie tablicy i ocenianie znalezionych zgłoszeń…",
4753
4757
  "huntFailed": "Polowanie nie powiodło się",
@@ -4760,6 +4764,7 @@
4760
4764
  "noCandidates": "Na tej tablicy nie ma pasujących otwartych, nieprzypisanych błędów.",
4761
4765
  "ratings": "Wpływ {impact}/5, złożoność {complexity}/5, pewność {confidence}",
4762
4766
  "viaModel": "Ocenione przez {model}.",
4767
+ "scannedBoard": "Przeszukano {board}.",
4763
4768
  "truncated": "Przeszukano tylko pierwsze pasujące zgłoszenia w liczbie {count}; ta tablica zawiera ich więcej.",
4764
4769
  "comments": "{count} komentarz | {count} komentarze | {count} komentarzy",
4765
4770
  "confidence": {
@@ -4773,6 +4778,10 @@
4773
4778
  "failed": "Nie udało się dokończyć oceny, więc te zgłoszenia są nieocenione.",
4774
4779
  "over_budget": "Ta przestrzeń robocza przekroczyła budżet wydatków, więc nie zostały ocenione.",
4775
4780
  "empty": "Nie ma czego oceniać."
4781
+ },
4782
+ "refusal": {
4783
+ "boardFromService": "Ten tracker przeszukuje repozytorium powiązane z wybraną usługą, więc nie ma własnej tablicy.",
4784
+ "missingBoard": "Wybierz tablicę, na której chcesz szukać."
4776
4785
  }
4777
4786
  },
4778
4787
  "pipeline": {
@@ -4739,7 +4739,9 @@
4739
4739
  "tracker": "Takip aracı",
4740
4740
  "board": "Pano",
4741
4741
  "pickBoard": "Bir pano seçin",
4742
- "boardPlaceholder": "Proje anahtarı, takım kimliği veya owner/repo",
4742
+ "boardPlaceholder": "Proje anahtarı veya takım kimliği",
4743
+ "boardFromService": "Bu servise bağlı depo",
4744
+ "boardNeedsRepo": "Bu servise bağlı bir depo yok, dolayısıyla taranacak kayıt da yok. Servis panelinden bir depo bağlayın.",
4743
4745
  "boardsFailed": "Panolar yüklenemedi: {reason}",
4744
4746
  "issueType": "Kayıt türü",
4745
4747
  "issueTypeHelp": "Varsayılan olarak bug. Kayıt türü olmayan araçlarda yok sayılır.",
@@ -4748,6 +4750,8 @@
4748
4750
  "labelsHelp": "Virgülle ayrılır. Hepsinin bulunması gerekir.",
4749
4751
  "adoptInto": "Seçilen hatayı şuraya ekle",
4750
4752
  "adoptingInto": "Seçilen hata {container} içine eklenir",
4753
+ "huntIn": "Taranacak servis",
4754
+ "huntingIn": "{container} servisine bağlı depo taranıyor; seçilen hata da oraya eklenir.",
4751
4755
  "run": "Avla",
4752
4756
  "running": "Pano okunuyor ve bulunanlar değerlendiriliyor…",
4753
4757
  "huntFailed": "Av başarısız oldu",
@@ -4760,6 +4764,7 @@
4760
4764
  "noCandidates": "Bu panoda eşleşen açık ve atanmamış hata yok.",
4761
4765
  "ratings": "Etki {impact}/5, karmaşıklık {complexity}/5, güven {confidence}",
4762
4766
  "viaModel": "{model} tarafından değerlendirildi.",
4767
+ "scannedBoard": "{board} tarandı.",
4763
4768
  "truncated": "Yalnızca eşleşen ilk {count} hata tarandı; bu panoda daha fazlası var.",
4764
4769
  "comments": "{count} yorum | {count} yorum",
4765
4770
  "confidence": {
@@ -4773,6 +4778,10 @@
4773
4778
  "failed": "Değerlendirme tamamlanamadı, bu yüzden bunlar değerlendirilmedi.",
4774
4779
  "over_budget": "Bu çalışma alanı harcama bütçesini aştığı için bunlar değerlendirilmedi.",
4775
4780
  "empty": "Değerlendirilecek bir şey yok."
4781
+ },
4782
+ "refusal": {
4783
+ "boardFromService": "Bu izleyici, seçilen hizmete bağlı depoyu tarar; bu yüzden kendine ait bir panosu yoktur.",
4784
+ "missingBoard": "Aranacak panoyu seçin."
4776
4785
  }
4777
4786
  },
4778
4787
  "pipeline": {
@@ -4739,7 +4739,9 @@
4739
4739
  "tracker": "Трекер",
4740
4740
  "board": "Дошка",
4741
4741
  "pickBoard": "Оберіть дошку",
4742
- "boardPlaceholder": "Ключ проєкту, ідентифікатор команди або owner/repo",
4742
+ "boardPlaceholder": "Ключ проєкту або ідентифікатор команди",
4743
+ "boardFromService": "Репозиторій, пов'язаний із цим сервісом",
4744
+ "boardNeedsRepo": "Із цим сервісом не пов'язано жодного репозиторію, тож у ньому немає задач для перегляду. Пов'яжіть репозиторій на панелі сервісу.",
4743
4745
  "boardsFailed": "Не вдалося завантажити дошки: {reason}",
4744
4746
  "issueType": "Тип запиту",
4745
4747
  "issueTypeHelp": "Типово bug. Ігнорується трекерами без типів запитів.",
@@ -4748,6 +4750,8 @@
4748
4750
  "labelsHelp": "Через кому. Усі мають бути присутні.",
4749
4751
  "adoptInto": "Додати обрану помилку до",
4750
4752
  "adoptingInto": "Обрана помилка додається до {container}",
4753
+ "huntIn": "Сервіс для перегляду",
4754
+ "huntingIn": "Переглядається репозиторій, пов'язаний із {container}; туди ж потрапить обрана помилка.",
4751
4755
  "run": "Полювати",
4752
4756
  "running": "Читаємо дошку та оцінюємо знайдене…",
4753
4757
  "huntFailed": "Полювання не вдалося",
@@ -4760,6 +4764,7 @@
4760
4764
  "noCandidates": "На цій дошці немає відкритих і не призначених помилок, що відповідають умовам.",
4761
4765
  "ratings": "Вплив {impact}/5, складність {complexity}/5, впевненість {confidence}",
4762
4766
  "viaModel": "Оцінено моделлю {model}.",
4767
+ "scannedBoard": "Переглянуто {board}.",
4763
4768
  "truncated": "Переглянуто лише перші відповідні помилки в кількості {count}; на цій дошці їх більше.",
4764
4769
  "comments": "{count} коментар | {count} коментарі | {count} коментарів",
4765
4770
  "confidence": {
@@ -4773,6 +4778,10 @@
4773
4778
  "failed": "Не вдалося завершити оцінювання, тому ці помилки не оцінено.",
4774
4779
  "over_budget": "Цей робочий простір перевищив бюджет витрат, тому їх не оцінено.",
4775
4780
  "empty": "Нема чого оцінювати."
4781
+ },
4782
+ "refusal": {
4783
+ "boardFromService": "Цей трекер сканує репозиторій, з яким пов’язано вибраний сервіс, тож власної дошки не має.",
4784
+ "missingBoard": "Виберіть дошку, на якій шукати."
4776
4785
  }
4777
4786
  },
4778
4787
  "pipeline": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.271.0",
3
+ "version": "0.272.0",
4
4
  "description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,7 +26,7 @@
26
26
  "@modular-vue/vue": "^1.4.1",
27
27
  "@nuxt/ui": "^4.10.0",
28
28
  "@nuxtjs/i18n": "^10.6.0",
29
- "@pinia/nuxt": "^1.0.1",
29
+ "@pinia/nuxt": "^1.0.2",
30
30
  "@toad-contracts/core": "0.4.0",
31
31
  "@toad-contracts/frontend-http-client": "0.3.2",
32
32
  "@toad-contracts/valibot": "0.5.0",
@@ -35,12 +35,12 @@
35
35
  "@vue-flow/node-resizer": "^1.5.1",
36
36
  "@vueuse/core": "^14.4.0",
37
37
  "markdown-it": "^15.0.0",
38
- "pinia": "^4.0.2",
38
+ "pinia": "^4.0.3",
39
39
  "pinia-plugin-persistedstate": "^4.7.1",
40
40
  "valibot": "^1.4.2",
41
41
  "vue": "3.5.41",
42
42
  "wretch": "^3.0.9",
43
- "@cat-factory/contracts": "0.310.0"
43
+ "@cat-factory/contracts": "0.311.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@toad-contracts/testing": "0.3.2",