@cat-factory/app 0.129.0 → 0.131.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 (47) 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 +24 -13
  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/layout/WorkspaceMembersSettings.vue +256 -0
  20. package/app/components/panels/InspectorPanel.vue +23 -10
  21. package/app/components/panels/inspector/TaskExecution.vue +13 -4
  22. package/app/components/prReview/PrReviewWindow.vue +7 -3
  23. package/app/components/requirements/RequirementsReviewWindow.vue +33 -5
  24. package/app/components/settings/WorkspaceSettingsPanel.vue +20 -0
  25. package/app/components/visualConfirm/VisualConfirmationWindow.vue +7 -3
  26. package/app/composables/api/workspaces.ts +31 -1
  27. package/app/composables/useBlockDeletion.ts +7 -0
  28. package/app/composables/useBlockDrag.ts +5 -0
  29. package/app/composables/useFrameResize.ts +4 -0
  30. package/app/composables/useWorkspaceAccess.spec.ts +102 -0
  31. package/app/composables/useWorkspaceAccess.ts +84 -0
  32. package/app/stores/workspace.spec.ts +19 -0
  33. package/app/stores/workspace.ts +27 -7
  34. package/app/stores/workspaceMembers.spec.ts +121 -0
  35. package/app/stores/workspaceMembers.ts +74 -0
  36. package/app/types/domain.ts +6 -0
  37. package/i18n/locales/de.json +41 -1
  38. package/i18n/locales/en.json +44 -1
  39. package/i18n/locales/es.json +41 -1
  40. package/i18n/locales/fr.json +41 -1
  41. package/i18n/locales/he.json +41 -1
  42. package/i18n/locales/it.json +41 -1
  43. package/i18n/locales/ja.json +41 -1
  44. package/i18n/locales/pl.json +41 -1
  45. package/i18n/locales/tr.json +41 -1
  46. package/i18n/locales/uk.json +41 -1
  47. package/package.json +1 -1
