@cat-factory/app 0.173.0 → 0.176.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.
- package/README.md +25 -13
- package/app/components/board/nodes/BlockNode.vue +30 -0
- package/app/components/focus/BlockFocusView.vue +35 -1
- package/app/components/initiative/InitiativePlanningWindow.vue +57 -15
- package/app/components/layout/IntegrationsHub.vue +14 -1
- package/app/components/panels/InspectorPanel.vue +20 -31
- package/app/components/panels/inspector/ContainerSummary.vue +54 -62
- package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +11 -0
- package/app/components/panels/inspector/TaskExecution.vue +5 -20
- package/app/composables/useBlockDeletion.ts +8 -1
- package/app/composables/useRunReset.ts +48 -0
- package/app/modular/nav-contributions.spec.ts +54 -19
- package/app/modular/nav-contributions.ts +26 -12
- package/app/modular/panels/inspector.logic.spec.ts +8 -2
- package/app/modular/panels/inspector.logic.ts +14 -2
- package/app/stores/observability.ts +4 -0
- package/i18n/locales/de.json +17 -12
- package/i18n/locales/en.json +17 -12
- package/i18n/locales/es.json +17 -12
- package/i18n/locales/fr.json +17 -12
- package/i18n/locales/he.json +17 -12
- package/i18n/locales/it.json +17 -12
- package/i18n/locales/ja.json +17 -12
- package/i18n/locales/pl.json +17 -12
- package/i18n/locales/tr.json +17 -12
- package/i18n/locales/uk.json +17 -12
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -56,10 +56,10 @@ over the WebSocket. How that sync works is written up in
|
|
|
56
56
|
## Interface modes (basic / advanced)
|
|
57
57
|
|
|
58
58
|
The SPA renders at one of two **interface tiers**. `basic` (the default) is the everyday
|
|
59
|
-
surface
|
|
60
|
-
|
|
61
|
-
to
|
|
62
|
-
match wins:
|
|
59
|
+
**delivery** surface — plan work on a board, run it, review and merge it: the run/pipeline
|
|
60
|
+
options that only exist to override a workspace-level default are left at that default, and
|
|
61
|
+
the nav is trimmed to what that loop needs. `advanced` shows everything. The tier resolves in
|
|
62
|
+
a fixed order, first match wins:
|
|
63
63
|
|
|
64
64
|
1. **`NUXT_PUBLIC_UI_MODE`** (`basic` | `advanced`) — the deployment pin. Like
|
|
65
65
|
`NUXT_PUBLIC_API_BASE` it is baked in at **build** time (`ssr: false`), and while it is
|
|
@@ -82,20 +82,32 @@ hoc where it can be avoided:
|
|
|
82
82
|
- **A nav destination** declares `advanced: true` in `app/modular/nav-contributions.ts`. The
|
|
83
83
|
shared `navSlotFilter` drops it in basic mode across all three shells (sidebar, command
|
|
84
84
|
palette, toolbar), independently of its RBAC `gate` — both must pass. A consumer module's
|
|
85
|
-
own contributions take the same flag. The bar
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
85
|
+
own contributions take the same flag. The bar is **whether the everyday delivery loop needs
|
|
86
|
+
it**, and marking an item does one of two distinguishable things:
|
|
87
|
+
- **Reached another way** — a shortcut whose surface a basic destination also opens, so
|
|
88
|
+
nothing is lost (the Merge / Service-best-practices palette entries into Workspace
|
|
89
|
+
settings, the local-models knob the Integrations hub already offers).
|
|
90
|
+
- **Out of the tier** — the sole route, hidden on purpose, so the capability is _absent_
|
|
91
|
+
from basic mode and the tier switch is the way to it (Sandbox, Kaizen, repo bootstrap,
|
|
92
|
+
and the deployment-wide operator + reports rollups).
|
|
93
|
+
|
|
94
|
+
Sole-route items stay in basic when the delivery loop runs on them: the pipeline builder,
|
|
95
|
+
add-from-repo, the fragment library, the infrastructure/PREnv windows, and the workspace /
|
|
96
|
+
model configuration a run actually reads. `nav-contributions.spec.ts` pins the advanced set
|
|
97
|
+
against a table naming each item's kind and reason, so promoting one forces that claim to be
|
|
98
|
+
written down rather than assumed.
|
|
99
|
+
|
|
94
100
|
- **A less-used option inside a surface** reads `useUiModeStore().isAdvanced`. Hide, never
|
|
95
101
|
disable, and only ever hide an OVERRIDE: what remains must be exactly the default the hidden
|
|
96
102
|
field would have shown, so a basic-mode user never gets different behaviour from an advanced
|
|
97
103
|
one — only fewer choices. An input nothing else supplies (the pipeline, the apriori branches)
|
|
98
104
|
stays in both tiers however advanced it feels.
|
|
105
|
+
- **A whole AUTHORING affordance** may be tier-scoped the same way — the frame header's
|
|
106
|
+
recurring-schedule and initiative buttons are advanced-only — but only while the tier hides
|
|
107
|
+
the ability to CREATE, never the ability to SEE. Existing state has to stay legible in basic
|
|
108
|
+
mode through its normal surfaces (a live schedule still badges its task card and opens its
|
|
109
|
+
inspector panel; an initiative is still a block on the board with its own inspector), or the
|
|
110
|
+
tier turns into a way for a user to be acted on by configuration they cannot find.
|
|
99
111
|
- **An override control on an EXISTING entity gates on `showOverrideField(isAdvanced, …values)`**
|
|
100
112
|
(`app/utils/uiMode.ts`) rather than on `isAdvanced` alone. Hiding an override is only safe
|
|
101
113
|
while it is unset — always true for a creation form, never guaranteed for a block that a
|
|
@@ -24,6 +24,7 @@ const agentRuns = useAgentRunsStore()
|
|
|
24
24
|
const services = useServicesStore()
|
|
25
25
|
const reviews = useReviewStage()
|
|
26
26
|
const access = useWorkspaceAccess()
|
|
27
|
+
const uiMode = useUiModeStore()
|
|
27
28
|
const { t } = useI18n()
|
|
28
29
|
const { lod } = useSemanticZoom()
|
|
29
30
|
// Coarse-pointer (touch) bumps the frame-header actions from `xs` to `sm` so
|
|
@@ -161,6 +162,13 @@ function addRecurring() {
|
|
|
161
162
|
ui.openAddRecurring(props.id)
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
// Hunt this service's tracker board for a bug worth picking up. Scoped to THIS frame, so
|
|
166
|
+
// an adopted candidate lands here rather than wherever the board's first frame happens to be.
|
|
167
|
+
function huntBugs() {
|
|
168
|
+
ui.expandFrame(props.id)
|
|
169
|
+
ui.openBugHunt(null, props.id)
|
|
170
|
+
}
|
|
171
|
+
|
|
164
172
|
function createInitiative() {
|
|
165
173
|
ui.expandFrame(props.id)
|
|
166
174
|
ui.openCreateInitiative(props.id)
|
|
@@ -491,8 +499,18 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
491
499
|
:title="t('board.frame.createTaskFromIssueTitle')"
|
|
492
500
|
@click.stop="createTaskFromIssue"
|
|
493
501
|
/>
|
|
502
|
+
<!-- Recurring pipelines + initiatives are ADVANCED-tier authoring: both plan
|
|
503
|
+
work rather than do it (a schedule that fires runs on a cadence, an
|
|
504
|
+
initiative that groups tasks under a goal), and the basic frame header is
|
|
505
|
+
the most-used control strip on the board. Hiding the CREATE affordance
|
|
506
|
+
removes neither's existing state from basic mode — a live schedule still
|
|
507
|
+
badges its task card and opens its inspector panel, and an initiative is
|
|
508
|
+
still a block on the board with its own inspector — so what a basic-mode
|
|
509
|
+
user loses is the ability to author a new one, not sight of one. -->
|
|
494
510
|
<UButton
|
|
511
|
+
v-if="uiMode.isAdvanced"
|
|
495
512
|
class="nodrag"
|
|
513
|
+
data-testid="frame-add-recurring"
|
|
496
514
|
:size="isTouch ? 'sm' : 'xs'"
|
|
497
515
|
variant="ghost"
|
|
498
516
|
color="neutral"
|
|
@@ -501,6 +519,7 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
501
519
|
@click.stop="addRecurring"
|
|
502
520
|
/>
|
|
503
521
|
<UButton
|
|
522
|
+
v-if="uiMode.isAdvanced"
|
|
504
523
|
class="nodrag"
|
|
505
524
|
data-testid="frame-add-initiative"
|
|
506
525
|
:size="isTouch ? 'sm' : 'xs'"
|
|
@@ -510,6 +529,17 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
510
529
|
:title="t('board.frame.createInitiativeTitle')"
|
|
511
530
|
@click.stop="createInitiative"
|
|
512
531
|
/>
|
|
532
|
+
<UButton
|
|
533
|
+
v-if="tasks.anyOffered"
|
|
534
|
+
class="nodrag"
|
|
535
|
+
data-testid="frame-hunt-bugs"
|
|
536
|
+
:size="isTouch ? 'sm' : 'xs'"
|
|
537
|
+
variant="ghost"
|
|
538
|
+
color="neutral"
|
|
539
|
+
icon="i-lucide-radar"
|
|
540
|
+
:title="t('board.frame.huntBugsTitle')"
|
|
541
|
+
@click.stop="huntBugs"
|
|
542
|
+
/>
|
|
513
543
|
</template>
|
|
514
544
|
<UButton
|
|
515
545
|
class="nodrag"
|
|
@@ -5,6 +5,7 @@ import { blockTypeMeta, STATUS_META } from '~/utils/catalog'
|
|
|
5
5
|
import { pipelineAllowedForManualStart } from '~/utils/pipeline'
|
|
6
6
|
import PipelineProgress from '~/components/pipeline/PipelineProgress.vue'
|
|
7
7
|
import IconButton from '~/components/common/IconButton.vue'
|
|
8
|
+
import { useInitiativePlanning } from '~/composables/useInitiativePlanning'
|
|
8
9
|
|
|
9
10
|
const board = useBoardStore()
|
|
10
11
|
const pipelines = usePipelinesStore()
|
|
@@ -44,6 +45,20 @@ function runPipeline(id: string) {
|
|
|
44
45
|
if (pipeline && block.value) void execution.start(block.value.id, pipeline)
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
/**
|
|
49
|
+
* An initiative block accepts exactly ONE pipeline — its preset's planning pipeline — and the
|
|
50
|
+
* engine refuses every other, so it gets the same single "Run planning" control the board card and
|
|
51
|
+
* the inspector offer rather than a picker whose every other row would be rejected on click. Same
|
|
52
|
+
* composable as those two surfaces, so which pipeline it starts can't drift across the three.
|
|
53
|
+
*/
|
|
54
|
+
const isInitiative = computed(() => block.value?.level === 'initiative')
|
|
55
|
+
const {
|
|
56
|
+
planningPipeline,
|
|
57
|
+
running: planningRunning,
|
|
58
|
+
starting: planningStarting,
|
|
59
|
+
runPlanning,
|
|
60
|
+
} = useInitiativePlanning(() => block.value?.id ?? '')
|
|
61
|
+
|
|
47
62
|
function close() {
|
|
48
63
|
ui.focus(null)
|
|
49
64
|
}
|
|
@@ -99,9 +114,28 @@ function openApprovalFor(approvalId: string) {
|
|
|
99
114
|
{{ statusMeta.label }}
|
|
100
115
|
</UBadge>
|
|
101
116
|
<div class="ms-auto flex items-center gap-2">
|
|
117
|
+
<!-- An initiative has one legal pipeline, so it gets the button, not the picker. -->
|
|
118
|
+
<UButton
|
|
119
|
+
v-if="isInitiative"
|
|
120
|
+
color="primary"
|
|
121
|
+
variant="soft"
|
|
122
|
+
size="sm"
|
|
123
|
+
icon="i-lucide-play"
|
|
124
|
+
:loading="planningStarting || planningRunning"
|
|
125
|
+
:disabled="!planningPipeline || planningRunning || planningStarting"
|
|
126
|
+
data-testid="focus-run-planning"
|
|
127
|
+
@click="runPlanning"
|
|
128
|
+
>
|
|
129
|
+
{{ t('initiative.inspector.runPlanning') }}
|
|
130
|
+
</UButton>
|
|
102
131
|
<!-- The rich picker rather than a list of names: the run starts the moment a row is
|
|
103
132
|
clicked, so the preview is the only chance to see which agents it will run. -->
|
|
104
|
-
<PipelinePicker
|
|
133
|
+
<PipelinePicker
|
|
134
|
+
v-else
|
|
135
|
+
model-value=""
|
|
136
|
+
:options="runOptions"
|
|
137
|
+
@update:model-value="runPipeline"
|
|
138
|
+
>
|
|
105
139
|
<template #trigger>
|
|
106
140
|
<UButton
|
|
107
141
|
color="primary"
|
|
@@ -127,6 +127,25 @@ async function flushThen(action: (id: string) => Promise<unknown>) {
|
|
|
127
127
|
|
|
128
128
|
const onContinue = () => flushThen((id) => initiatives.continuePlanning(id))
|
|
129
129
|
const onProceed = () => flushThen((id) => initiatives.proceedPlanning(id))
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The escape hatch for a planning run that stalled. It belongs HERE, not only in the inspector's
|
|
133
|
+
* execution panel behind this window: submit and plan-now are the two things that wedge, so the
|
|
134
|
+
* human who needs a way out is looking at exactly this footer. Offered whenever a run still owns
|
|
135
|
+
* the block — including mid-pass and after a failed pass, which is where a wedge actually shows up
|
|
136
|
+
* and where neither of the other two buttons is even rendered.
|
|
137
|
+
*
|
|
138
|
+
* Discarding returns the block to `planned`, which re-enables "Run planning"; the interviewer gate
|
|
139
|
+
* drops the previous run's round bookkeeping on that fresh start, so the re-run genuinely
|
|
140
|
+
* re-interviews instead of force-converging on its first pass. Close on success — leaving the
|
|
141
|
+
* window open on the now-empty idle state would read as another dead end.
|
|
142
|
+
*/
|
|
143
|
+
const { resetting, resetRun } = useRunReset()
|
|
144
|
+
const canDiscard = computed(() => !!block.value?.executionId)
|
|
145
|
+
async function onDiscard() {
|
|
146
|
+
if (!blockId.value) return
|
|
147
|
+
if (await resetRun(blockId.value)) close()
|
|
148
|
+
}
|
|
130
149
|
</script>
|
|
131
150
|
|
|
132
151
|
<template>
|
|
@@ -221,28 +240,51 @@ const onProceed = () => flushThen((id) => initiatives.proceedPlanning(id))
|
|
|
221
240
|
</template>
|
|
222
241
|
</div>
|
|
223
242
|
|
|
224
|
-
<!-- Action rail.
|
|
225
|
-
re-submit a question set already in flight, and the resume is a
|
|
243
|
+
<!-- Action rail. The submit/plan-now pair shows only while the run is actually parked on the
|
|
244
|
+
human: mid-pass they would re-submit a question set already in flight, and the resume is a
|
|
245
|
+
no-op once it isn't. Discard is the opposite — it is offered for as long as a run owns the
|
|
246
|
+
block, because the phases where those two are hidden (working, failed) are exactly the ones
|
|
247
|
+
a wedged run sits in. -->
|
|
226
248
|
<footer
|
|
227
|
-
v-if="initiative && phase === 'awaiting' && questions.length > 0"
|
|
249
|
+
v-if="initiative && (canDiscard || (phase === 'awaiting' && questions.length > 0))"
|
|
228
250
|
class="flex items-center justify-between gap-3 border-t border-slate-800 px-5 py-3"
|
|
229
251
|
>
|
|
230
|
-
<
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
252
|
+
<UButton
|
|
253
|
+
v-if="canDiscard"
|
|
254
|
+
color="error"
|
|
255
|
+
variant="ghost"
|
|
256
|
+
size="sm"
|
|
257
|
+
icon="i-lucide-trash-2"
|
|
258
|
+
:loading="resetting"
|
|
259
|
+
:disabled="resuming"
|
|
260
|
+
:title="t('initiative.planning.discardTitle')"
|
|
261
|
+
data-testid="initiative-planning-discard"
|
|
262
|
+
@click="onDiscard"
|
|
263
|
+
>
|
|
264
|
+
{{ t('initiative.planning.discard') }}
|
|
265
|
+
</UButton>
|
|
266
|
+
<!-- `ms-auto` rather than relying on `justify-between`: discard is conditional, and without
|
|
267
|
+
it this group left-aligns on the (transient) render where it is the only child. -->
|
|
268
|
+
<div
|
|
269
|
+
v-if="phase === 'awaiting' && questions.length > 0"
|
|
270
|
+
class="ms-auto flex items-center gap-2"
|
|
271
|
+
>
|
|
272
|
+
<p class="text-[11px] text-slate-500">
|
|
273
|
+
<span
|
|
274
|
+
v-if="unanswered > 0"
|
|
275
|
+
class="text-amber-400/90"
|
|
276
|
+
data-testid="initiative-planning-unanswered"
|
|
277
|
+
>
|
|
278
|
+
{{ t('initiative.planning.unanswered', { count: unanswered }) }}
|
|
279
|
+
</span>
|
|
280
|
+
<span v-else>{{ t('initiative.planning.hint') }}</span>
|
|
281
|
+
</p>
|
|
241
282
|
<UButton
|
|
242
283
|
color="neutral"
|
|
243
284
|
variant="ghost"
|
|
244
285
|
size="sm"
|
|
245
286
|
:loading="resuming"
|
|
287
|
+
:disabled="resetting"
|
|
246
288
|
:title="t('initiative.planning.proceedTitle')"
|
|
247
289
|
data-testid="initiative-planning-proceed"
|
|
248
290
|
@click="onProceed"
|
|
@@ -253,7 +295,7 @@ const onProceed = () => flushThen((id) => initiatives.proceedPlanning(id))
|
|
|
253
295
|
color="primary"
|
|
254
296
|
size="sm"
|
|
255
297
|
:loading="resuming"
|
|
256
|
-
:disabled="unanswered > 0"
|
|
298
|
+
:disabled="unanswered > 0 || resetting"
|
|
257
299
|
:title="
|
|
258
300
|
unanswered > 0
|
|
259
301
|
? t('initiative.planning.unanswered', { count: unanswered })
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { showOverrideField } from '~/utils/uiMode'
|
|
3
|
+
|
|
2
4
|
// The Integrations hub: a single modal that lists every external system the WORKSPACE can
|
|
3
5
|
// enable or link in. Each row reuses the existing per-integration panel handlers on the `ui`
|
|
4
6
|
// store (so the integrations themselves are unchanged); opening one closes the hub and
|
|
@@ -25,6 +27,7 @@ const publicApiKeys = usePublicApiKeysStore()
|
|
|
25
27
|
const userSecrets = useUserSecretsStore()
|
|
26
28
|
const apiKeys = useApiKeysStore()
|
|
27
29
|
const workspace = useWorkspaceStore()
|
|
30
|
+
const uiMode = useUiModeStore()
|
|
28
31
|
|
|
29
32
|
// True when the per-user "My setup" hub is reachable (UserMenu renders only when signed in).
|
|
30
33
|
// When false (auth disabled / local mode) we fold the personal rows back into this hub so
|
|
@@ -268,7 +271,17 @@ const groups = computed<IntegrationGroup[]>(() => {
|
|
|
268
271
|
// Gated like every other backend-toggleable system: hidden until a probe confirms
|
|
269
272
|
// the observability module is enabled (`available === true`), so a disabled backend
|
|
270
273
|
// doesn't show a dead "Connect" row that only 503s.
|
|
271
|
-
|
|
274
|
+
//
|
|
275
|
+
// Also an ADVANCED-tier row. Post-release health is the one integration here that acts
|
|
276
|
+
// AFTER delivery rather than during it — it watches monitors once a release ships and can
|
|
277
|
+
// spawn an on-call agent — so it sits outside the everyday loop the basic tier serves.
|
|
278
|
+
// Gated with `showOverrideField` rather than a bare `isAdvanced`: an already-connected
|
|
279
|
+
// Datadog is live behaviour on this workspace, and a tier must never conceal a connection a
|
|
280
|
+
// basic-mode user would then have no way to inspect or disconnect.
|
|
281
|
+
if (
|
|
282
|
+
releaseHealth.available &&
|
|
283
|
+
showOverrideField(uiMode.isAdvanced, releaseHealth.connection.connected || null)
|
|
284
|
+
) {
|
|
272
285
|
out.push({
|
|
273
286
|
title: t('layout.integrationsHub.groups.observability'),
|
|
274
287
|
items: [
|
|
@@ -13,7 +13,6 @@ const pipelines = usePipelinesStore()
|
|
|
13
13
|
const execution = useExecutionStore()
|
|
14
14
|
const ui = useUiStore()
|
|
15
15
|
const documents = useDocumentsStore()
|
|
16
|
-
const tasks = useTasksStore()
|
|
17
16
|
const fragments = useFragmentsStore()
|
|
18
17
|
const agentRuns = useAgentRunsStore()
|
|
19
18
|
const github = useGitHubStore()
|
|
@@ -60,6 +59,14 @@ watch(
|
|
|
60
59
|
)
|
|
61
60
|
const isContainer = computed(() => level.value === 'frame' || level.value === 'module')
|
|
62
61
|
const isTask = computed(() => level.value === 'task')
|
|
62
|
+
const isInitiative = computed(() => level.value === 'initiative')
|
|
63
|
+
/**
|
|
64
|
+
* Blocks whose inspector carries a pipeline RUN — a task, and an initiative (whose planning
|
|
65
|
+
* pipeline is an ordinary run of ordinary agent steps). Both get the execution panel and the
|
|
66
|
+
* Focus view; what differs is only how the run is STARTED (a task picks any pipeline, an
|
|
67
|
+
* initiative may only run its planning one, so it keeps its own "Run planning" control).
|
|
68
|
+
*/
|
|
69
|
+
const hasRuns = computed(() => isTask.value || isInitiative.value)
|
|
63
70
|
|
|
64
71
|
const instance = computed(() => execution.getInstance(block.value?.executionId))
|
|
65
72
|
const typeMeta = computed(() => (block.value ? blockTypeMeta(block.value.type) : null))
|
|
@@ -104,7 +111,10 @@ const runBlockedReason = computed(() => {
|
|
|
104
111
|
const canRun = computed(() => runnable.value && access.canExecuteRuns.value)
|
|
105
112
|
|
|
106
113
|
// The delete control names what it removes, so selecting a task and deleting it
|
|
107
|
-
// reads as "Delete task" rather than ambiguously removing the whole service.
|
|
114
|
+
// reads as "Delete task" rather than ambiguously removing the whole service. An
|
|
115
|
+
// initiative is its own level (it hangs off a frame like a module does), so it must
|
|
116
|
+
// name ITSELF — offering to "delete service" there describes the wrong blast radius
|
|
117
|
+
// entirely: the frame and every other thing under it survive.
|
|
108
118
|
const deleteLabel = computed(() =>
|
|
109
119
|
schedule.value
|
|
110
120
|
? t('panels.inspector.deleteRecurringPipeline')
|
|
@@ -112,7 +122,9 @@ const deleteLabel = computed(() =>
|
|
|
112
122
|
? t('panels.inspector.deleteTask')
|
|
113
123
|
: level.value === 'module'
|
|
114
124
|
? t('panels.inspector.deleteModule')
|
|
115
|
-
:
|
|
125
|
+
: isInitiative.value
|
|
126
|
+
? t('panels.inspector.deleteInitiative')
|
|
127
|
+
: t('panels.inspector.deleteService'),
|
|
116
128
|
)
|
|
117
129
|
|
|
118
130
|
// A task is "started" once a pipeline has been launched on it (it has an
|
|
@@ -437,33 +449,9 @@ const showOriginalDescription = ref(false)
|
|
|
437
449
|
>
|
|
438
450
|
{{ block!.pullRequest!.branch }}
|
|
439
451
|
</UButton>
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
variant="soft"
|
|
444
|
-
size="xs"
|
|
445
|
-
icon="i-lucide-ticket"
|
|
446
|
-
@click="ui.openTaskImport()"
|
|
447
|
-
>
|
|
448
|
-
{{
|
|
449
|
-
tasks.anyOffered
|
|
450
|
-
? t('panels.inspector.importIssue')
|
|
451
|
-
: t('panels.inspector.connectTracker')
|
|
452
|
-
}}
|
|
453
|
-
</UButton>
|
|
454
|
-
<!-- Hunt this service's tracker board for a bug worth picking up. Scoped to the
|
|
455
|
-
selected container, so an adopted candidate lands here rather than wherever the
|
|
456
|
-
board's first frame happens to be. -->
|
|
457
|
-
<UButton
|
|
458
|
-
v-if="isContainer && tasks.anyOffered"
|
|
459
|
-
color="neutral"
|
|
460
|
-
variant="soft"
|
|
461
|
-
size="xs"
|
|
462
|
-
icon="i-lucide-radar"
|
|
463
|
-
@click="ui.openBugHunt(null, block!.id)"
|
|
464
|
-
>
|
|
465
|
-
{{ t('panels.inspector.huntBugs') }}
|
|
466
|
-
</UButton>
|
|
452
|
+
<!-- Tracker entry points (import an issue, hunt bugs) live on the service frame's
|
|
453
|
+
header, where they are already scoped to a frame — the inspector doesn't
|
|
454
|
+
duplicate them. -->
|
|
467
455
|
<UButton
|
|
468
456
|
v-if="isContainer && documents.available && documents.anyConnected"
|
|
469
457
|
color="neutral"
|
|
@@ -543,11 +531,12 @@ const showOriginalDescription = ref(false)
|
|
|
543
531
|
</UButton>
|
|
544
532
|
</UDropdownMenu>
|
|
545
533
|
<UButton
|
|
546
|
-
v-if="
|
|
534
|
+
v-if="hasRuns"
|
|
547
535
|
color="neutral"
|
|
548
536
|
variant="soft"
|
|
549
537
|
size="sm"
|
|
550
538
|
icon="i-lucide-maximize-2"
|
|
539
|
+
data-testid="inspector-focus"
|
|
551
540
|
@click="ui.focus(block.id)"
|
|
552
541
|
>
|
|
553
542
|
{{ t('panels.inspector.focus') }}
|
|
@@ -22,72 +22,64 @@ function addTask() {
|
|
|
22
22
|
</script>
|
|
23
23
|
|
|
24
24
|
<template>
|
|
25
|
+
<!-- Modules and tasks are two peer collapsible lists, not one "Contents" group: the
|
|
26
|
+
grouping added a level of nesting without adding information, and a service's task
|
|
27
|
+
list is long enough to want collapsing independently of its modules. -->
|
|
25
28
|
<InspectorSection
|
|
26
|
-
|
|
27
|
-
:
|
|
28
|
-
:
|
|
29
|
+
v-if="modules.length"
|
|
30
|
+
:title="t('inspector.container.modulesTitle')"
|
|
31
|
+
:hint="t('inspector.container.modulesHint')"
|
|
32
|
+
:count="modules.length"
|
|
29
33
|
default-open
|
|
30
34
|
>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
)
|
|
51
|
-
}}</span>
|
|
52
|
-
</li>
|
|
53
|
-
</ul>
|
|
54
|
-
</div>
|
|
35
|
+
<ul class="space-y-1">
|
|
36
|
+
<li
|
|
37
|
+
v-for="m in modules"
|
|
38
|
+
:key="m.id"
|
|
39
|
+
class="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1 hover:bg-slate-800/60"
|
|
40
|
+
@click="ui.select(m.id)"
|
|
41
|
+
>
|
|
42
|
+
<UIcon name="i-lucide-package" class="h-3.5 w-3.5 text-violet-400" />
|
|
43
|
+
<span class="truncate text-xs text-slate-200">{{ m.title }}</span>
|
|
44
|
+
<span class="ms-auto text-[10px] text-slate-500">{{
|
|
45
|
+
t(
|
|
46
|
+
'inspector.container.taskCount',
|
|
47
|
+
{ count: board.tasksOf(m.id).length },
|
|
48
|
+
board.tasksOf(m.id).length,
|
|
49
|
+
)
|
|
50
|
+
}}</span>
|
|
51
|
+
</li>
|
|
52
|
+
</ul>
|
|
53
|
+
</InspectorSection>
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
class="
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
<span class="ms-auto text-[10px] text-slate-500">{{
|
|
85
|
-
STATUS_META[task.status].label
|
|
86
|
-
}}</span>
|
|
87
|
-
</li>
|
|
88
|
-
</ul>
|
|
89
|
-
<div v-else class="text-[11px] text-slate-500">{{ t('inspector.container.noTasks') }}</div>
|
|
90
|
-
</div>
|
|
55
|
+
<InspectorSection
|
|
56
|
+
:title="isFrame ? t('inspector.container.allTasksTitle') : t('inspector.container.tasksTitle')"
|
|
57
|
+
:hint="t('inspector.container.tasksHint')"
|
|
58
|
+
:count="tasks.length"
|
|
59
|
+
default-open
|
|
60
|
+
>
|
|
61
|
+
<template #actions>
|
|
62
|
+
<ReviewDebtBadge />
|
|
63
|
+
<UButton size="xs" variant="soft" color="primary" icon="i-lucide-plus" @click="addTask">
|
|
64
|
+
{{ t('inspector.container.addTask') }}
|
|
65
|
+
</UButton>
|
|
66
|
+
</template>
|
|
67
|
+
<ul v-if="tasks.length" class="space-y-1">
|
|
68
|
+
<li
|
|
69
|
+
v-for="task in tasks"
|
|
70
|
+
:key="task.id"
|
|
71
|
+
class="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1 hover:bg-slate-800/60"
|
|
72
|
+
@click="ui.select(task.id)"
|
|
73
|
+
>
|
|
74
|
+
<span
|
|
75
|
+
class="h-2 w-2 shrink-0 rounded-full"
|
|
76
|
+
:style="{ backgroundColor: STATUS_META[task.status].color }"
|
|
77
|
+
/>
|
|
78
|
+
<span class="truncate text-xs text-slate-200">{{ task.title }}</span>
|
|
79
|
+
<span class="ms-auto text-[10px] text-slate-500">{{ STATUS_META[task.status].label }}</span>
|
|
80
|
+
</li>
|
|
81
|
+
</ul>
|
|
82
|
+
<div v-else class="text-[11px] text-slate-500">{{ t('inspector.container.noTasks') }}</div>
|
|
91
83
|
<p v-if="isFrame" class="text-[11px] text-slate-500">
|
|
92
84
|
{{ t('inspector.container.servicesHint') }}
|
|
93
85
|
</p>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { computed, reactive, ref, watch } from 'vue'
|
|
3
3
|
import type { Block } from '~/types/domain'
|
|
4
4
|
import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
|
|
5
|
+
import { showOverrideField } from '~/utils/uiMode'
|
|
5
6
|
|
|
6
7
|
// Per-service (frame) post-release-health mapping: which observability monitors/SLOs the
|
|
7
8
|
// `post-release-health` gate watches after this service's PRs ship. Keyed by THIS block's
|
|
@@ -11,6 +12,7 @@ const props = defineProps<{ block: Block }>()
|
|
|
11
12
|
|
|
12
13
|
const store = useReleaseHealthStore()
|
|
13
14
|
const ui = useUiStore()
|
|
15
|
+
const uiMode = useUiModeStore()
|
|
14
16
|
const toast = useToast()
|
|
15
17
|
const { t } = useI18n()
|
|
16
18
|
const { confirmAction, toastDone } = useConfirmAction()
|
|
@@ -21,6 +23,14 @@ const draft = reactive({ monitorIds: '', sloIds: '', envTag: '' })
|
|
|
21
23
|
const connected = computed(() => store.connection.connected)
|
|
22
24
|
const saved = computed(() => store.configForBlock(props.block.id))
|
|
23
25
|
|
|
26
|
+
// Post-release health is an ADVANCED-tier concern: with no mapping the gate is a pass-through,
|
|
27
|
+
// so basic mode hides the whole section rather than showing a service-level knob whose absence
|
|
28
|
+
// changes nothing. `showOverrideField` (not a bare `isAdvanced`) because the mapping is per
|
|
29
|
+
// SERVICE and durable — once a teammate on the advanced tier maps monitors onto this frame,
|
|
30
|
+
// hiding the panel would leave a basic-mode user with releases being watched (and an on-call
|
|
31
|
+
// agent that can spawn) by a configuration they can neither see nor clear.
|
|
32
|
+
const show = computed(() => showOverrideField(uiMode.isAdvanced, saved.value))
|
|
33
|
+
|
|
24
34
|
function parseIds(csv: string): string[] {
|
|
25
35
|
return csv
|
|
26
36
|
.split(',')
|
|
@@ -91,6 +101,7 @@ async function clear() {
|
|
|
91
101
|
|
|
92
102
|
<template>
|
|
93
103
|
<InspectorSection
|
|
104
|
+
v-if="show"
|
|
94
105
|
:title="t('inspector.releaseHealth.title')"
|
|
95
106
|
:hint="t('inspector.releaseHealth.sectionHint')"
|
|
96
107
|
>
|
|
@@ -192,26 +192,11 @@ async function stopRun() {
|
|
|
192
192
|
stopping.value = false
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const ok = await confirm({
|
|
201
|
-
title: t('inspector.execution.resetConfirm.title'),
|
|
202
|
-
description: t('inspector.execution.resetConfirm.body'),
|
|
203
|
-
variant: 'destructive',
|
|
204
|
-
confirmLabel: t('inspector.execution.resetConfirm.confirm'),
|
|
205
|
-
icon: 'i-lucide-trash-2',
|
|
206
|
-
})
|
|
207
|
-
if (!ok) return
|
|
208
|
-
resetting.value = true
|
|
209
|
-
try {
|
|
210
|
-
await execution.cancel(props.block.id)
|
|
211
|
-
} finally {
|
|
212
|
-
resetting.value = false
|
|
213
|
-
}
|
|
214
|
-
}
|
|
195
|
+
// Destructive: discards the run and returns the block to planned, behind a confirm. Shared with
|
|
196
|
+
// the initiative planning window (which offers the same escape hatch in place) so the two can't
|
|
197
|
+
// drift on the prompt or on what "discard" means.
|
|
198
|
+
const { resetting, resetRun: discardRun } = useRunReset()
|
|
199
|
+
const resetRun = () => discardRun(props.block.id)
|
|
215
200
|
|
|
216
201
|
/**
|
|
217
202
|
* The reviewer-effort tag for this merge, preselected from evidence rather than starting blank: if
|
|
@@ -76,7 +76,14 @@ export function useBlockDeletion() {
|
|
|
76
76
|
? 'task'
|
|
77
77
|
: block.level === 'module'
|
|
78
78
|
? 'module'
|
|
79
|
-
:
|
|
79
|
+
: // An initiative names itself rather than falling through to the service copy, which
|
|
80
|
+
// would describe a blast radius orders of magnitude larger than the real one. Its
|
|
81
|
+
// cascade is also genuinely different from a container's: the plan goes with it, but
|
|
82
|
+
// the tasks its loop already spawned are NOT descendants — the backend only detaches
|
|
83
|
+
// their membership link — so the count branch below deliberately doesn't apply.
|
|
84
|
+
block.level === 'initiative'
|
|
85
|
+
? 'initiative'
|
|
86
|
+
: 'service'
|
|
80
87
|
const title = t(`panels.inspector.confirmDelete.${kind}.title`)
|
|
81
88
|
// For a container (service/module) state the exact cascade size so the blast radius is
|
|
82
89
|
// explicit — "and everything inside it" hides how many tasks/modules go with it.
|