@cat-factory/app 0.128.0 → 0.130.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/app/components/board/AgentFailureCard.vue +3 -1
- package/app/components/board/AgentStopButton.vue +3 -0
- package/app/components/board/BoardCanvas.vue +16 -3
- package/app/components/board/RecurringPipelineModal.vue +7 -1
- package/app/components/board/nodes/BlockNode.vue +47 -41
- package/app/components/brainstorm/BrainstormWindow.vue +25 -5
- package/app/components/clarity/ClarityReviewWindow.vue +17 -4
- package/app/components/docs/DocInterviewWindow.vue +5 -1
- package/app/components/followUp/FollowUpWindow.vue +15 -1
- package/app/components/forkDecision/ForkDecisionWindow.vue +7 -2
- package/app/components/gates/GateResultView.vue +7 -1
- package/app/components/humanTest/HumanTestWindow.vue +11 -5
- package/app/components/layout/BoardSwitcher.vue +21 -12
- package/app/components/layout/BoardToolbar.vue +8 -4
- package/app/components/layout/CommandBar.vue +137 -123
- package/app/components/layout/NotificationsInbox.vue +5 -1
- package/app/components/layout/SideBar.vue +157 -115
- package/app/components/layout/SpendWarningBanner.vue +3 -0
- package/app/components/panels/InspectorPanel.vue +23 -10
- package/app/components/panels/inspector/TaskExecution.vue +13 -4
- package/app/components/prReview/PrReviewWindow.vue +7 -3
- package/app/components/requirements/RequirementsReviewWindow.vue +33 -5
- package/app/components/settings/ApiTokensPanel.vue +18 -0
- package/app/components/visualConfirm/VisualConfirmationWindow.vue +7 -3
- package/app/composables/useBlockDeletion.ts +7 -0
- package/app/composables/useBlockDrag.ts +5 -0
- package/app/composables/useFrameResize.ts +4 -0
- package/app/composables/useWorkspaceAccess.spec.ts +102 -0
- package/app/composables/useWorkspaceAccess.ts +84 -0
- package/app/stores/publicApiKeys.spec.ts +1 -0
- package/app/stores/workspace.spec.ts +19 -0
- package/app/stores/workspace.ts +27 -7
- package/app/types/domain.ts +6 -0
- package/i18n/locales/de.json +7 -0
- package/i18n/locales/en.json +10 -0
- package/i18n/locales/es.json +7 -0
- package/i18n/locales/fr.json +7 -0
- package/i18n/locales/he.json +7 -0
- package/i18n/locales/it.json +7 -0
- package/i18n/locales/ja.json +7 -0
- package/i18n/locales/pl.json +7 -0
- package/i18n/locales/tr.json +7 -0
- package/i18n/locales/uk.json +7 -0
- package/package.json +2 -2
|
@@ -19,6 +19,7 @@ const board = useBoardStore()
|
|
|
19
19
|
const execution = useExecutionStore()
|
|
20
20
|
const visualConfirm = useVisualConfirmStore()
|
|
21
21
|
const { t } = useI18n()
|
|
22
|
+
const access = useWorkspaceAccess()
|
|
22
23
|
|
|
23
24
|
// Per-window blob cache; release the cached screenshot/reference object URLs when the window
|
|
24
25
|
// goes away, so the (potentially large) blob bytes don't linger for the rest of the session.
|
|
@@ -351,7 +352,8 @@ const canApprove = computed(
|
|
|
351
352
|
color="warning"
|
|
352
353
|
icon="i-lucide-wrench"
|
|
353
354
|
:loading="busy"
|
|
354
|
-
:disabled="busy || !hasFindings"
|
|
355
|
+
:disabled="busy || !hasFindings || !access.canExecuteRuns.value"
|
|
356
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
355
357
|
@click="submitFix"
|
|
356
358
|
>
|
|
357
359
|
{{ t('visualConfirm.requestFix.send') }}
|
|
@@ -430,7 +432,8 @@ const canApprove = computed(
|
|
|
430
432
|
color="neutral"
|
|
431
433
|
icon="i-lucide-refresh-cw"
|
|
432
434
|
:loading="busy"
|
|
433
|
-
:disabled="busy || !awaitingHuman"
|
|
435
|
+
:disabled="busy || !awaitingHuman || !access.canExecuteRuns.value"
|
|
436
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
434
437
|
@click="recapture"
|
|
435
438
|
>
|
|
436
439
|
{{ t('visualConfirm.recapture') }}
|
|
@@ -439,7 +442,8 @@ const canApprove = computed(
|
|
|
439
442
|
color="primary"
|
|
440
443
|
icon="i-lucide-circle-check"
|
|
441
444
|
:loading="busy"
|
|
442
|
-
:disabled="!canApprove"
|
|
445
|
+
:disabled="!canApprove || !access.canExecuteRuns.value"
|
|
446
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
443
447
|
@click="approve"
|
|
444
448
|
>
|
|
445
449
|
{{ t('visualConfirm.approve') }}
|
|
@@ -18,6 +18,7 @@ export function useBlockDeletion() {
|
|
|
18
18
|
const toast = useToast()
|
|
19
19
|
const { confirm } = useConfirm()
|
|
20
20
|
const { t } = useI18n()
|
|
21
|
+
const access = useWorkspaceAccess()
|
|
21
22
|
|
|
22
23
|
/** A service is any top-level frame; only services are archivable. */
|
|
23
24
|
function isService(block: Block): boolean {
|
|
@@ -37,6 +38,9 @@ export function useBlockDeletion() {
|
|
|
37
38
|
*/
|
|
38
39
|
async function archiveBlock(block: Block | undefined | null): Promise<boolean> {
|
|
39
40
|
if (!block || !isService(block)) return false
|
|
41
|
+
// Archiving is a `board.write` mutation — a read-only viewer's keyboard/inspector path
|
|
42
|
+
// no-ops (the inspector button is disabled for them; this guards the shortcut too).
|
|
43
|
+
if (!access.canWriteBoard.value) return false
|
|
40
44
|
const ok = await confirm({
|
|
41
45
|
title: t('panels.inspector.confirmArchive.title'),
|
|
42
46
|
description: t('panels.inspector.confirmArchive.body', { name: block.title }),
|
|
@@ -94,6 +98,9 @@ export function useBlockDeletion() {
|
|
|
94
98
|
|
|
95
99
|
async function deleteBlock(block: Block | undefined | null): Promise<boolean> {
|
|
96
100
|
if (!block) return false
|
|
101
|
+
// Deletion is a `board.write` mutation — no-op for a read-only viewer (guards both the
|
|
102
|
+
// inspector Delete button and the global Delete-key shortcut, which share this path).
|
|
103
|
+
if (!access.canWriteBoard.value) return false
|
|
97
104
|
// A service with unfinished work can't be deleted (the backend rejects it) — archive it
|
|
98
105
|
// instead. Route straight to the archive flow so the user is never handed a dead-end error.
|
|
99
106
|
if (isService(block) && unfinishedTaskCount(block) > 0) return archiveBlock(block)
|
|
@@ -18,6 +18,7 @@ const draggingId = ref<string | null>(null)
|
|
|
18
18
|
export function useBlockDrag() {
|
|
19
19
|
const board = useBoardStore()
|
|
20
20
|
const ui = useUiStore()
|
|
21
|
+
const access = useWorkspaceAccess()
|
|
21
22
|
|
|
22
23
|
function startDrag(
|
|
23
24
|
block: Block,
|
|
@@ -25,6 +26,10 @@ export function useBlockDrag() {
|
|
|
25
26
|
opts: { reparent?: boolean; clamp?: boolean } = {},
|
|
26
27
|
) {
|
|
27
28
|
if (e.button !== 0) return
|
|
29
|
+
// Read-only viewers can pan/inspect but never move or reparent a block — the drag
|
|
30
|
+
// is a `board.write` mutation, so it no-ops for them (the SPA mirror of the backend
|
|
31
|
+
// member floor; the affordance itself is hidden/disabled at the button level too).
|
|
32
|
+
if (!access.canWriteBoard.value) return
|
|
28
33
|
e.preventDefault()
|
|
29
34
|
e.stopPropagation()
|
|
30
35
|
const startX = e.clientX
|
|
@@ -12,6 +12,7 @@ import type { Block } from '~/types/domain'
|
|
|
12
12
|
export function useFrameResize() {
|
|
13
13
|
const board = useBoardStore()
|
|
14
14
|
const ui = useUiStore()
|
|
15
|
+
const access = useWorkspaceAccess()
|
|
15
16
|
/** Id of the frame currently being resized, for cursor/handle styling. */
|
|
16
17
|
const resizingId = ref<string | null>(null)
|
|
17
18
|
|
|
@@ -21,6 +22,9 @@ export function useFrameResize() {
|
|
|
21
22
|
*/
|
|
22
23
|
function startResize(block: Block, e: PointerEvent, edge: 'e' | 's' | 'se') {
|
|
23
24
|
if (e.button !== 0) return
|
|
25
|
+
// Resizing a frame persists its size — a `board.write` mutation, so a read-only
|
|
26
|
+
// viewer's resize no-ops (the grips are hidden for them at the component level).
|
|
27
|
+
if (!access.canWriteBoard.value) return
|
|
24
28
|
e.preventDefault()
|
|
25
29
|
e.stopPropagation()
|
|
26
30
|
const startX = e.clientX
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import type { WorkspaceAccess } from '~/types/domain'
|
|
3
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
4
|
+
import { useWorkspaceAccess } from '~/composables/useWorkspaceAccess'
|
|
5
|
+
|
|
6
|
+
// `useWorkspaceAccess` reads the active board's resolved `{ role, permissions }` off the
|
|
7
|
+
// workspace store and answers "can the caller do X here". The store is real (a fresh Pinia
|
|
8
|
+
// per test via test/setup.ts); the composable calls it through the Nuxt auto-import, so
|
|
9
|
+
// expose it as a global. No API/i18n is touched by the composable.
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.stubGlobal('useWorkspaceStore', useWorkspaceStore)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
/** Set the active board's access on the store, then build the composable over it. */
|
|
15
|
+
function withAccess(access: WorkspaceAccess | null) {
|
|
16
|
+
useWorkspaceStore().access = access
|
|
17
|
+
return useWorkspaceAccess()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('useWorkspaceAccess', () => {
|
|
21
|
+
it('dev-open (no access resolved) allows everything — backend parity', () => {
|
|
22
|
+
const a = withAccess(null)
|
|
23
|
+
expect(a.role.value).toBeNull()
|
|
24
|
+
expect(a.permissions.value).toBeNull()
|
|
25
|
+
expect(a.can('board.write')).toBe(true)
|
|
26
|
+
expect(a.can('members.manage')).toBe(true)
|
|
27
|
+
expect(a.canWriteBoard.value).toBe(true)
|
|
28
|
+
expect(a.canExecuteRuns.value).toBe(true)
|
|
29
|
+
expect(a.canManageSettings.value).toBe(true)
|
|
30
|
+
// dev-open is NOT a viewer — it sees all, so isViewer is false and isMember/isAdmin true.
|
|
31
|
+
expect(a.isViewer.value).toBe(false)
|
|
32
|
+
expect(a.isMember.value).toBe(true)
|
|
33
|
+
expect(a.isAdmin.value).toBe(true)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('viewer can only read', () => {
|
|
37
|
+
const a = withAccess({ role: 'viewer', permissions: ['workspace.read'] })
|
|
38
|
+
expect(a.role.value).toBe('viewer')
|
|
39
|
+
expect(a.can('workspace.read')).toBe(true)
|
|
40
|
+
expect(a.can('board.write')).toBe(false)
|
|
41
|
+
expect(a.can('runs.execute')).toBe(false)
|
|
42
|
+
expect(a.canWriteBoard.value).toBe(false)
|
|
43
|
+
expect(a.canExecuteRuns.value).toBe(false)
|
|
44
|
+
expect(a.isViewer.value).toBe(true)
|
|
45
|
+
expect(a.isMember.value).toBe(false)
|
|
46
|
+
expect(a.isAdmin.value).toBe(false)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('member can write the board and execute runs but not manage', () => {
|
|
50
|
+
const a = withAccess({
|
|
51
|
+
role: 'member',
|
|
52
|
+
permissions: ['workspace.read', 'board.write', 'runs.execute'],
|
|
53
|
+
})
|
|
54
|
+
expect(a.canWriteBoard.value).toBe(true)
|
|
55
|
+
expect(a.canExecuteRuns.value).toBe(true)
|
|
56
|
+
expect(a.canManageSettings.value).toBe(false)
|
|
57
|
+
expect(a.canManageIntegrations.value).toBe(false)
|
|
58
|
+
expect(a.canManageSecrets.value).toBe(false)
|
|
59
|
+
expect(a.canManageMembers.value).toBe(false)
|
|
60
|
+
expect(a.isViewer.value).toBe(false)
|
|
61
|
+
expect(a.isMember.value).toBe(true)
|
|
62
|
+
expect(a.isAdmin.value).toBe(false)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('admin holds every permission', () => {
|
|
66
|
+
const a = withAccess({
|
|
67
|
+
role: 'admin',
|
|
68
|
+
permissions: [
|
|
69
|
+
'workspace.read',
|
|
70
|
+
'board.write',
|
|
71
|
+
'runs.execute',
|
|
72
|
+
'settings.manage',
|
|
73
|
+
'integrations.manage',
|
|
74
|
+
'secrets.manage',
|
|
75
|
+
'members.manage',
|
|
76
|
+
],
|
|
77
|
+
})
|
|
78
|
+
expect(a.canManageSettings.value).toBe(true)
|
|
79
|
+
expect(a.canManageIntegrations.value).toBe(true)
|
|
80
|
+
expect(a.canManageSecrets.value).toBe(true)
|
|
81
|
+
expect(a.canManageMembers.value).toBe(true)
|
|
82
|
+
expect(a.isAdmin.value).toBe(true)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('can() reads the granted permission SET, not the role name', () => {
|
|
86
|
+
// A future upgrade-overlay/custom grant could carry a permission beyond the role's
|
|
87
|
+
// default set; `can()` must honour the array the backend actually sent.
|
|
88
|
+
const a = withAccess({ role: 'member', permissions: ['workspace.read', 'runs.execute'] })
|
|
89
|
+
expect(a.can('board.write')).toBe(false)
|
|
90
|
+
expect(a.can('runs.execute')).toBe(true)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('reacts to a live access change (board switch / snapshot refresh)', () => {
|
|
94
|
+
const a = withAccess({ role: 'viewer', permissions: ['workspace.read'] })
|
|
95
|
+
expect(a.canWriteBoard.value).toBe(false)
|
|
96
|
+
useWorkspaceStore().access = {
|
|
97
|
+
role: 'member',
|
|
98
|
+
permissions: ['workspace.read', 'board.write', 'runs.execute'],
|
|
99
|
+
}
|
|
100
|
+
expect(a.canWriteBoard.value).toBe(true)
|
|
101
|
+
})
|
|
102
|
+
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { computed } from 'vue'
|
|
2
|
+
import type { WorkspacePermission, WorkspaceRole } from '~/types/domain'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The signed-in caller's resolved workspace-RBAC access to the ACTIVE board — the
|
|
6
|
+
* central helper for gating _workspace-scoped_ affordances in the SPA (board editing,
|
|
7
|
+
* run controls, HITL actions, admin settings panels). It reads the `access` the auth
|
|
8
|
+
* gate resolved server-side and attached to the workspace snapshot (`{ role, permissions }`),
|
|
9
|
+
* so the frontend never re-derives the permission math — it consumes the same source of
|
|
10
|
+
* truth the backend enforces against.
|
|
11
|
+
*
|
|
12
|
+
* **Dev-open parity.** When auth is disabled the backend attaches no `access` (it resolves
|
|
13
|
+
* no access object and allows everything), so an absent snapshot access here means dev-open
|
|
14
|
+
* ⇒ `can()` returns `true` for every permission and `role` is `null`. This mirrors the
|
|
15
|
+
* backend's `requirePermission` "absent access with no user ⇒ allow" branch exactly, so the
|
|
16
|
+
* SPA never hides an affordance the backend would have permitted.
|
|
17
|
+
*
|
|
18
|
+
* This is deliberately distinct from the ACCOUNT-scoped admin checks
|
|
19
|
+
* (`accounts.activeAccount?.roles?.includes('admin')`), which stay account-scoped: this
|
|
20
|
+
* composable answers "what can you do inside THIS board", the account check answers "what
|
|
21
|
+
* can you do to the tenant".
|
|
22
|
+
*/
|
|
23
|
+
export function useWorkspaceAccess() {
|
|
24
|
+
const workspace = useWorkspaceStore()
|
|
25
|
+
|
|
26
|
+
/** The caller's effective role on the active board, or `null` in dev-open (auth off). */
|
|
27
|
+
const role = computed<WorkspaceRole | null>(() => workspace.access?.role ?? null)
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The permission set the backend granted the caller, or `null` in dev-open. `can()`
|
|
31
|
+
* short-circuits to allow-all when this is null, so a null set is never a "deny".
|
|
32
|
+
*/
|
|
33
|
+
const permissions = computed<ReadonlySet<WorkspacePermission> | null>(() =>
|
|
34
|
+
workspace.access ? new Set(workspace.access.permissions) : null,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Whether the caller holds `permission` on the active board. Absent access (dev-open)
|
|
39
|
+
* ⇒ `true` (backend-parity). Use this — never a raw role comparison — so a future
|
|
40
|
+
* custom-role model or an escape-hatch grant is honoured automatically.
|
|
41
|
+
*/
|
|
42
|
+
function can(permission: WorkspacePermission): boolean {
|
|
43
|
+
const set = permissions.value
|
|
44
|
+
return set === null || set.has(permission)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Board mutation (blocks CRUD/move/reparent/archive, epics, initiatives, pipelines). */
|
|
48
|
+
const canWriteBoard = computed(() => can('board.write'))
|
|
49
|
+
/** Run lifecycle + all HITL windows (start/stop/merge, retry, decision approvals). */
|
|
50
|
+
const canExecuteRuns = computed(() => can('runs.execute'))
|
|
51
|
+
/** Board configuration (workspace settings, presets, risk/merge policies, observability). */
|
|
52
|
+
const canManageSettings = computed(() => can('settings.manage'))
|
|
53
|
+
/** Integration connections (GitHub/Slack/environments/runner-pool/sources, bootstrap). */
|
|
54
|
+
const canManageIntegrations = computed(() => can('integrations.manage'))
|
|
55
|
+
/** Secrets (vendor credentials, workspace + public API keys, test secrets). */
|
|
56
|
+
const canManageSecrets = computed(() => can('secrets.manage'))
|
|
57
|
+
/** Roster + access-mode flip (workspace member CRUD). */
|
|
58
|
+
const canManageMembers = computed(() => can('members.manage'))
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A read-only viewer (resolved a role, but not `>= member`). Distinct from dev-open:
|
|
62
|
+
* `isViewer` is `false` when there is no access object at all, since dev-open sees all.
|
|
63
|
+
*/
|
|
64
|
+
const isViewer = computed(() => role.value === 'viewer')
|
|
65
|
+
/** Resolved at least the member tier (or dev-open). */
|
|
66
|
+
const isMember = computed(() => role.value === null || role.value !== 'viewer')
|
|
67
|
+
/** A workspace admin (or dev-open). */
|
|
68
|
+
const isAdmin = computed(() => role.value === null || role.value === 'admin')
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
role,
|
|
72
|
+
permissions,
|
|
73
|
+
can,
|
|
74
|
+
canWriteBoard,
|
|
75
|
+
canExecuteRuns,
|
|
76
|
+
canManageSettings,
|
|
77
|
+
canManageIntegrations,
|
|
78
|
+
canManageSecrets,
|
|
79
|
+
canManageMembers,
|
|
80
|
+
isViewer,
|
|
81
|
+
isMember,
|
|
82
|
+
isAdmin,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -216,4 +216,23 @@ describe('workspace store cold-open speculative snapshot', () => {
|
|
|
216
216
|
expect(getWorkspace).toHaveBeenCalledWith('ws3')
|
|
217
217
|
expect(useBoardStore().getBlock('f3')).toBeDefined()
|
|
218
218
|
})
|
|
219
|
+
|
|
220
|
+
// Slice 8 (workspace-RBAC frontend): the resolved `access` the auth gate attaches to the
|
|
221
|
+
// snapshot must land on the store so `useWorkspaceAccess()` can gate affordances.
|
|
222
|
+
it('hydrates the resolved workspace access from the snapshot', async () => {
|
|
223
|
+
const snap = snapshot('ws1', [block('f1')]) as WorkspaceSnapshot
|
|
224
|
+
snap.access = { role: 'viewer', permissions: ['workspace.read'] }
|
|
225
|
+
const getWorkspace = vi.fn().mockResolvedValue(snap)
|
|
226
|
+
vi.stubGlobal('useApi', () => ({ getWorkspace }))
|
|
227
|
+
|
|
228
|
+
const ws = useWorkspaceStore()
|
|
229
|
+
await ws.switchTo('ws1')
|
|
230
|
+
expect(ws.access).toEqual({ role: 'viewer', permissions: ['workspace.read'] })
|
|
231
|
+
|
|
232
|
+
// A subsequent snapshot WITHOUT access (older backend / dev-open) clears it back to null.
|
|
233
|
+
const snap2 = snapshot('ws1', [block('f1')]) as WorkspaceSnapshot
|
|
234
|
+
getWorkspace.mockResolvedValue(snap2)
|
|
235
|
+
await ws.refresh()
|
|
236
|
+
expect(ws.access).toBeNull()
|
|
237
|
+
})
|
|
219
238
|
})
|
package/app/stores/workspace.ts
CHANGED
|
@@ -4,7 +4,8 @@ import type {
|
|
|
4
4
|
BudgetCaps,
|
|
5
5
|
InfraSetup,
|
|
6
6
|
SpendStatus,
|
|
7
|
-
|
|
7
|
+
WorkspaceAccess,
|
|
8
|
+
WorkspaceListItem,
|
|
8
9
|
WorkspaceSnapshot,
|
|
9
10
|
} from '~/types/domain'
|
|
10
11
|
import { useAccountsStore } from '~/stores/accounts'
|
|
@@ -53,8 +54,12 @@ export const useWorkspaceStore = defineStore(
|
|
|
53
54
|
|
|
54
55
|
/** Active workspace id (persisted so a reload reopens the same board). */
|
|
55
56
|
const workspaceId = ref<string | null>(null)
|
|
56
|
-
/**
|
|
57
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Every board visible to the user, across the accounts they belong to. Each row is
|
|
59
|
+
* annotated by `GET /workspaces` with the caller's effective workspace-RBAC role
|
|
60
|
+
* (`viewerRole`) so a restricted board can be badged in the switcher.
|
|
61
|
+
*/
|
|
62
|
+
const workspaces = ref<WorkspaceListItem[]>([])
|
|
58
63
|
/** True once the initial snapshot has been loaded and stores hydrated. */
|
|
59
64
|
const ready = ref(false)
|
|
60
65
|
/** Set when bootstrap fails so the UI can show a retry. */
|
|
@@ -73,6 +78,14 @@ export const useWorkspaceStore = defineStore(
|
|
|
73
78
|
* doesn't compute it (⇒ no banner).
|
|
74
79
|
*/
|
|
75
80
|
const infraSetup = ref<InfraSetup | null>(null)
|
|
81
|
+
/**
|
|
82
|
+
* The signed-in caller's resolved workspace-RBAC access to the ACTIVE board — their
|
|
83
|
+
* effective role + the permission set it grants, from the auth gate's resolution
|
|
84
|
+
* (attached to the snapshot with zero extra reads). Null on an older backend OR in
|
|
85
|
+
* dev-open (auth disabled) — `useWorkspaceAccess()` then allows everything (backend
|
|
86
|
+
* parity). Consumers MUST go through `useWorkspaceAccess()`, never read this directly.
|
|
87
|
+
*/
|
|
88
|
+
const access = ref<WorkspaceAccess | null>(null)
|
|
76
89
|
|
|
77
90
|
/** The boards belonging to the active account (all boards when auth is off). */
|
|
78
91
|
const accountWorkspaces = computed(() => {
|
|
@@ -118,10 +131,16 @@ export const useWorkspaceStore = defineStore(
|
|
|
118
131
|
budgetCaps.value = snapshot.budgetCaps ?? null
|
|
119
132
|
useUserSettingsStore().hydrate(snapshot.userSettings ?? null)
|
|
120
133
|
infraSetup.value = snapshot.infraSetup ?? null
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
134
|
+
access.value = snapshot.access ?? null
|
|
135
|
+
// Keep the board list in step (e.g. a freshly created board, or a rename). The
|
|
136
|
+
// snapshot's `workspace` carries no `viewerRole` (that's a `GET /workspaces` list
|
|
137
|
+
// annotation), so preserve any existing badge rather than clobbering it to absent.
|
|
138
|
+
const existingRow = workspaces.value.find((w) => w.id === snapshot.workspace.id)
|
|
139
|
+
if (existingRow) {
|
|
140
|
+
Object.assign(existingRow, snapshot.workspace)
|
|
141
|
+
} else {
|
|
142
|
+
workspaces.value.unshift(snapshot.workspace)
|
|
143
|
+
}
|
|
125
144
|
useBoardStore().hydrate(snapshot.blocks, boardSince)
|
|
126
145
|
useBoardStore().hydrateArchived(snapshot.archivedServices ?? [])
|
|
127
146
|
usePipelinesStore().hydrate(snapshot.pipelines, snapshot.pipelineCatalogVersions)
|
|
@@ -337,6 +356,7 @@ export const useWorkspaceStore = defineStore(
|
|
|
337
356
|
userSpend,
|
|
338
357
|
budgetCaps,
|
|
339
358
|
infraSetup,
|
|
359
|
+
access,
|
|
340
360
|
init,
|
|
341
361
|
switchTo,
|
|
342
362
|
selectAccount,
|
package/app/types/domain.ts
CHANGED
|
@@ -63,7 +63,13 @@ export type {
|
|
|
63
63
|
SpendStatus,
|
|
64
64
|
BudgetCaps,
|
|
65
65
|
Workspace,
|
|
66
|
+
WorkspaceListItem,
|
|
66
67
|
WorkspaceSnapshot,
|
|
68
|
+
WorkspaceRole,
|
|
69
|
+
WorkspacePermission,
|
|
70
|
+
WorkspaceAccessMode,
|
|
71
|
+
WorkspaceAccess,
|
|
72
|
+
WorkspaceMember,
|
|
67
73
|
TaskLimitMode,
|
|
68
74
|
WorkspaceSettings,
|
|
69
75
|
UpdateWorkspaceSettingsInput,
|
package/i18n/locales/de.json
CHANGED
|
@@ -534,6 +534,8 @@
|
|
|
534
534
|
"created": "Erstellt am {date}",
|
|
535
535
|
"lastUsed": "zuletzt verwendet am {date}",
|
|
536
536
|
"neverUsed": "nie verwendet",
|
|
537
|
+
"createdBy": "erstellt von {user}",
|
|
538
|
+
"createdByYou": "Ihnen",
|
|
537
539
|
"revoke": "Token widerrufen"
|
|
538
540
|
},
|
|
539
541
|
"add": {
|
|
@@ -3897,6 +3899,11 @@
|
|
|
3897
3899
|
"keep": "Weiter bearbeiten"
|
|
3898
3900
|
}
|
|
3899
3901
|
},
|
|
3902
|
+
"access": {
|
|
3903
|
+
"noBoardWrite": "Nur-Lese-Zugriff: Sie können dieses Board ansehen, aber nicht bearbeiten.",
|
|
3904
|
+
"noRunExecute": "Nur-Lese-Zugriff: Sie können Ausführungen ansehen, aber nicht starten oder steuern.",
|
|
3905
|
+
"viewerBadge": "Betrachter"
|
|
3906
|
+
},
|
|
3900
3907
|
"clarification": {
|
|
3901
3908
|
"answerPlaceholder": "Ihre Antwort",
|
|
3902
3909
|
"notRelevant": "Nicht relevant",
|
package/i18n/locales/en.json
CHANGED
|
@@ -83,6 +83,14 @@
|
|
|
83
83
|
"keep": "Keep editing"
|
|
84
84
|
}
|
|
85
85
|
},
|
|
86
|
+
"access": {
|
|
87
|
+
"noBoardWrite": "Read-only access: you can view this board but can't edit it.",
|
|
88
|
+
"noRunExecute": "Read-only access: you can view runs but can't start or control them.",
|
|
89
|
+
"viewerBadge": "Viewer",
|
|
90
|
+
"@viewerBadge": {
|
|
91
|
+
"description": "Short badge label on a board the caller can only view (read-only workspace role). Localize as the noun for a read-only observer."
|
|
92
|
+
}
|
|
93
|
+
},
|
|
86
94
|
"clarification": {
|
|
87
95
|
"answerPlaceholder": "Your answer",
|
|
88
96
|
"notRelevant": "Not relevant",
|
|
@@ -2483,6 +2491,8 @@
|
|
|
2483
2491
|
"created": "Created {date}",
|
|
2484
2492
|
"lastUsed": "last used {date}",
|
|
2485
2493
|
"neverUsed": "never used",
|
|
2494
|
+
"createdBy": "created by {user}",
|
|
2495
|
+
"createdByYou": "you",
|
|
2486
2496
|
"revoke": "Revoke token"
|
|
2487
2497
|
},
|
|
2488
2498
|
"add": {
|
package/i18n/locales/es.json
CHANGED
|
@@ -74,6 +74,11 @@
|
|
|
74
74
|
"keep": "Seguir editando"
|
|
75
75
|
}
|
|
76
76
|
},
|
|
77
|
+
"access": {
|
|
78
|
+
"noBoardWrite": "Acceso de solo lectura: puedes ver este tablero pero no editarlo.",
|
|
79
|
+
"noRunExecute": "Acceso de solo lectura: puedes ver las ejecuciones pero no iniciarlas ni controlarlas.",
|
|
80
|
+
"viewerBadge": "Observador"
|
|
81
|
+
},
|
|
77
82
|
"clarification": {
|
|
78
83
|
"answerPlaceholder": "Tu respuesta",
|
|
79
84
|
"notRelevant": "No relevante",
|
|
@@ -2298,6 +2303,8 @@
|
|
|
2298
2303
|
"created": "Creado el {date}",
|
|
2299
2304
|
"lastUsed": "usado por última vez el {date}",
|
|
2300
2305
|
"neverUsed": "nunca usado",
|
|
2306
|
+
"createdBy": "creado por {user}",
|
|
2307
|
+
"createdByYou": "ti",
|
|
2301
2308
|
"revoke": "Revocar token"
|
|
2302
2309
|
},
|
|
2303
2310
|
"add": {
|
package/i18n/locales/fr.json
CHANGED
|
@@ -74,6 +74,11 @@
|
|
|
74
74
|
"keep": "Continuer l'édition"
|
|
75
75
|
}
|
|
76
76
|
},
|
|
77
|
+
"access": {
|
|
78
|
+
"noBoardWrite": "Accès en lecture seule : vous pouvez consulter ce tableau mais pas le modifier.",
|
|
79
|
+
"noRunExecute": "Accès en lecture seule : vous pouvez consulter les exécutions mais pas les lancer ni les contrôler.",
|
|
80
|
+
"viewerBadge": "Observateur"
|
|
81
|
+
},
|
|
77
82
|
"clarification": {
|
|
78
83
|
"answerPlaceholder": "Votre réponse",
|
|
79
84
|
"notRelevant": "Non pertinent",
|
|
@@ -2298,6 +2303,8 @@
|
|
|
2298
2303
|
"created": "Créé le {date}",
|
|
2299
2304
|
"lastUsed": "dernière utilisation le {date}",
|
|
2300
2305
|
"neverUsed": "jamais utilisé",
|
|
2306
|
+
"createdBy": "créé par {user}",
|
|
2307
|
+
"createdByYou": "vous",
|
|
2301
2308
|
"revoke": "Révoquer le jeton"
|
|
2302
2309
|
},
|
|
2303
2310
|
"add": {
|
package/i18n/locales/he.json
CHANGED
|
@@ -74,6 +74,11 @@
|
|
|
74
74
|
"keep": "להמשיך לערוך"
|
|
75
75
|
}
|
|
76
76
|
},
|
|
77
|
+
"access": {
|
|
78
|
+
"noBoardWrite": "גישת קריאה בלבד: אפשר לצפות בלוח הזה אך לא לערוך אותו.",
|
|
79
|
+
"noRunExecute": "גישת קריאה בלבד: אפשר לצפות בהרצות אך לא להפעיל או לשלוט בהן.",
|
|
80
|
+
"viewerBadge": "צופה"
|
|
81
|
+
},
|
|
77
82
|
"clarification": {
|
|
78
83
|
"answerPlaceholder": "התשובה שלך",
|
|
79
84
|
"notRelevant": "לא רלוונטי",
|
|
@@ -2419,6 +2424,8 @@
|
|
|
2419
2424
|
"created": "נוצר בתאריך {date}",
|
|
2420
2425
|
"lastUsed": "שימוש אחרון בתאריך {date}",
|
|
2421
2426
|
"neverUsed": "מעולם לא היה בשימוש",
|
|
2427
|
+
"createdBy": "נוצר על ידי {user}",
|
|
2428
|
+
"createdByYou": "אתה",
|
|
2422
2429
|
"revoke": "בטל אסימון"
|
|
2423
2430
|
},
|
|
2424
2431
|
"add": {
|
package/i18n/locales/it.json
CHANGED
|
@@ -534,6 +534,8 @@
|
|
|
534
534
|
"created": "Creato il {date}",
|
|
535
535
|
"lastUsed": "ultimo utilizzo il {date}",
|
|
536
536
|
"neverUsed": "mai utilizzato",
|
|
537
|
+
"createdBy": "creato da {user}",
|
|
538
|
+
"createdByYou": "te",
|
|
537
539
|
"revoke": "Revoca token"
|
|
538
540
|
},
|
|
539
541
|
"add": {
|
|
@@ -3897,6 +3899,11 @@
|
|
|
3897
3899
|
"keep": "Continua a modificare"
|
|
3898
3900
|
}
|
|
3899
3901
|
},
|
|
3902
|
+
"access": {
|
|
3903
|
+
"noBoardWrite": "Accesso in sola lettura: puoi visualizzare questa board ma non modificarla.",
|
|
3904
|
+
"noRunExecute": "Accesso in sola lettura: puoi visualizzare le esecuzioni ma non avviarle né controllarle.",
|
|
3905
|
+
"viewerBadge": "Osservatore"
|
|
3906
|
+
},
|
|
3900
3907
|
"clarification": {
|
|
3901
3908
|
"answerPlaceholder": "La tua risposta",
|
|
3902
3909
|
"notRelevant": "Non pertinente",
|
package/i18n/locales/ja.json
CHANGED
|
@@ -74,6 +74,11 @@
|
|
|
74
74
|
"keep": "編集を続ける"
|
|
75
75
|
}
|
|
76
76
|
},
|
|
77
|
+
"access": {
|
|
78
|
+
"noBoardWrite": "読み取り専用アクセス: このボードは閲覧できますが編集はできません。",
|
|
79
|
+
"noRunExecute": "読み取り専用アクセス: 実行は閲覧できますが、開始や制御はできません。",
|
|
80
|
+
"viewerBadge": "閲覧者"
|
|
81
|
+
},
|
|
77
82
|
"clarification": {
|
|
78
83
|
"answerPlaceholder": "回答を入力",
|
|
79
84
|
"notRelevant": "関連なし",
|
|
@@ -2420,6 +2425,8 @@
|
|
|
2420
2425
|
"created": "{date} に作成",
|
|
2421
2426
|
"lastUsed": "最終使用 {date}",
|
|
2422
2427
|
"neverUsed": "未使用",
|
|
2428
|
+
"createdBy": "作成者: {user}",
|
|
2429
|
+
"createdByYou": "あなた",
|
|
2423
2430
|
"revoke": "トークンを取り消す"
|
|
2424
2431
|
},
|
|
2425
2432
|
"add": {
|
package/i18n/locales/pl.json
CHANGED
|
@@ -74,6 +74,11 @@
|
|
|
74
74
|
"keep": "Kontynuuj edycję"
|
|
75
75
|
}
|
|
76
76
|
},
|
|
77
|
+
"access": {
|
|
78
|
+
"noBoardWrite": "Dostęp tylko do odczytu: możesz przeglądać tę tablicę, ale nie możesz jej edytować.",
|
|
79
|
+
"noRunExecute": "Dostęp tylko do odczytu: możesz przeglądać uruchomienia, ale nie możesz ich uruchamiać ani nimi sterować.",
|
|
80
|
+
"viewerBadge": "Obserwator"
|
|
81
|
+
},
|
|
77
82
|
"clarification": {
|
|
78
83
|
"answerPlaceholder": "Twoja odpowiedź",
|
|
79
84
|
"notRelevant": "Nieistotne",
|
|
@@ -2298,6 +2303,8 @@
|
|
|
2298
2303
|
"created": "Utworzono {date}",
|
|
2299
2304
|
"lastUsed": "ostatnio użyto {date}",
|
|
2300
2305
|
"neverUsed": "nigdy nie użyto",
|
|
2306
|
+
"createdBy": "utworzone przez {user}",
|
|
2307
|
+
"createdByYou": "Ciebie",
|
|
2301
2308
|
"revoke": "Unieważnij token"
|
|
2302
2309
|
},
|
|
2303
2310
|
"add": {
|
package/i18n/locales/tr.json
CHANGED
|
@@ -74,6 +74,11 @@
|
|
|
74
74
|
"keep": "Düzenlemeye devam et"
|
|
75
75
|
}
|
|
76
76
|
},
|
|
77
|
+
"access": {
|
|
78
|
+
"noBoardWrite": "Salt okunur erişim: bu panoyu görüntüleyebilirsiniz ancak düzenleyemezsiniz.",
|
|
79
|
+
"noRunExecute": "Salt okunur erişim: çalıştırmaları görüntüleyebilirsiniz ancak başlatamaz veya denetleyemezsiniz.",
|
|
80
|
+
"viewerBadge": "İzleyici"
|
|
81
|
+
},
|
|
77
82
|
"clarification": {
|
|
78
83
|
"answerPlaceholder": "Yanıtınız",
|
|
79
84
|
"notRelevant": "İlgili değil",
|
|
@@ -2420,6 +2425,8 @@
|
|
|
2420
2425
|
"created": "{date} tarihinde oluşturuldu",
|
|
2421
2426
|
"lastUsed": "son kullanım {date}",
|
|
2422
2427
|
"neverUsed": "hiç kullanılmadı",
|
|
2428
|
+
"createdBy": "oluşturan: {user}",
|
|
2429
|
+
"createdByYou": "siz",
|
|
2423
2430
|
"revoke": "Belirteci iptal et"
|
|
2424
2431
|
},
|
|
2425
2432
|
"add": {
|
package/i18n/locales/uk.json
CHANGED
|
@@ -74,6 +74,11 @@
|
|
|
74
74
|
"keep": "Продовжити редагування"
|
|
75
75
|
}
|
|
76
76
|
},
|
|
77
|
+
"access": {
|
|
78
|
+
"noBoardWrite": "Доступ лише для перегляду: ви можете переглядати цю дошку, але не можете її редагувати.",
|
|
79
|
+
"noRunExecute": "Доступ лише для перегляду: ви можете переглядати запуски, але не можете їх запускати чи керувати ними.",
|
|
80
|
+
"viewerBadge": "Глядач"
|
|
81
|
+
},
|
|
77
82
|
"clarification": {
|
|
78
83
|
"answerPlaceholder": "Ваша відповідь",
|
|
79
84
|
"notRelevant": "Не актуально",
|
|
@@ -2298,6 +2303,8 @@
|
|
|
2298
2303
|
"created": "Створено {date}",
|
|
2299
2304
|
"lastUsed": "востаннє використано {date}",
|
|
2300
2305
|
"neverUsed": "ніколи не використовувався",
|
|
2306
|
+
"createdBy": "створено {user}",
|
|
2307
|
+
"createdByYou": "вами",
|
|
2301
2308
|
"revoke": "Відкликати токен"
|
|
2302
2309
|
},
|
|
2303
2310
|
"add": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.130.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",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"valibot": "^1.4.2",
|
|
35
35
|
"vue": "3.5.39",
|
|
36
36
|
"wretch": "^3.0.9",
|
|
37
|
-
"@cat-factory/contracts": "0.147.
|
|
37
|
+
"@cat-factory/contracts": "0.147.1"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@toad-contracts/testing": "0.3.2",
|