@@ -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,
@@ -0,0 +1,121 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { useWorkspaceMembersStore } from '~/stores/workspaceMembers'
3
+ import { useWorkspaceStore } from '~/stores/workspace'
4
+ import type { WorkspaceMember } from '~/types/domain'
5
+
6
+ /** Minimal member view — only the fields the store passes through. */
7
+ function member(over: Partial<WorkspaceMember> = {}): WorkspaceMember {
8
+ return {
9
+ workspaceId: 'ws1',
10
+ userId: 'usr_1',
11
+ role: 'member',
12
+ createdAt: 1,
13
+ addedBy: null,
14
+ ...over,
15
+ }
16
+ }
17
+
18
+ describe('workspaceMembers store', () => {
19
+ beforeEach(() => {
20
+ // The store patches the board-list row on an access-mode flip; seed one restricted-capable row.
21
+ const workspace = useWorkspaceStore()
22
+ workspace.workspaces = [
23
+ { id: 'ws1', name: 'Board', accountId: 'acc1', accessMode: 'account', viewerRole: 'admin' },
24
+ ] as never
25
+ })
26
+
27
+ it('load replaces the roster and records which board it belongs to', async () => {
28
+ vi.stubGlobal('useApi', () => ({
29
+ listWorkspaceMembers: () =>
30
+ Promise.resolve([member({ userId: 'a' }), member({ userId: 'b' })]),
31
+ }))
32
+
33
+ const store = useWorkspaceMembersStore()
34
+ await store.load('ws1')
35
+
36
+ expect(store.members.map((m) => m.userId)).toEqual(['a', 'b'])
37
+ expect(store.loadedFor).toBe('ws1')
38
+ })
39
+
40
+ it('load is monotonic — a superseded slow fetch cannot clobber a newer board', async () => {
41
+ // Two loads in flight; the OLDER-issued one (wsA) resolves LAST. It must be dropped so
42
+ // the switcher never renders the previous board's roster over the current one.
43
+ const resolvers = new Map<string, (v: WorkspaceMember[]) => void>()
44
+ vi.stubGlobal('useApi', () => ({
45
+ listWorkspaceMembers: (ws: string) =>
46
+ new Promise<WorkspaceMember[]>((resolve) => resolvers.set(ws, resolve)),
47
+ }))
48
+
49
+ const store = useWorkspaceMembersStore()
50
+ const first = store.load('wsA') // slow, issued first
51
+ const second = store.load('wsB') // newer, issued second
52
+
53
+ resolvers.get('wsB')?.([member({ userId: 'b', workspaceId: 'wsB' })])
54
+ await second
55
+ resolvers.get('wsA')?.([member({ userId: 'a', workspaceId: 'wsA' })])
56
+ await first
57
+
58
+ expect(store.members.map((m) => m.userId)).toEqual(['b'])
59
+ expect(store.loadedFor).toBe('wsB')
60
+ })
61
+
62
+ it('add upserts the returned member', async () => {
63
+ vi.stubGlobal('useApi', () => ({
64
+ listWorkspaceMembers: () => Promise.resolve([member({ userId: 'a' })]),
65
+ addWorkspaceMember: (_ws: string, userId: string, role: WorkspaceMember['role']) =>
66
+ Promise.resolve(member({ userId, role })),
67
+ }))
68
+
69
+ const store = useWorkspaceMembersStore()
70
+ await store.load('ws1')
71
+ await store.add('ws1', 'b', 'viewer')
72
+
73
+ expect(store.members.map((m) => m.userId)).toEqual(['a', 'b'])
74
+ expect(store.members.find((m) => m.userId === 'b')?.role).toBe('viewer')
75
+ })
76
+
77
+ it('setRole replaces the member in place', async () => {
78
+ vi.stubGlobal('useApi', () => ({
79
+ listWorkspaceMembers: () => Promise.resolve([member({ userId: 'a', role: 'member' })]),
80
+ setWorkspaceMemberRole: (_ws: string, userId: string, role: WorkspaceMember['role']) =>
81
+ Promise.resolve(member({ userId, role })),
82
+ }))
83
+
84
+ const store = useWorkspaceMembersStore()
85
+ await store.load('ws1')
86
+ await store.setRole('ws1', 'a', 'admin')
87
+
88
+ expect(store.members).toHaveLength(1)
89
+ expect(store.members[0]?.role).toBe('admin')
90
+ })
91
+
92
+ it('remove drops the member from the roster', async () => {
93
+ vi.stubGlobal('useApi', () => ({
94
+ listWorkspaceMembers: () =>
95
+ Promise.resolve([member({ userId: 'a' }), member({ userId: 'b' })]),
96
+ removeWorkspaceMember: () => Promise.resolve(),
97
+ }))
98
+
99
+ const store = useWorkspaceMembersStore()
100
+ await store.load('ws1')
101
+ await store.remove('ws1', 'a')
102
+
103
+ expect(store.members.map((m) => m.userId)).toEqual(['b'])
104
+ })
105
+
106
+ it('setAccessMode patches the board-list row in place, preserving the viewerRole annotation', async () => {
107
+ vi.stubGlobal('useApi', () => ({
108
+ setWorkspaceAccessMode: (workspaceId: string, accessMode: string) =>
109
+ // The single-workspace response carries no `viewerRole` (that's a list annotation).
110
+ Promise.resolve({ id: workspaceId, name: 'Board', accountId: 'acc1', accessMode }),
111
+ }))
112
+
113
+ const store = useWorkspaceMembersStore()
114
+ await store.setAccessMode('ws1', 'restricted')
115
+
116
+ const row = useWorkspaceStore().workspaces.find((w) => w.id === 'ws1')
117
+ expect(row?.accessMode).toBe('restricted')
118
+ // The merge must not clobber the list-only badge.
119
+ expect(row?.viewerRole).toBe('admin')
120
+ })
121
+ })
@@ -0,0 +1,74 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref } from 'vue'
3
+ import { useUpsertList } from '~/composables/useUpsertList'
4
+ import type { WorkspaceAccessMode, WorkspaceMember, WorkspaceRole } from '~/types/domain'
5
+ import { useWorkspaceStore } from '~/stores/workspace'
6
+
7
+ /**
8
+ * The active board's workspace-RBAC roster (member-management, slice 9). Owns the
9
+ * `workspace_members` list plus the board's access-mode flip, sitting BELOW the account
10
+ * tier: an account admin can restrict a board to an explicit member list, while an
11
+ * unrestricted (`account`) board keeps the legacy "every account member sees it" behaviour.
12
+ *
13
+ * Every write requires `members.manage` server-side, so the SPA only mounts this surface
14
+ * (the Members tab in workspace settings) when `useWorkspaceAccess().canManageMembers`.
15
+ * The list is loaded lazily when that panel opens — it is not part of the board snapshot.
16
+ */
17
+ export const useWorkspaceMembersStore = defineStore('workspaceMembers', () => {
18
+ const api = useApi()
19
+
20
+ const { items: members, upsert: upsertMember } = useUpsertList<WorkspaceMember>({
21
+ key: (m) => m.userId,
22
+ })
23
+ /**
24
+ * The workspace id the currently-committed roster belongs to. The Members panel renders
25
+ * the roster only while this matches its own `workspaceId`, so switching boards never
26
+ * briefly shows the previous board's members before the new load resolves.
27
+ */
28
+ const loadedFor = ref<string | null>(null)
29
+ // Monotonic request token: only the latest-issued load() commits, so a slow, superseded
30
+ // fetch can't clobber a newer board's roster (the ordering hazard the coherence rules warn about).
31
+ let loadSeq = 0
32
+
33
+ /** Load the board's member roster (enriched with display details by the backend). */
34
+ async function load(workspaceId: string) {
35
+ const token = ++loadSeq
36
+ const roster = await api.listWorkspaceMembers(workspaceId)
37
+ if (token !== loadSeq) return // a newer load() superseded this one; drop the stale result
38
+ members.value = roster
39
+ loadedFor.value = workspaceId
40
+ }
41
+
42
+ /** Add an account member to the board at a workspace role (upsert). */
43
+ async function add(workspaceId: string, userId: string, role: WorkspaceRole) {
44
+ upsertMember(await api.addWorkspaceMember(workspaceId, userId, role))
45
+ }
46
+
47
+ /** Change an existing member's workspace role. */
48
+ async function setRole(workspaceId: string, userId: string, role: WorkspaceRole) {
49
+ upsertMember(await api.setWorkspaceMemberRole(workspaceId, userId, role))
50
+ }
51
+
52
+ /** Remove a member from the board. */
53
+ async function remove(workspaceId: string, userId: string) {
54
+ await api.removeWorkspaceMember(workspaceId, userId)
55
+ members.value = members.value.filter((m) => m.userId !== userId)
56
+ }
57
+
58
+ /**
59
+ * Flip the board's access mode. `restricted` limits it to the roster; `account` restores
60
+ * the legacy behaviour. Patches the board's list row in place so the switcher badge and
61
+ * `activeWorkspace.accessMode` reflect the flip without a re-fetch.
62
+ */
63
+ async function setAccessMode(workspaceId: string, accessMode: WorkspaceAccessMode) {
64
+ const updated = await api.setWorkspaceAccessMode(workspaceId, accessMode)
65
+ const workspace = useWorkspaceStore()
66
+ const row = workspace.workspaces.find((w) => w.id === workspaceId)
67
+ // Merge only the workspace fields (incl. `accessMode`), preserving the row's
68
+ // `viewerRole` list annotation, which the single-workspace response doesn't carry.
69
+ if (row) Object.assign(row, updated)
70
+ return updated
71
+ }
72
+
73
+ return { members, loadedFor, load, add, setRole, remove, setAccessMode }
74
+ })
@@ -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,
@@ -709,7 +709,8 @@
709
709
  "budget": "Budget",
