@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.
Files changed (44) hide show
  1. package/app/components/board/AgentFailureCard.vue +3 -1
  2. package/app/components/board/AgentStopButton.vue +3 -0
  3. package/app/components/board/BoardCanvas.vue +16 -3
  4. package/app/components/board/RecurringPipelineModal.vue +7 -1
  5. package/app/components/board/nodes/BlockNode.vue +47 -41
  6. package/app/components/brainstorm/BrainstormWindow.vue +25 -5
  7. package/app/components/clarity/ClarityReviewWindow.vue +17 -4
  8. package/app/components/docs/DocInterviewWindow.vue +5 -1
  9. package/app/components/followUp/FollowUpWindow.vue +15 -1
  10. package/app/components/forkDecision/ForkDecisionWindow.vue +7 -2
  11. package/app/components/gates/GateResultView.vue +7 -1
  12. package/app/components/humanTest/HumanTestWindow.vue +11 -5
  13. package/app/components/layout/BoardSwitcher.vue +21 -12
  14. package/app/components/layout/BoardToolbar.vue +8 -4
  15. package/app/components/layout/CommandBar.vue +137 -123
  16. package/app/components/layout/NotificationsInbox.vue +5 -1
  17. package/app/components/layout/SideBar.vue +157 -115
  18. package/app/components/layout/SpendWarningBanner.vue +3 -0
  19. package/app/components/panels/InspectorPanel.vue +23 -10
  20. package/app/components/panels/inspector/TaskExecution.vue +13 -4
  21. package/app/components/prReview/PrReviewWindow.vue +7 -3
  22. package/app/components/requirements/RequirementsReviewWindow.vue +33 -5
  23. package/app/components/settings/ApiTokensPanel.vue +18 -0
  24. package/app/components/visualConfirm/VisualConfirmationWindow.vue +7 -3
  25. package/app/composables/useBlockDeletion.ts +7 -0
  26. package/app/composables/useBlockDrag.ts +5 -0
  27. package/app/composables/useFrameResize.ts +4 -0
  28. package/app/composables/useWorkspaceAccess.spec.ts +102 -0
  29. package/app/composables/useWorkspaceAccess.ts +84 -0
  30. package/app/stores/publicApiKeys.spec.ts +1 -0
  31. package/app/stores/workspace.spec.ts +19 -0
  32. package/app/stores/workspace.ts +27 -7
  33. package/app/types/domain.ts +6 -0
  34. package/i18n/locales/de.json +7 -0
  35. package/i18n/locales/en.json +10 -0
  36. package/i18n/locales/es.json +7 -0
  37. package/i18n/locales/fr.json +7 -0
  38. package/i18n/locales/he.json +7 -0
  39. package/i18n/locales/it.json +7 -0
  40. package/i18n/locales/ja.json +7 -0
  41. package/i18n/locales/pl.json +7 -0
  42. package/i18n/locales/tr.json +7 -0
  43. package/i18n/locales/uk.json +7 -0
  44. 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
+ }
@@ -11,6 +11,7 @@ function key(over: Partial<PublicApiKey> = {}): PublicApiKey {
11
11
  workspaceId: 'ws1',
12
12
  label: 'CI',
13
13
  scope: 'write',
14
+ createdByUserId: null,
14
15
  createdAt: 1,
15
16
  lastUsedAt: null,
16
17
  revokedAt: null,
@@ -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
  })
@@ -4,7 +4,8 @@ import type {
4
4
  BudgetCaps,
5
5
  InfraSetup,
6
6
  SpendStatus,
7
- Workspace,
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
- /** Every board visible to the user, across the accounts they belong to. */
57
- const workspaces = ref<Workspace[]>([])
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
- // Keep the board list in step (e.g. a freshly created board, or a rename).
122
- const i = workspaces.value.findIndex((w) => w.id === snapshot.workspace.id)
123
- if (i >= 0) workspaces.value[i] = snapshot.workspace
124
- else workspaces.value.unshift(snapshot.workspace)
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,
@@ -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,
@@ -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",
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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",
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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.128.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.0"
37
+ "@cat-factory/contracts": "0.147.1"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@toad-contracts/testing": "0.3.2",