@flowdular/sdk 0.3.1 → 0.3.2

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 (119) hide show
  1. package/.ai/platform-capabilities.md +2 -0
  2. package/.ai/references/catalog/migrations/0005_catalog_list_indexes.down.sql +3 -0
  3. package/.ai/references/catalog/migrations/0005_catalog_list_indexes.up.sql +11 -0
  4. package/.ai/references/catalog/module.json +11 -1
  5. package/.ai/references/catalog/package.json +2 -2
  6. package/.ai/references/catalog/spec/module.yaml +25 -4
  7. package/.ai/references/catalog/src/agent/tools.ts +19 -10
  8. package/.ai/references/catalog/src/api/endpoints.ts +150 -10
  9. package/.ai/references/catalog/src/api/list-cursor.ts +83 -0
  10. package/.ai/references/catalog/src/client/CatalogView.tsrx +505 -159
  11. package/.ai/references/catalog/src/client/api.ts +124 -36
  12. package/.ai/references/catalog/src/client/contribution.tsrx +5 -0
  13. package/.ai/references/catalog/src/client/state.ts +169 -3
  14. package/.ai/references/catalog/src/domain/lists.ts +7 -0
  15. package/.ai/references/catalog/src/domain/types.ts +20 -0
  16. package/.ai/references/catalog/src/platform.ts +20 -0
  17. package/.ai/references/catalog/src/services/catalog-service.ts +143 -8
  18. package/.ai/references/catalog/src/services/database-repository.ts +104 -17
  19. package/.ai/references/catalog/src/services/item-export.ts +81 -0
  20. package/.ai/references/catalog/src/services/migration.ts +27 -1
  21. package/.ai/references/catalog/src/services/repository.ts +31 -2
  22. package/.ai/references/catalog/tests/agent-tools.test.ts +6 -5
  23. package/.ai/references/catalog/tests/client-state.test.ts +124 -0
  24. package/.ai/references/catalog/tests/endpoints.test.ts +269 -0
  25. package/.ai/references/catalog/tests/export.test.ts +134 -0
  26. package/.ai/references/catalog/tests/idempotency.test.ts +15 -14
  27. package/.ai/references/catalog/tests/list.test.ts +217 -0
  28. package/.ai/references/catalog/tests/migrations.test.ts +58 -2
  29. package/.ai/references/catalog/tests/module.test.ts +2 -1
  30. package/.ai/references/catalog/tests/support/database.ts +14 -0
  31. package/.ai/references/catalog/translations/en.json +35 -4
  32. package/.ai/references/catalog/translations/pl.json +35 -4
  33. package/.ai/references/catalog.provenance.json +34 -26
  34. package/README.md +1 -1
  35. package/assets/flowdular-banner.webp +0 -0
  36. package/modules/access/module.json +1 -1
  37. package/modules/access/spec/module.yaml +1 -1
  38. package/modules/agents/module.json +1 -1
  39. package/modules/agents/spec/module.yaml +1 -1
  40. package/modules/approvals/module.json +1 -1
  41. package/modules/approvals/spec/module.yaml +1 -1
  42. package/modules/audit/module.json +1 -1
  43. package/modules/audit/spec/module.yaml +1 -1
  44. package/modules/auth/module.json +2 -2
  45. package/modules/auth/package.json +1 -1
  46. package/modules/auth/spec/module.yaml +2 -2
  47. package/modules/auth/src/middleware/authentication.ts +5 -1
  48. package/modules/auth/src/services/auth-service.ts +26 -0
  49. package/modules/auth/src/services/database-repository.ts +9 -0
  50. package/modules/auth/src/services/repository.ts +2 -0
  51. package/modules/automations/module.json +1 -1
  52. package/modules/automations/spec/module.yaml +1 -1
  53. package/modules/connectors/module.json +1 -1
  54. package/modules/connectors/spec/module.yaml +1 -1
  55. package/modules/directory/module.json +1 -1
  56. package/modules/directory/spec/module.yaml +1 -1
  57. package/modules/documents/module.json +1 -1
  58. package/modules/documents/spec/module.yaml +1 -1
  59. package/modules/exports/module.json +1 -1
  60. package/modules/exports/spec/module.yaml +1 -1
  61. package/modules/import/module.json +1 -1
  62. package/modules/import/spec/module.yaml +1 -1
  63. package/modules/metering/module.json +1 -1
  64. package/modules/metering/spec/module.yaml +1 -1
  65. package/modules/notifications/module.json +1 -1
  66. package/modules/notifications/spec/module.yaml +1 -1
  67. package/modules/reports/module.json +1 -1
  68. package/modules/reports/spec/module.yaml +1 -1
  69. package/modules/sandbox/module.json +1 -1
  70. package/modules/sandbox/spec/module.yaml +1 -1
  71. package/modules/search/module.json +1 -1
  72. package/modules/search/spec/module.yaml +1 -1
  73. package/modules/system/migrations/0001_system_module_activations.down.sql +2 -0
  74. package/modules/system/migrations/0001_system_module_activations.up.sql +17 -0
  75. package/modules/system/module.json +3 -2
  76. package/modules/system/package.json +2 -2
  77. package/modules/system/spec/module.yaml +98 -1
  78. package/modules/system/src/client/ModulesView.tsrx +101 -8
  79. package/modules/system/src/client/api.ts +52 -11
  80. package/modules/system/src/client/module-columns.tsrx +32 -6
  81. package/modules/system/src/client/state.ts +26 -0
  82. package/modules/system/src/domain/modules.ts +93 -0
  83. package/modules/system/src/index.ts +13 -0
  84. package/modules/system/src/platform.ts +41 -1
  85. package/modules/system/src/server/capability.ts +6 -0
  86. package/modules/system/src/server/endpoints.ts +125 -5
  87. package/modules/system/src/server/index.ts +26 -0
  88. package/modules/system/src/server/module-catalog.ts +19 -0
  89. package/modules/system/src/server/runtime.ts +102 -0
  90. package/modules/system/src/services/database-repository.ts +91 -0
  91. package/modules/system/src/services/migration.ts +39 -0
  92. package/modules/system/src/services/module-activation-service.ts +245 -0
  93. package/modules/system/src/services/repository.ts +17 -0
  94. package/modules/system/translations/en.json +17 -1
  95. package/modules/system/translations/pl.json +17 -1
  96. package/modules/users/module.json +2 -2
  97. package/modules/users/package.json +1 -1
  98. package/modules/users/spec/module.yaml +6 -9
  99. package/modules/users/src/client/UsersView.tsrx +27 -13
  100. package/modules/users/src/client/member-columns.tsrx +13 -4
  101. package/modules/users/src/client/state.ts +16 -0
  102. package/modules/users/src/services/users-service.ts +3 -0
  103. package/modules/users/translations/en.json +2 -1
  104. package/modules/users/translations/pl.json +2 -1
  105. package/modules/workflows/module.json +2 -2
  106. package/modules/workflows/package.json +1 -1
  107. package/modules/workflows/spec/module.yaml +2 -2
  108. package/modules/workflows/src/services/database-repository.ts +93 -53
  109. package/package.json +1 -1
  110. package/packages/client/package.json +1 -0
  111. package/packages/client/src/ApplicationShell.tsrx +46 -5
  112. package/packages/client/src/index.ts +5 -0
  113. package/packages/client/src/shell/modules.ts +49 -0
  114. package/packages/client/src/shell/types.ts +3 -0
  115. package/packages/contracts/src/index.ts +11 -1
  116. package/packages/server/src/endpoint.ts +17 -1
  117. package/packages/server/src/index.ts +10 -0
  118. package/packages/server/src/module-activation.ts +69 -0
  119. package/assets/flowdular-banner.png +0 -0