710
710
  "merge": "Risikorichtlinien",
711
711
  "tracker": "Issue-Tracker",
712
- "fragments": "Dienst-Best-Practices"
712
+ "fragments": "Dienst-Best-Practices",
713
+ "members": "Mitglieder"
713
714
  },
714
715
  "waiting": {
715
716
  "heading": "Warten auf einen Menschen",
@@ -1978,6 +1979,40 @@
1978
1979
  },
1979
1980
  "accountSkills": {
1980
1981
  "intro": "Verknüpfe Repositorys, in denen dein Team Claude Skills erstellt (ein SKILL.md-Ordner plus Ressourcendateien). Synchronisierte Skills landen im Katalog dieses Kontos, der von allen Boards geteilt wird, und der Skill-Schritt einer Pipeline führt einen davon für eine Aufgabe aus."
1982
+ },
1983
+ "workspaceMembers": {
1984
+ "you": "{name} (du)",
1985
+ "roles": {
1986
+ "admin": "Admin",
1987
+ "member": "Mitglied",
1988
+ "viewer": "Betrachter"
1989
+ },
1990
+ "accessMode": {
1991
+ "title": "Board-Zugriff",
1992
+ "accountHint": "Alle im zugehörigen Konto können dieses Board sehen.",
1993
+ "restrictedHint": "Nur die unten aufgeführten Mitglieder können dieses Board sehen.",
1994
+ "restrictToggle": "Auf Mitglieder beschränken"
1995
+ },
1996
+ "roster": {
1997
+ "title": "Mitglieder",
1998
+ "empty": "Noch keine Mitglieder.",
1999
+ "remove": "Mitglied entfernen"
2000
+ },
2001
+ "add": {
2002
+ "title": "Mitglied hinzufügen",
2003
+ "selectMember": "Kontomitglied auswählen",
2004
+ "submit": "Hinzufügen",
2005
+ "added": "Mitglied hinzugefügt",
2006
+ "allAdded": "Alle Kontomitglieder sind bereits auf diesem Board.",
2007
+ "noAccount": "Dieses Board ist noch mit keinem Konto verknüpft."
2008
+ },
2009
+ "errors": {
2010
+ "load": "Mitglieder konnten nicht geladen werden",
2011
+ "accessMode": "Board-Zugriff konnte nicht geändert werden",
2012
+ "setRole": "Mitgliedsrolle konnte nicht aktualisiert werden",
2013
+ "add": "Mitglied konnte nicht hinzugefügt werden",
2014
+ "remove": "Mitglied konnte nicht entfernt werden"
2015
+ }
1981
2016
  }
