@cat-factory/app 0.169.0 → 0.170.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -4
- package/app/modular/nav-contributions.spec.ts +39 -10
- package/app/modular/nav-contributions.ts +18 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,9 +56,10 @@ over the WebSocket. How that sync works is written up in
|
|
|
56
56
|
## Interface modes (basic / advanced)
|
|
57
57
|
|
|
58
58
|
The SPA renders at one of two **interface tiers**. `basic` (the default) is the everyday
|
|
59
|
-
surface: the
|
|
60
|
-
|
|
61
|
-
everything. The tier resolves in a fixed order, first
|
|
59
|
+
surface: the run/pipeline options that only exist to override a workspace-level default are
|
|
60
|
+
left at that default, and the nav is trimmed to the destinations that are the **only route**
|
|
61
|
+
to their capability. `advanced` shows everything. The tier resolves in a fixed order, first
|
|
62
|
+
match wins:
|
|
62
63
|
|
|
63
64
|
1. **`NUXT_PUBLIC_UI_MODE`** (`basic` | `advanced`) — the deployment pin. Like
|
|
64
65
|
`NUXT_PUBLIC_API_BASE` it is baked in at **build** time (`ssr: false`), and while it is
|
|
@@ -81,7 +82,15 @@ hoc where it can be avoided:
|
|
|
81
82
|
- **A nav destination** declares `advanced: true` in `app/modular/nav-contributions.ts`. The
|
|
82
83
|
shared `navSlotFilter` drops it in basic mode across all three shells (sidebar, command
|
|
83
84
|
palette, toolbar), independently of its RBAC `gate` — both must pass. A consumer module's
|
|
84
|
-
own contributions take the same flag.
|
|
85
|
+
own contributions take the same flag. The bar for setting it is **route count, not how
|
|
86
|
+
advanced the surface feels**: hiding the only way to reach a capability removes the
|
|
87
|
+
capability from the tier, while hiding a shortcut into a surface a basic destination also
|
|
88
|
+
opens removes nothing. So the flag belongs on a surface that sits beside the delivery path
|
|
89
|
+
(Sandbox, Kaizen), on a palette shortcut into a Workspace-settings tab, or on a knob the
|
|
90
|
+
Integrations hub already offers — and never on a sole route (the pipeline builder, the
|
|
91
|
+
fragment library, the infrastructure/PREnv windows, the operator + reports views).
|
|
92
|
+
`nav-contributions.spec.ts` pins the advanced set against a table of each item's
|
|
93
|
+
alternative route, so promoting one forces that claim to be written down.
|
|
85
94
|
- **A less-used option inside a surface** reads `useUiModeStore().isAdvanced`. Hide, never
|
|
86
95
|
disable, and only ever hide an OVERRIDE: what remains must be exactly the default the hidden
|
|
87
96
|
field would have shown, so a basic-mode user never gets different behaviour from an advanced
|
|
@@ -99,11 +99,36 @@ describe('navSlotFilter', () => {
|
|
|
99
99
|
expect(kept).toContain('integrations-hub')
|
|
100
100
|
expect(kept).toContain('workspace-settings')
|
|
101
101
|
expect(kept).toContain('model-config')
|
|
102
|
-
// ...and the
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
// ...and so does every destination that is the SOLE route to its capability, however deep
|
|
103
|
+
// it feels: authoring a flow, the standards library, the PREnv/runner plumbing, and the
|
|
104
|
+
// aggregate run-health / spend views.
|
|
105
|
+
expect(kept).toContain('build-pipeline')
|
|
106
|
+
expect(kept).toContain('fragments')
|
|
107
|
+
expect(kept).toContain('infrastructure')
|
|
108
|
+
expect(kept).toContain('environment-setup')
|
|
109
|
+
expect(kept).toContain('operator-dashboard')
|
|
110
|
+
expect(kept).toContain('reports')
|
|
111
|
+
// What drops is beside the delivery path (experimentation) or a shortcut into a surface
|
|
112
|
+
// basic mode already reaches another way.
|
|
105
113
|
expect(kept).not.toContain('sandbox')
|
|
106
|
-
expect(kept).not.toContain('
|
|
114
|
+
expect(kept).not.toContain('kaizen')
|
|
115
|
+
expect(kept).not.toContain('merge-thresholds')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('never makes an advanced item the only route to its capability', () => {
|
|
119
|
+
// The tier's governing rule (see the catalog comment): every advanced destination either
|
|
120
|
+
// sits beside the delivery path, or is a shortcut into a surface a basic destination also
|
|
121
|
+
// opens. Spelled out here as a table so promoting an item to `advanced` forces an explicit
|
|
122
|
+
// claim about how basic mode still reaches it, rather than a silent capability loss.
|
|
123
|
+
const ALTERNATIVE_ROUTE: Record<string, string> = {
|
|
124
|
+
sandbox: 'none needed - experimentation surface, not on the delivery path',
|
|
125
|
+
kaizen: 'none needed - self-grading history, not on the delivery path',
|
|
126
|
+
'merge-thresholds': 'workspace-settings -> Merge tab',
|
|
127
|
+
'service-fragment-defaults': 'workspace-settings -> Service best practices tab',
|
|
128
|
+
'local-models': 'integrations-hub -> Local runners',
|
|
129
|
+
}
|
|
130
|
+
const advanced = NAV_CONTRIBUTIONS.filter((i) => i.advanced).map((i) => i.id)
|
|
131
|
+
expect(advanced.sort()).toEqual(Object.keys(ALTERNATIVE_ROUTE).sort())
|
|
107
132
|
})
|
|
108
133
|
|
|
109
134
|
it('keeps the tier switch itself reachable in basic mode', () => {
|
|
@@ -119,15 +144,19 @@ describe('navSlotFilter', () => {
|
|
|
119
144
|
})
|
|
120
145
|
|
|
121
146
|
it('keeps the tier and the permission axes independent — both must pass', () => {
|
|
122
|
-
// `
|
|
147
|
+
// `sandbox` is advanced AND needs integrations.manage: neither axis alone reveals it.
|
|
123
148
|
const advancedOnly: NavGates = { ...NO_GATES, advancedMode: true }
|
|
124
|
-
expect(ids(navSlotFilter(slots(), { gates: advancedOnly }))).not.toContain('
|
|
149
|
+
expect(ids(navSlotFilter(slots(), { gates: advancedOnly }))).not.toContain('sandbox')
|
|
125
150
|
|
|
126
|
-
const permissionOnly: NavGates = {
|
|
127
|
-
|
|
151
|
+
const permissionOnly: NavGates = {
|
|
152
|
+
...NO_GATES,
|
|
153
|
+
canManageIntegrations: true,
|
|
154
|
+
advancedMode: false,
|
|
155
|
+
}
|
|
156
|
+
expect(ids(navSlotFilter(slots(), { gates: permissionOnly }))).not.toContain('sandbox')
|
|
128
157
|
|
|
129
|
-
const both: NavGates = { ...NO_GATES,
|
|
130
|
-
expect(ids(navSlotFilter(slots(), { gates: both }))).toContain('
|
|
158
|
+
const both: NavGates = { ...NO_GATES, canManageIntegrations: true, advancedMode: true }
|
|
159
|
+
expect(ids(navSlotFilter(slots(), { gates: both }))).toContain('sandbox')
|
|
131
160
|
})
|
|
132
161
|
|
|
133
162
|
it('leaves at least one sidebar destination in every basic-mode section it keeps', () => {
|
|
@@ -153,13 +153,24 @@ const S = (...s: NavSurface[]) => s as readonly NavSurface[]
|
|
|
153
153
|
* with accounts enabled).
|
|
154
154
|
* - one icon per destination across shells.
|
|
155
155
|
*
|
|
156
|
-
* `advanced: true` marks the power-user half, hidden in basic interface mode. The line
|
|
157
|
-
* drawn
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
156
|
+
* `advanced: true` marks the power-user half, hidden in basic interface mode. The line is
|
|
157
|
+
* drawn by ROUTE COUNT, not by how advanced a surface feels: hiding the only way to reach a
|
|
158
|
+
* capability removes the capability from the tier, while hiding a shortcut to a surface basic
|
|
159
|
+
* mode already reaches removes nothing. So an item is advanced only when both of these hold —
|
|
160
|
+
* it is not the sole route to its capability, and nothing on the delivery path needs it:
|
|
161
|
+
*
|
|
162
|
+
* - `sandbox` / `kaizen` — experimentation and self-grading surfaces, beside the delivery
|
|
163
|
+
* path rather than on it.
|
|
164
|
+
* - `merge-thresholds` / `service-fragment-defaults` — palette shortcuts into Workspace
|
|
165
|
+
* settings tabs (Merge, Service best practices), which basic mode reaches via
|
|
166
|
+
* `workspace-settings`.
|
|
167
|
+
* - `local-models` — a per-user endpoint knob the Integrations hub already offers.
|
|
168
|
+
*
|
|
169
|
+
* Everything else stays in basic BECAUSE it is a sole route: authoring a flow
|
|
170
|
+
* (`build-pipeline`), the standards/skills library (`fragments`, whose only other route is a
|
|
171
|
+
* button two levels into Workspace settings), the PREnv + runner plumbing (`infrastructure`,
|
|
172
|
+
* `environment-setup`), and the operator/cost views that are the only aggregate read of run
|
|
173
|
+
* health and spend (`operator-dashboard`, `reports`).
|
|
163
174
|
*/
|
|
164
175
|
export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
|
|
165
176
|
{
|
|
@@ -167,7 +178,6 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
|
|
|
167
178
|
labelKey: 'nav.buildPipeline',
|
|
168
179
|
icon: 'i-lucide-workflow',
|
|
169
180
|
surfaces: S('sidebar', 'command'),
|
|
170
|
-
advanced: true,
|
|
171
181
|
gate: (g) => g.canWriteBoard,
|
|
172
182
|
action: 'buildPipeline',
|
|
173
183
|
testId: 'nav-build-pipeline',
|
|
@@ -253,7 +263,6 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
|
|
|
253
263
|
labelKey: 'nav.infrastructure',
|
|
254
264
|
icon: 'i-lucide-server-cog',
|
|
255
265
|
surfaces: S('sidebar'),
|
|
256
|
-
advanced: true,
|
|
257
266
|
gate: (g) => g.infrastructureAvailable,
|
|
258
267
|
action: 'infrastructure',
|
|
259
268
|
testId: 'nav-infrastructure',
|
|
@@ -264,7 +273,6 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
|
|
|
264
273
|
labelKey: 'nav.environmentSetup',
|
|
265
274
|
icon: 'i-lucide-flask-conical',
|
|
266
275
|
surfaces: S('sidebar'),
|
|
267
|
-
advanced: true,
|
|
268
276
|
gate: (g) => g.infrastructureAvailable,
|
|
269
277
|
action: 'environmentSetup',
|
|
270
278
|
testId: 'nav-environment-setup',
|
|
@@ -275,7 +283,6 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
|
|
|
275
283
|
labelKey: 'nav.contextFragments',
|
|
276
284
|
icon: 'i-lucide-book-marked',
|
|
277
285
|
surfaces: S('sidebar', 'command'),
|
|
278
|
-
advanced: true,
|
|
279
286
|
gate: (g) => g.libraryAvailable && g.canManageSettings,
|
|
280
287
|
action: 'fragmentLibrary',
|
|
281
288
|
testId: 'nav-fragments',
|
|
@@ -381,7 +388,6 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
|
|
|
381
388
|
labelKey: 'nav.operatorDashboard',
|
|
382
389
|
icon: 'i-lucide-gauge',
|
|
383
390
|
surfaces: S('sidebar'),
|
|
384
|
-
advanced: true,
|
|
385
391
|
gate: (g) => g.accountsEnabled && g.isAccountAdmin,
|
|
386
392
|
action: 'operatorDashboard',
|
|
387
393
|
testId: 'nav-operator-dashboard',
|
|
@@ -392,7 +398,6 @@ export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
|
|
|
392
398
|
labelKey: 'nav.reports',
|
|
393
399
|
icon: 'i-lucide-chart-column',
|
|
394
400
|
surfaces: S('sidebar'),
|
|
395
|
-
advanced: true,
|
|
396
401
|
gate: (g) => g.accountsEnabled && g.isAccountAdmin,
|
|
397
402
|
action: 'reports',
|
|
398
403
|
testId: 'nav-reports',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.170.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",
|