@@ -12,7 +12,7 @@ capabilities:
12
12
  - translations
13
13
  dependencies:
14
14
  - id: system.core
15
- range: ^0.7.0
15
+ range: ^0.8.0
16
16
  - id: auth.core
17
17
  range: ^0.13.0
18
18
  tenancy: required
@@ -14,7 +14,7 @@
14
14
  "dependencies": [
15
15
  {
16
16
  "id": "system.core",
17
- "range": "^0.7.0"
17
+ "range": "^0.8.0"
18
18
  },
19
19
  {
20
20
  "id": "auth.core",
@@ -13,7 +13,7 @@ capabilities:
13
13
  - cli
14
14
  dependencies:
15
15
  - id: system.core
16
- range: ^0.7.0
16
+ range: ^0.8.0
17
17
  - id: auth.core
18
18
  range: ^0.13.0
19
19
  tenancy: required
@@ -14,7 +14,7 @@
14
14
  "dependencies": [
15
15
  {
16
16
  "id": "system.core",
17
- "range": "^0.7.0"
17
+ "range": "^0.8.0"
18
18
  },
19
19
  {
20
20
  "id": "auth.core",
@@ -13,7 +13,7 @@ capabilities:
13
13
  - cli
14
14
  dependencies:
15
15
  - id: system.core
16
- range: ^0.7.0
16
+ range: ^0.8.0
17
17
  - id: auth.core
18
18
  range: ^0.13.0
19
19
  tenancy: required
@@ -14,7 +14,7 @@
14
14
  "dependencies": [
15
15
  {
16
16
  "id": "system.core",
17
- "range": "^0.7.0"
17
+ "range": "^0.8.0"
18
18
  },
19
19
  {
20
20
  "id": "auth.core",
@@ -12,7 +12,7 @@ capabilities:
12
12
  - translations
13
13
  dependencies:
14
14
  - id: system.core
15
- range: ^0.7.0
15
+ range: ^0.8.0
16
16
  - id: auth.core
17
17
  range: ^0.13.0
18
18
  - id: documents.core
@@ -14,7 +14,7 @@
14
14
  "dependencies": [
15
15
  {
16
16
  "id": "system.core",
17
- "range": "^0.7.0"
17
+ "range": "^0.8.0"
18
18
  },
19
19
  {
20
20
  "id": "auth.core",
@@ -13,7 +13,7 @@ capabilities:
13
13
  - cli
14
14
  dependencies:
15
15
  - id: system.core
16
- range: ^0.7.0
16
+ range: ^0.8.0
17
17
  - id: auth.core
18
18
  range: ^0.13.0
19
19
  - id: reports.core
@@ -14,7 +14,7 @@
14
14
  "dependencies": [
15
15
  {
16
16
  "id": "system.core",
17
- "range": "^0.7.0"
17
+ "range": "^0.8.0"
18
18
  },
19
19
  {
20
20
  "id": "auth.core",
@@ -13,7 +13,7 @@ capabilities:
13
13
  - cli
14
14
  dependencies:
15
15
  - id: system.core
16
- range: ^0.7.0
16
+ range: ^0.8.0
17
17
  - id: auth.core
18
18
  range: ^0.13.0
19
19
  tenancy: required
@@ -14,7 +14,7 @@
14
14
  "dependencies": [
15
15
  {
16
16
  "id": "system.core",
17
- "range": "^0.7.0"
17
+ "range": "^0.8.0"
18
18
  },
19
19
  {
20
20
  "id": "auth.core",
@@ -11,7 +11,7 @@ capabilities:
11
11
  - translations
12
12
  dependencies:
13
13
  - id: system.core
14
- range: ^0.7.0
14
+ range: ^0.8.0
15
15
  - id: auth.core
16
16
  range: ^0.13.0
17
17
  provides:
@@ -13,7 +13,7 @@
13
13
  "dependencies": [
14
14
  {
15
15
  "id": "system.core",
16
- "range": "^0.7.0"
16
+ "range": "^0.8.0"
17
17
  },
18
18
  {
19
19
  "id": "auth.core",
@@ -13,7 +13,7 @@ capabilities:
13
13
  - cli
14
14
  dependencies:
15
15
  - id: system.core
16
- range: ^0.7.0
16
+ range: ^0.8.0
17
17
  - id: auth.core
18
18
  range: ^0.13.0
19
19
  tenancy: required
@@ -14,7 +14,7 @@
14
14
  "dependencies": [
15
15
  {
16
16
  "id": "system.core",
17
- "range": "^0.7.0"
17
+ "range": "^0.8.0"
18
18
  },
19
19
  {
20
20
  "id": "auth.core",
@@ -12,7 +12,7 @@ capabilities:
12
12
  - translations
13
13
  dependencies:
14
14
  - id: system.core
15
- range: ^0.7.0
15
+ range: ^0.8.0
16
16
  - id: auth.core
17
17
  range: ^0.13.0
18
18
  tenancy: required
@@ -0,0 +1,2 @@
1
+ DROP POLICY IF EXISTS system_module_activations_tenant_policy ON system_module_activations;
2
+ DROP TABLE IF EXISTS system_module_activations;
@@ -0,0 +1,17 @@
1
+ -- One row per module whose activation an owner changed for a workspace. A
2
+ -- composed module without a row is active; the row keeps the last decision and
3
+ -- who made it. The primary key is the only lookup: the snapshot reads every row
4
+ -- of one tenant at once.
5
+ CREATE TABLE IF NOT EXISTS system_module_activations (
6
+ tenant_id TEXT NOT NULL,
7
+ module_id TEXT NOT NULL CHECK (length(module_id) BETWEEN 1 AND 128),
8
+ active SMALLINT NOT NULL CHECK (active IN (0, 1)),
9
+ changed_by TEXT NOT NULL CHECK (length(changed_by) BETWEEN 1 AND 64),
10
+ changed_at BIGINT NOT NULL,
11
+ PRIMARY KEY (tenant_id, module_id)
12
+ );
13
+ ALTER TABLE system_module_activations ENABLE ROW LEVEL SECURITY;
14
+ ALTER TABLE system_module_activations FORCE ROW LEVEL SECURITY;
15
+ CREATE POLICY system_module_activations_tenant_policy ON system_module_activations
16
+ USING (tenant_id = current_setting('coreloom.tenant_id', true))
17
+ WITH CHECK (tenant_id = current_setting('coreloom.tenant_id', true));
@@ -3,14 +3,15 @@
3
3
  "schemaVersion": 1,
4
4
  "id": "system.core",
5
5
  "package": "@flowdular/module-system",
6
- "version": "0.7.3",
6
+ "version": "0.8.0",
7
7
  "profile": "full",
8
- "capabilities": ["api", "client", "translations"],
8
+ "capabilities": ["api", "database", "client", "translations"],
9
9
  "platform": {
10
10
  "server": true,
11
11
  "client": true
12
12
  },
13
13
  "dependencies": [],
14
+ "provides": ["system.modules.v1"],
14
15
  "tenancy": "required",
15
16
  "locales": ["en", "pl"],
16
17
  "stability": "experimental",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowdular/module-system",
3
- "version": "0.7.3",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts",
@@ -16,6 +16,7 @@
16
16
  "dependencies": {
17
17
  "@flowdular/client": "workspace:*",
18
18
  "@flowdular/contracts": "workspace:*",
19
+ "@flowdular/database": "workspace:*",
19
20
  "@flowdular/kernel": "workspace:*",
20
21
  "@flowdular/module-auth": "workspace:*",
21
22
  "@flowdular/server": "workspace:*",
@@ -26,7 +27,6 @@
26
27
  "yaml": "2.9.0"
27
28
  },
28
29
  "devDependencies": {
29
- "@flowdular/database": "workspace:*",
30
30
  "@flowdular/database-testing": "workspace:*",
31
31
  "@tsrx/typescript-plugin": "0.3.120",
32
32
  "@types/node": "24.13.3",
@@ -1,12 +1,13 @@
1
1
  schemaVersion: 2
2
2
  id: system.core
3
- specVersion: 0.7.3
3
+ specVersion: 0.8.0
4
4
  status: approved
5
5
  name: System Core
6
6
  description: Provides the application shell, platform health, module discovery, and the administration surface for module settings.
7
7
  profile: full
8
8
  capabilities:
9
9
  - api
10
+ - database
10
11
  - client
11
12
  - translations
12
13
  dependencies: []
@@ -26,6 +27,10 @@ invariants:
26
27
  - Module settings are declared by their owning module, stored per tenant by auth.core, and changed only through the settings administration API served by system.core, with an audit record.
27
28
  - system.core declares the workspace time zone as a shared tenant setting, system.core.timeZone, defaulting to UTC. Only a name the runtime zone database resolves is stored, in its canonical IANA spelling, and any module that declares a dependency on system.core reads it live through the settings runtime; a value this deployment cannot resolve reads back as UTC instead of failing the reader.
28
29
  - A feature flag is a module setting declared with kind flag, which is a non-secret boolean with a declared default, a label, a description, and tenant scope, the only scope a flag may take. It is stored, read, validated and overridden exactly like any other setting, so a module reads it on the request path through the settings runtime at no cost beyond the settings read itself; system.core adds no flag store, no flag endpoint and no second write path.
30
+ - The composed module set is CLI-owned and baked at build; a module cannot be added to a running application from the UI. system.core owns per-workspace activation of the modules the application already composes, stored in the tenant table system_module_activations under forced row-level security. A composed module without a row is active.
31
+ - system.core, auth.core, users.core and profile.core are required and always active. A module that another active module declares as a dependency, through a declared module dependency or a required public capability, cannot be deactivated; the refusal names the active dependents. A module cannot be activated while one of its dependencies is inactive.
32
+ - Every activation change appends one audit event, system.module.activated or system.module.deactivated, to the auth.core trail with the module id and the actor. Repeating the current state changes nothing and writes no event.
33
+ - Activation is enforced in three places from one per-workspace snapshot published as the public capability system.modules.v1 and memoised for at most thirty seconds per tenant, refreshed at once by a change made through this instance. defineEndpoint answers 403 MODULE_INACTIVE for an endpoint of an inactive module in the principal's workspace, required modules exempt; the application shell hides the navigation, views, widgets and command palette entries of inactive modules; the endpoint gate never reads a tenant id from the request.
29
34
  permissions:
30
35
  - id: system.workspace.access
31
36
  description: Open the authenticated ERP workspace.
@@ -44,6 +49,28 @@ dataOwnership:
44
49
  - Feature modules own their navigation metadata and screen implementations.
45
50
  - Module settings values are stored by auth.core on behalf of the declaring module; system.core owns the settings API, the per-module settings section of the Modules drawer, the Flags screen, and the settings permissions.
46
51
  - A feature flag is owned by the module that declares it; system.core only lists and edits it, and auth.core records the change.
52
+ - system.core owns the per-workspace module activation rows and the system.modules.v1 capability; the composed module set stays owned by the CLI and flowdular.json.
53
+ entities:
54
+ - id: module-activation
55
+ name: Module activation
56
+ description: The last activation decision an owner made for one composed module in one workspace.
57
+ fields:
58
+ - id: moduleId
59
+ type: string
60
+ required: true
61
+ unique: tenant
62
+ maxLength: 128
63
+ - id: active
64
+ type: boolean
65
+ required: true
66
+ - id: changedBy
67
+ type: string
68
+ required: true
69
+ maxLength: 64
70
+ description: Account id of the owner who made the change.
71
+ - id: changedAt
72
+ type: datetime
73
+ required: true
47
74
  acceptanceScenarios:
48
75
  - id: SYSTEM-MULTI-VIEW-MODULE
49
76
  given: An enabled module contributes multiple valid navigation entries and matching views.
@@ -117,15 +144,85 @@ acceptanceScenarios:
117
144
  given: Enabled modules declare feature flags and a principal with system.settings.read opens the Flags screen in Administration > Modules.
118
145
  when: The principal reads the flags and a principal with system.settings.manage turns one on for this workspace, then resets it.
119
146
  then: Every declared flag is listed grouped by its owning module with its current value and a link to the audit trail; the override applies to that workspace alone and takes effect on the next settings read; a reset restores the declared default; each change appends one settings.flag.changed audit event carrying the module, the key, the previous and next values and the actor; and a principal without system.settings.manage is refused with 401 or 403 and changes nothing.
147
+ - id: SYSTEM-MODULE-ACTIVATE
148
+ given: An owner with system.settings.manage opens Administration > Modules, where every composed module is listed with its version, activation state and dependents and is active by default.
149
+ when: The owner deactivates an optional module, confirms the dialog, and later activates it again.
150
+ then: The module is inactive for that workspace alone and active again after the second change; each change is stored with the actor and appends one system.module.deactivated or system.module.activated audit event; a member without system.settings.manage sees the row action refused with a reason, and a request without a browser session, without the scope or without a CSRF token is refused with 401 or 403 and changes nothing.
151
+ - id: SYSTEM-MODULE-DEACTIVATE-REFUSED-DEPENDENCY
152
+ given: An active module that another active module depends on, or a required module.
153
+ when: An owner attempts to deactivate it.
154
+ then: The server refuses with 409 MODULE_HAS_ACTIVE_DEPENDENTS naming the active dependents, or 409 MODULE_REQUIRED, the screen shows the reason, nothing is stored and no audit event is written; once the dependents are inactive the deactivation is accepted, and activating a module whose dependency is inactive is refused with 409 MODULE_DEPENDENCY_INACTIVE.
155
+ - id: SYSTEM-MODULE-INACTIVE-ENDPOINT
156
+ given: A module an owner deactivated for the workspace.
157
+ when: A principal of that workspace calls any endpoint the module defines, or a principal of another workspace calls the same endpoint.
158
+ then: The first request is refused with 403 MODULE_INACTIVE after the permission check, the second is served, and an endpoint of system.core, auth.core, users.core or profile.core is never gated.
159
+ - id: SYSTEM-MODULE-INACTIVE-NAVIGATION
160
+ given: A module an owner deactivated for the workspace.
161
+ when: A member of that workspace opens the application shell.
162
+ then: The shell reads the active module ids through GET /api/system/modules/active, hides the module's navigation entries, views, workspace widgets and command palette search, returns a bookmark of one of its views to the dashboard, and shows every module when the read fails.
120
163
  screens:
121
164
  - id: flags
122
165
  kind: list
123
166
  title: Flags
124
167
  navigationGroup: Administration
168
+ - id: modules
169
+ kind: list
170
+ entity: module-activation
171
+ title: Modules
172
+ navigationGroup: Administration
173
+ columns:
174
+ - moduleId
175
+ - active
125
176
  actions:
177
+ - id: activate-module
178
+ entity: module-activation
179
+ permission: system.settings.manage
180
+ kind: update
181
+ risk: workspace-write
182
+ idempotent: true
183
+ description: Activate a composed optional module for this workspace after its dependencies are active.
184
+ - id: deactivate-module
185
+ entity: module-activation
186
+ permission: system.settings.manage
187
+ kind: update
188
+ risk: workspace-write
189
+ idempotent: true
190
+ description: Deactivate a composed optional module for this workspace unless a required module or an active dependent needs it.
126
191
  - id: set-flag
127
192
  permission: system.settings.manage
128
193
  kind: update
129
194
  risk: workspace-write
130
195
  idempotent: true
131
196
  description: Turn a declared feature flag on or off for this workspace, or clear the override so the module default applies again.
197
+ decisions:
198
+ - id: D-MODULE-ACTIVATION-SCOPE
199
+ question: What can the administration UI change about modules?
200
+ answer: Per-workspace activation of the modules the application already composes; adding or removing a module stays a CLI operation because the composition is baked at build.
201
+ decidedBy: user
202
+ - id: D-MODULE-ACTIVATION-REQUIRED
203
+ question: Which modules can never be deactivated?
204
+ answer: system.core, auth.core, users.core and profile.core, and any module another active module depends on through a declared module dependency or a required public capability; the refusal names the active dependents.
205
+ decidedBy: user
206
+ - id: D-MODULE-ACTIVATION-DEFAULT
207
+ question: What is the state of a module nobody has touched?
208
+ answer: Active; the table holds only decisions an owner made, and a row is written for the module being changed, never for the whole set.
209
+ decidedBy: default
210
+ - id: D-MODULE-ACTIVATION-SNAPSHOT
211
+ question: How often is the activation read?
212
+ answer: Once per tenant into a memoised snapshot held for thirty seconds and at most 1024 tenants per process, invalidated at once by a change through the same instance; another instance sees the change within the snapshot lifetime.
213
+ decidedBy: default
214
+ - id: D-MODULE-ACTIVATION-AUDIT
215
+ question: Where is a change recorded?
216
+ answer: One system.module.activated or system.module.deactivated event in the auth.core audit trail, carrying the module id, the resulting state and the actor; a no-op repeat writes nothing.
217
+ decidedBy: default
218
+ - id: D-MODULE-ACTIVATION-SHELL
219
+ question: How does the shell learn which modules are active?
220
+ answer: From GET /api/system/modules/active, served to every member holding system.workspace.access, read once per workspace before the shell renders; a failed read shows every module because the server gate still refuses an inactive module's API.
221
+ decidedBy: default
222
+ - id: D-MODULE-ACTIVATION-AGENT-TOOLS
223
+ question: Are the agent tools of an inactive module withheld?
224
+ answer: Not yet. The harness tool registry has no per-tenant module hook and a tool carries no module id, so this is a documented follow-up; a tool of an inactive module that calls the module's service still runs.
225
+ decidedBy: default
226
+ outOfScope:
227
+ - Adding or removing a module from a running application through the UI; the composition is CLI-owned and baked at build.
228
+ - Withholding the agent tools of an inactive module from a workspace, which needs a per-tenant hook in the harness tool registry.
@@ -2,6 +2,7 @@ import { useEffect, useMemo } from 'octane';
2
2
  import {
3
3
  Alert,
4
4
  Button,
5
+ ConfirmDialog,
5
6
  Drawer,
6
7
  EmptyState,
7
8
  Icon,
@@ -14,11 +15,21 @@ import {
14
15
  } from '@flowdular/sdk/ui';
15
16
  import { activeLocale, t } from '@flowdular/sdk/client';
16
17
  import { useValue } from 'segment-state';
17
- import { ApiError, loadModuleCatalog, type ModuleCatalogEntry } from './api.ts';
18
+ import {
19
+ ApiError,
20
+ loadModuleCatalog,
21
+ setModuleActivation,
22
+ type ModuleCatalogEntry,
23
+ } from './api.ts';
18
24
  import { FlagsPanel } from './FlagsPanel.tsrx';
19
- import { moduleColumns, surfaces } from './module-columns.tsrx';
25
+ import {
26
+ activationLabel,
27
+ activationTone,
28
+ moduleColumns,
29
+ surfaces,
30
+ } from './module-columns.tsrx';
20
31
  import { ModuleSettingsSection } from './ModuleSettingsSection.tsrx';
21
- import { createModulesClientState } from './state.ts';
32
+ import { activationRefusal, createModulesClientState } from './state.ts';
22
33
 
23
34
  const TABS_ID = 'system-modules';
24
35
 
@@ -38,6 +49,11 @@ export function ModulesView(props: ModulesViewProps) @{
38
49
  client.state.selectedModuleId,
39
50
  );
40
51
  const [tab, setTab] = useValue(client.state.tab);
52
+ const [pending, setPending] = useValue(client.state.pending);
53
+ const [busyModuleId, setBusyModuleId] = useValue(client.state.busyModuleId);
54
+ const [activationError, setActivationError] = useValue(
55
+ client.state.activationError,
56
+ );
41
57
  const selected =
42
58
  modules.find((module) => module.id === selectedModuleId) ?? null;
43
59
  const term = query.trim().toLocaleLowerCase(activeLocale());
@@ -50,6 +66,9 @@ export function ModulesView(props: ModulesViewProps) @{
50
66
  module.name.toLocaleLowerCase(activeLocale()).includes(term),
51
67
  );
52
68
  const enabled = modules.filter((module) => module.enabled);
69
+ const active = enabled.filter((module) => module.active);
70
+ const pendingModule =
71
+ modules.find((module) => module.id === pending?.moduleId) ?? null;
53
72
 
54
73
  const refresh = async () => {
55
74
  setStatus('loading');
@@ -76,6 +95,33 @@ export function ModulesView(props: ModulesViewProps) @{
76
95
  void refresh();
77
96
  }, []);
78
97
 
98
+ /* The server is the authority on dependencies: a refusal it answers is
99
+ shown with the modules it named, and the catalog is read again so the
100
+ table never shows a state the workspace no longer has. */
101
+ const confirmActivation = async () => {
102
+ if (!pending) return;
103
+ setBusyModuleId(pending.moduleId);
104
+ setActivationError('');
105
+ try {
106
+ await setModuleActivation(
107
+ pending.moduleId,
108
+ pending.active,
109
+ props.csrfToken,
110
+ );
111
+ setPending(null);
112
+ await refresh();
113
+ } catch (changeError) {
114
+ setPending(null);
115
+ setActivationError(
116
+ changeError instanceof Error
117
+ ? changeError.message
118
+ : t('system.error.activation'),
119
+ );
120
+ } finally {
121
+ setBusyModuleId(null);
122
+ }
123
+ };
124
+
79
125
  <div class="ui-view">
80
126
  <PageHeader
81
127
  eyebrow={t('system.page.eyebrow')}
@@ -118,9 +164,13 @@ export function ModulesView(props: ModulesViewProps) @{
118
164
  @if (status === 'error') {
119
165
  <Alert>{error}</Alert>
120
166
  }
167
+ @if (activationError !== '') {
168
+ <Alert>{activationError}</Alert>
169
+ }
121
170
  <TableCard
122
171
  title={t('system.table.title')}
123
172
  count={t('system.table.count', {
173
+ active: active.length,
124
174
  enabled: enabled.length,
125
175
  total: modules.length,
126
176
  })}
@@ -158,6 +208,23 @@ export function ModulesView(props: ModulesViewProps) @{
158
208
  icon: 'external',
159
209
  onSelect: () => setSelectedModuleId(module.id),
160
210
  },
211
+ {
212
+ id: 'activation',
213
+ label: module.enabled && module.active
214
+ ? t('system.action.deactivate')
215
+ : t('system.action.activate'),
216
+ icon: module.enabled && module.active ? 'x' : 'play',
217
+ tone: module.enabled && module.active ? 'danger' : 'default',
218
+ disabled: busyModuleId !== null ||
219
+ activationRefusal(module, props.canManageSettings, t) !==
220
+ null,
221
+ reason: activationRefusal(module, props.canManageSettings, t) ??
222
+ undefined,
223
+ onSelect: () => setPending({
224
+ moduleId: module.id,
225
+ active: !module.active,
226
+ }),
227
+ },
161
228
  ]}
162
229
  noteIcon="code"
163
230
  note={<>
@@ -173,6 +240,28 @@ export function ModulesView(props: ModulesViewProps) @{
173
240
  </>
174
241
  }
175
242
  </div>
243
+ <ConfirmDialog
244
+ open={pending !== null}
245
+ title={pending?.active
246
+ ? t('system.activation.confirmActivateTitle', {
247
+ module: pendingModule?.name ?? pending.moduleId,
248
+ })
249
+ : t('system.activation.confirmDeactivateTitle', {
250
+ module: pendingModule?.name ?? pending?.moduleId ?? '',
251
+ })}
252
+ confirmLabel={pending?.active
253
+ ? t('system.action.activate')
254
+ : t('system.action.deactivate')}
255
+ cancelLabel={t('system.action.cancel')}
256
+ tone={pending?.active ? 'primary' : 'danger'}
257
+ busy={busyModuleId !== null}
258
+ onConfirm={() => void confirmActivation()}
259
+ onCancel={() => setPending(null)}
260
+ >
261
+ {pending?.active
262
+ ? t('system.activation.confirmActivate')
263
+ : t('system.activation.confirmDeactivate')}
264
+ </ConfirmDialog>
176
265
  <Drawer
177
266
  open={tab === 'catalog' && selected !== null}
178
267
  title={selected?.name ?? t('system.drawer.module')}
@@ -224,11 +313,9 @@ function ModuleDetail(props: {
224
313
  </span>
225
314
  <span class="ui-cell">
226
315
  <small>{t('system.table.status')}</small>
227
- <Tag tone={props.module.enabled ? 'success' : 'neutral'} dot>
228
- {props.module.enabled
229
- ? t('system.status.enabled')
230
- : t('system.status.available')}
231
- </Tag>
316
+ <Tag tone={activationTone(props.module)} dot>{activationLabel(
317
+ props.module,
318
+ )}</Tag>
232
319
  </span>
233
320
  </div>
234
321
  <span class="ui-cell">
@@ -241,6 +328,12 @@ function ModuleDetail(props: {
241
328
  <small>{t('system.detail.directory')}</small>
242
329
  <span class="ui-mono">{props.module.directory}</span>
243
330
  </span>
331
+ <span class="ui-cell">
332
+ <small>{t('system.table.dependents')}</small>
333
+ <span class="ui-mono">
334
+ {props.module.dependents.join(', ') || t('system.value.none')}
335
+ </span>
336
+ </span>
244
337
  </div>
245
338
  <div class="ui-form__section">
246
339
  <div class="ui-form__section-head">
@@ -1,7 +1,8 @@
1
- import type { ModuleCatalogEntry } from '../server/module-catalog.ts';
1
+ import type { ModuleCatalogPayload } from '../server/endpoints.ts';
2
+ import type { ModuleActivationEntry } from '../domain/modules.ts';
2
3
  import { t } from '@flowdular/sdk/client/i18n';
3
4
 
4
- export type { ModuleCatalogEntry };
5
+ export type ModuleCatalogEntry = ModuleCatalogPayload;
5
6
 
6
7
  export interface ModuleCatalog {
7
8
  readonly modules: readonly ModuleCatalogEntry[];
@@ -10,27 +11,67 @@ export interface ModuleCatalog {
10
11
 
11
12
  export class ApiError extends Error {
12
13
  readonly status: number;
14
+ /** The modules a refusal names, when the server named any. */
15
+ readonly modules: readonly string[];
13
16
 
14
- constructor(status: number, message: string) {
17
+ constructor(
18
+ status: number,
19
+ message: string,
20
+ modules: readonly string[] = [],
21
+ ) {
15
22
  super(message);
16
23
  this.name = 'ApiError';
17
24
  this.status = status;
25
+ this.modules = modules;
18
26
  }
19
27
  }
20
28
 
21
- export async function loadModuleCatalog(): Promise<ModuleCatalog> {
22
- const response = await fetch('/api/system/modules', {
23
- headers: { accept: 'application/json' },
24
- credentials: 'same-origin',
25
- });
26
- const value = (await response.json()) as ModuleCatalog & {
27
- readonly error?: { readonly message?: string };
29
+ async function payload<T>(response: Response, fallback: string): Promise<T> {
30
+ const value = (await response.json()) as T & {
31
+ readonly error?: {
32
+ readonly message?: string;
33
+ readonly modules?: readonly string[];
34
+ };
28
35
  };
29
36
  if (!response.ok) {
30
37
  throw new ApiError(
31
38
  response.status,
32
- value.error?.message ?? t('system.error.catalog'),
39
+ value.error?.message ?? fallback,
40
+ value.error?.modules ?? [],
33
41
  );
34
42
  }
35
43
  return value;
36
44
  }
45
+
46
+ export async function loadModuleCatalog(): Promise<ModuleCatalog> {
47
+ const response = await fetch('/api/system/modules', {
48
+ headers: { accept: 'application/json' },
49
+ credentials: 'same-origin',
50
+ });
51
+ return payload<ModuleCatalog>(response, t('system.error.catalog'));
52
+ }
53
+
54
+ export async function setModuleActivation(
55
+ moduleId: string,
56
+ active: boolean,
57
+ csrfToken: string,
58
+ ): Promise<ModuleActivationEntry> {
59
+ const response = await fetch(
60
+ active ? '/api/system/modules/activate' : '/api/system/modules/deactivate',
61
+ {
62
+ method: 'POST',
63
+ headers: {
64
+ 'content-type': 'application/json',
65
+ 'x-csrf-token': csrfToken,
66
+ },
67
+ credentials: 'same-origin',
68
+ body: JSON.stringify({ moduleId }),
69
+ },
70
+ );
71
+ return (
72
+ await payload<{ readonly module: ModuleActivationEntry }>(
73
+ response,
74
+ t('system.error.activation'),
75
+ )
76
+ ).module;
77
+ }