1982
2017
  },
1983
2018
  "board": {
@@ -3899,6 +3934,11 @@
3899
3934
  "keep": "Weiter bearbeiten"
3900
3935
  }
3901
3936
  },
3937
+ "access": {
3938
+ "noBoardWrite": "Nur-Lese-Zugriff: Sie können dieses Board ansehen, aber nicht bearbeiten.",
3939
+ "noRunExecute": "Nur-Lese-Zugriff: Sie können Ausführungen ansehen, aber nicht starten oder steuern.",
3940
+ "viewerBadge": "Betrachter"
3941
+ },
3902
3942
  "clarification": {
3903
3943
  "answerPlaceholder": "Ihre Antwort",
3904
3944
  "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",
@@ -1943,6 +1951,40 @@
1943
1951
  },
1944
1952
  "accountSkills": {
1945
1953
  "intro": "Link repos where your team authors Claude Skills (a SKILL.md folder plus resource files). Synced skills join this account's catalog, shared by every board, and a pipeline's Skill step runs one against a task."
1954
+ },
1955
+ "workspaceMembers": {
1956
+ "you": "{name} (you)",
1957
+ "roles": {
1958
+ "admin": "Admin",
1959
+ "member": "Member",
1960
+ "viewer": "Viewer"
1961
+ },
1962
+ "accessMode": {
1963
+ "title": "Board access",
1964
+ "accountHint": "Everyone in the owning account can see this board.",
1965
+ "restrictedHint": "Only the members listed below can see this board.",
1966
+ "restrictToggle": "Restrict to members"
1967
+ },
1968
+ "roster": {
1969
+ "title": "Members",
1970
+ "empty": "No members yet.",
1971
+ "remove": "Remove member"
1972
+ },
1973
+ "add": {
1974
+ "title": "Add a member",
1975
+ "selectMember": "Select an account member",
1976
+ "submit": "Add",
1977
+ "added": "Member added",
1978
+ "allAdded": "Every account member is already on this board.",
1979
+ "noAccount": "This board isn't linked to an account yet."
1980
+ },
1981
+ "errors": {
1982
+ "load": "Could not load members",
1983
+ "accessMode": "Could not change board access",
1984
+ "setRole": "Could not update member role",
1985
+ "add": "Could not add member",
1986
+ "remove": "Could not remove member"
1987
+ }
1946
1988
  }
1947
1989
  },
1948
1990
  "settings": {
@@ -2658,7 +2700,8 @@
2658
2700
  "budget": "Budget",
2659
2701
  "merge": "Risk policies",
2660
2702
  "tracker": "Issue tracker",
2661
- "fragments": "Service best practices"
2703
+ "fragments": "Service best practices",
2704
+ "members": "Members"
2662
2705
  },
2663
2706
  "waiting": {
2664
2707
  "heading": "Waiting for a human",
@@ -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",
@@ -1883,6 +1888,40 @@
1883
1888
  },
1884
1889
  "accountSkills": {
1885
1890
  "intro": "Vincula repositorios donde tu equipo crea Claude Skills (una carpeta SKILL.md más archivos de recursos). Las habilidades sincronizadas se añaden al catálogo de esta cuenta, compartido por todos los tableros, y el paso Skill de una tubería ejecuta una sobre una tarea."
1891
+ },
1892
+ "workspaceMembers": {
1893
+ "you": "{name} (tú)",
1894
+ "roles": {
1895
+ "admin": "Administrador",
1896
+ "member": "Miembro",
1897
+ "viewer": "Observador"
1898
+ },
1899
+ "accessMode": {
1900
+ "title": "Acceso al tablero",
1901
+ "accountHint": "Todos los miembros de la cuenta propietaria pueden ver este tablero.",
1902
+ "restrictedHint": "Solo los miembros que aparecen abajo pueden ver este tablero.",
1903
+ "restrictToggle": "Restringir a miembros"
1904
+ },
1905
+ "roster": {
1906
+ "title": "Miembros",
1907
+ "empty": "Aún no hay miembros.",
1908
+ "remove": "Quitar miembro"
1909
+ },
1910
+ "add": {
1911
+ "title": "Añadir un miembro",
1912
+ "selectMember": "Selecciona un miembro de la cuenta",
1913
+ "submit": "Añadir",
1914
+ "added": "Miembro añadido",
1915
+ "allAdded": "Todos los miembros de la cuenta ya están en este tablero.",
1916
+ "noAccount": "Este tablero aún no está vinculado a una cuenta."
1917
+ },
1918
+ "errors": {
1919
+ "load": "No se pudieron cargar los miembros",
1920
+ "accessMode": "No se pudo cambiar el acceso al tablero",
1921
+ "setRole": "No se pudo actualizar el rol del miembro",
1922
+ "add": "No se pudo añadir el miembro",
1923
+ "remove": "No se pudo quitar el miembro"
1924
+ }
1886
1925
  }
1887
1926
  },
1888
1927
  "settings": {
@@ -2473,7 +2512,8 @@
2473
2512
  "budget": "Presupuesto",
2474
2513
  "merge": "Políticas de riesgo",
2475
2514
  "tracker": "Gestor de incidencias",
2476
- "fragments": "Buenas prácticas del servicio"
2515
+ "fragments": "Buenas prácticas del servicio",
2516
+ "members": "Miembros"
2477
2517
  },
2478
2518
  "waiting": {
2479
2519
  "heading": "Esperando a una persona",
@@ -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",
@@ -1883,6 +1888,40 @@
1883
1888
  },
1884
1889
  "accountSkills": {
1885
1890
  "intro": "Reliez les dépôts où votre équipe crée des Claude Skills (un dossier SKILL.md plus des fichiers de ressources). Les compétences synchronisées rejoignent le catalogue de ce compte, partagé par tous les tableaux, et l'étape Skill d'un pipeline en exécute une sur une tâche."
1891
+ },
1892
+ "workspaceMembers": {
1893
+ "you": "{name} (vous)",
1894
+ "roles": {
1895
+ "admin": "Administrateur",
1896
+ "member": "Membre",
1897
+ "viewer": "Observateur"
1898
+ },
1899
+ "accessMode": {
1900
+ "title": "Accès au tableau",
1901
+ "accountHint": "Tous les membres du compte propriétaire peuvent voir ce tableau.",
1902
+ "restrictedHint": "Seuls les membres listés ci-dessous peuvent voir ce tableau.",
1903
+ "restrictToggle": "Limiter aux membres"
1904
+ },
1905
+ "roster": {
1906
+ "title": "Membres",
1907
+ "empty": "Aucun membre pour l'instant.",
1908
+ "remove": "Retirer le membre"
1909
+ },
1910
+ "add": {
1911
+ "title": "Ajouter un membre",
1912
+ "selectMember": "Sélectionner un membre du compte",
1913
+ "submit": "Ajouter",
1914
+ "added": "Membre ajouté",
1915
+ "allAdded": "Tous les membres du compte sont déjà sur ce tableau.",
1916
+ "noAccount": "Ce tableau n'est pas encore lié à un compte."
1917
+ },
1918
+ "errors": {
1919
+ "load": "Impossible de charger les membres",
1920
+ "accessMode": "Impossible de modifier l'accès au tableau",
1921
+ "setRole": "Impossible de mettre à jour le rôle du membre",
1922
+ "add": "Impossible d'ajouter le membre",
1923
+ "remove": "Impossible de retirer le membre"
1924
+ }
1886
1925
  }
1887
1926
  },
1888
1927
  "settings": {
@@ -2473,7 +2512,8 @@
2473
2512
  "budget": "Budget",
2474
2513
  "merge": "Politiques de risque",
2475
2514
  "tracker": "Suivi des tickets",
2476
- "fragments": "Bonnes pratiques du service"
2515
+ "fragments": "Bonnes pratiques du service",
2516
+ "members": "Membres"
2477
2517
  },
2478
2518
  "waiting": {
2479
2519
  "heading": "En attente d'un humain",
@@ -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": "לא רלוונטי",
@@ -1883,6 +1888,40 @@
1883
1888
  },
1884
1889
  "accountSkills": {
1885
1890
  "intro": "קישור מאגרים שבהם הצוות שלך יוצרת Claude Skills (תיקיית SKILL.md בתוספת קבצי משאבים). כישורים מסונכרנים מתווספים לקטלוג של חשבון זה, המשותף לכל הלוחות, ושלב ה-Skill של צינור מריץ אחד מהם על משימה."
1891
+ },
1892
+ "workspaceMembers": {
1893
+ "you": "{name} (אתה)",
1894
+ "roles": {
1895
+ "admin": "מנהל",
1896
+ "member": "חבר",
1897
+ "viewer": "צופה"
1898
+ },
1899
+ "accessMode": {
1900
+ "title": "גישה ללוח",
1901
+ "accountHint": "כל מי שבחשבון הבעלים יכול לראות את הלוח הזה.",
1902
+ "restrictedHint": "רק החברים המופיעים למטה יכולים לראות את הלוח הזה.",
1903
+ "restrictToggle": "הגבל לחברים"
1904
+ },
1905
+ "roster": {
1906
+ "title": "חברים",
1907
+ "empty": "אין חברים עדיין.",
1908
+ "remove": "הסר חבר"
1909
+ },
1910
+ "add": {
1911
+ "title": "הוסף חבר",
1912
+ "selectMember": "בחר חבר חשבון",
1913
+ "submit": "הוסף",
1914
+ "added": "החבר נוסף",
1915
+ "allAdded": "כל חברי החשבון כבר נמצאים בלוח הזה.",
1916
+ "noAccount": "הלוח הזה עדיין לא מקושר לחשבון."
1917
+ },
1918
+ "errors": {
1919
+ "load": "לא ניתן לטעון את החברים",
1920
+ "accessMode": "לא ניתן לשנות את הגישה ללוח",
1921
+ "setRole": "לא ניתן לעדכן את תפקיד החבר",
1922
+ "add": "לא ניתן להוסיף את החבר",
1923
+ "remove": "לא ניתן להסיר את החבר"
1924
+ }
1886
1925
  }
1887
1926
  },
1888
1927
  "settings": {
@@ -2594,7 +2633,8 @@
2594
2633
  "budget": "תקציב",
2595
2634
  "merge": "מדיניות סיכון",
2596
2635
  "tracker": "מעקב כרטיסים",
2597
- "fragments": "שיטות עבודה מומלצות לשירות"
2636
+ "fragments": "שיטות עבודה מומלצות לשירות",
2637
+ "members": "חברים"
2598
2638
  },
2599
2639
  "waiting": {
2600
2640
  "heading": "ממתין לאדם",
@@ -709,7 +709,8 @@
709
709
  "budget": "Budget",
710
710
  "merge": "Criteri di rischio",
711
711
  "tracker": "Tracker degli issue",
712
- "fragments": "Best practice dei servizi"
712
+ "fragments": "Best practice dei servizi",
713
+ "members": "Membri"
713
714
  },
714
715
  "waiting": {
715
716
  "heading": "In attesa di un umano",
@@ -1978,6 +1979,40 @@
1978
1979
  },
1979
1980
  "accountSkills": {
1980
1981
  "intro": "Collega i repository dove il tuo team crea le Claude Skills (una cartella SKILL.md più file di risorse). Le competenze sincronizzate entrano nel catalogo di questo account, condiviso da tutte le board, e il passo Skill di una pipeline ne esegue una su un'attività."
1982
+ },
1983
+ "workspaceMembers": {
1984
+ "you": "{name} (tu)",
1985
+ "roles": {
1986
+ "admin": "Amministratore",
1987
+ "member": "Membro",
1988
+ "viewer": "Osservatore"
1989
+ },
1990
+ "accessMode": {
1991
+ "title": "Accesso alla board",
1992
+ "accountHint": "Tutti i membri dell'account proprietario possono vedere questa board.",
1993
+ "restrictedHint": "Solo i membri elencati di seguito possono vedere questa board.",
1994
+ "restrictToggle": "Limita ai membri"
1995
+ },
1996
+ "roster": {
1997
+ "title": "Membri",
1998
+ "empty": "Ancora nessun membro.",
1999
+ "remove": "Rimuovi membro"
2000
+ },
2001
+ "add": {
2002
+ "title": "Aggiungi un membro",
2003
+ "selectMember": "Seleziona un membro dell'account",
2004
+ "submit": "Aggiungi",
2005
+ "added": "Membro aggiunto",
2006
+ "allAdded": "Tutti i membri dell'account sono già su questa board.",
2007
+ "noAccount": "Questa board non è ancora collegata a un account."
2008
+ },
2009
+ "errors": {
2010
+ "load": "Impossibile caricare i membri",
2011
+ "accessMode": "Impossibile modificare l'accesso alla board",
2012
+ "setRole": "Impossibile aggiornare il ruolo del membro",
2013
+ "add": "Impossibile aggiungere il membro",
2014
+ "remove": "Impossibile rimuovere il membro"
2015
+ }
1981
2016
  }
1982
2017
  },
1983
2018
  "board": {
@@ -3899,6 +3934,11 @@
3899
3934
  "keep": "Continua a modificare"
3900
3935
  }
3901
3936
  },
3937
+ "access": {
3938
+ "noBoardWrite": "Accesso in sola lettura: puoi visualizzare questa board ma non modificarla.",
3939
+ "noRunExecute": "Accesso in sola lettura: puoi visualizzare le esecuzioni ma non avviarle né controllarle.",
3940
+ "viewerBadge": "Osservatore"
3941
+ },
3902
3942
  "clarification": {
3903
3943
  "answerPlaceholder": "La tua risposta",
3904
3944
  "notRelevant": "Non pertinente",