@fluid-app/fluid-cli-portal 0.1.52 → 0.1.54
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 +3 -0
- package/dist/{backend-dev-plugin-B9-S4Is-.mjs → backend-dev-plugin-CY3yxgJ1.mjs} +1 -1
- package/dist/{backend-dev-plugin-B9-S4Is-.mjs.map → backend-dev-plugin-CY3yxgJ1.mjs.map} +1 -1
- package/dist/build-manifest.d.mts +91 -0
- package/dist/build-manifest.d.mts.map +1 -0
- package/dist/build-manifest.mjs +180 -0
- package/dist/build-manifest.mjs.map +1 -0
- package/dist/index.d.mts +657 -741
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1627 -1829
- package/dist/index.mjs.map +1 -1
- package/dist/portal-dev-plugin-CHlxpXc5.mjs +232 -0
- package/dist/portal-dev-plugin-CHlxpXc5.mjs.map +1 -0
- package/dist/portal-widget-dev-plugin-B53Lf9Ke.mjs +144 -0
- package/dist/portal-widget-dev-plugin-B53Lf9Ke.mjs.map +1 -0
- package/dist/pull-CUR8tbW9.mjs +1633 -0
- package/dist/pull-CUR8tbW9.mjs.map +1 -0
- package/dist/sdk.gen-DuPhfdxN.mjs +469 -0
- package/dist/sdk.gen-DuPhfdxN.mjs.map +1 -0
- package/dist/src-CpH5SpDH.mjs +2157 -0
- package/dist/src-CpH5SpDH.mjs.map +1 -0
- package/dist/vite-plugin.d.mts +15 -56
- package/dist/vite-plugin.d.mts.map +1 -1
- package/dist/vite-plugin.mjs +6 -3
- package/package.json +12 -3
- package/templates/base/.gitignore.template +6 -0
- package/templates/base/.oxlintrc.json +1 -2
- package/templates/base/AGENTS.md +4 -0
- package/templates/base/skills/fluid-portal-authoring/SKILL.md +238 -10
- package/templates/starter/.env.example +4 -3
- package/templates/starter/README.md.template +14 -1
- package/dist/portal-dev-plugin-BHE8Z4yr.mjs +0 -289
- package/dist/portal-dev-plugin-BHE8Z4yr.mjs.map +0 -1
- package/dist/pull-XOsFaqz2.mjs +0 -1204
- package/dist/pull-XOsFaqz2.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluid-app/fluid-cli-portal",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.54",
|
|
4
4
|
"description": "Fluid CLI plugin for building portal applications",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
"./vite-plugin": {
|
|
18
18
|
"import": "./dist/vite-plugin.mjs",
|
|
19
19
|
"types": "./dist/vite-plugin.d.mts"
|
|
20
|
+
},
|
|
21
|
+
"./build-manifest": {
|
|
22
|
+
"import": "./dist/build-manifest.mjs",
|
|
23
|
+
"types": "./dist/build-manifest.d.mts"
|
|
20
24
|
}
|
|
21
25
|
},
|
|
22
26
|
"publishConfig": {
|
|
@@ -40,20 +44,25 @@
|
|
|
40
44
|
"@swc/jest": "^0.2.39",
|
|
41
45
|
"@types/fs-extra": "^11.0.4",
|
|
42
46
|
"@types/jest": "^29.5.14",
|
|
47
|
+
"@types/node": "24.10.12",
|
|
43
48
|
"@types/prompts": "^2.4.9",
|
|
44
49
|
"jest": "^29.7.0",
|
|
45
50
|
"tsdown": "^0.21.0",
|
|
46
51
|
"typescript": "^5",
|
|
52
|
+
"@fluid-app/fluid-cli-widget-tooling": "0.1.0",
|
|
47
53
|
"@fluid-app/api-client-core": "0.1.0",
|
|
48
|
-
"@fluid-app/portal-core": "0.1.23",
|
|
49
54
|
"@fluid-app/fluidos-api-client": "0.1.0",
|
|
50
|
-
"@fluid-app/
|
|
55
|
+
"@fluid-app/portal-core": "0.1.23",
|
|
56
|
+
"@fluid-app/portal-sdk": "0.1.463",
|
|
57
|
+
"@fluid-app/typescript-config": "0.0.0",
|
|
58
|
+
"@fluid-app/portal-widgets": "0.1.22"
|
|
51
59
|
},
|
|
52
60
|
"engines": {
|
|
53
61
|
"node": ">=24.0.0"
|
|
54
62
|
},
|
|
55
63
|
"scripts": {
|
|
56
64
|
"build": "tsdown",
|
|
65
|
+
"postbuild": "node scripts/verify-dist-entries.mjs",
|
|
57
66
|
"dev": "tsdown --watch",
|
|
58
67
|
"lint": "oxlint",
|
|
59
68
|
"lint:fix": "oxlint --fix",
|
|
@@ -23,6 +23,12 @@ Thumbs.db
|
|
|
23
23
|
# Portal sync metadata (generated by fluid portal pull/push)
|
|
24
24
|
.portal-sync/
|
|
25
25
|
|
|
26
|
+
# Local tooling state — never belongs in the portal repo.
|
|
27
|
+
# .mist-desktop/ holds Mist Desktop's private chats, attachments, and
|
|
28
|
+
# screenshots for this project; .mist is its clone/slug marker.
|
|
29
|
+
.mist
|
|
30
|
+
.mist-desktop/
|
|
31
|
+
|
|
26
32
|
# Logs
|
|
27
33
|
*.log
|
|
28
34
|
pnpm-debug.log*
|
package/templates/base/AGENTS.md
CHANGED
|
@@ -30,6 +30,10 @@ Do not edit `.portal-sync/` by hand. It is generated sync metadata.
|
|
|
30
30
|
|
|
31
31
|
## Quality bar
|
|
32
32
|
|
|
33
|
+
- **Every screen needs a container root.** A screen's `component_tree` must hold exactly one `ContainerWidget` / `LayoutWidget` / `CardWidget` at the top, with all other widgets inside its `props.children`. Without it the screen still renders, but the admin visual builder has no drop zones and a human cannot drag anything onto the page. See the `fluid-portal-authoring` skill.
|
|
34
|
+
- **Choose the right system before building.** A company portal widget runs in a locked-down worker with no network access, so it can only present Fluid's own data (via capabilities or data sources) or props. Anything needing an API key, OAuth, webhooks, or server-side work is a **Mist app** — a hosted app with its own `public_url`, surfaced through a droplet (identity/credentials) plus a placement record, and embedded by **the Mist's public URL**. The droplet is the app's identity, not the app. See the `fluid-portal-authoring` skill.
|
|
35
|
+
- **Droplet widgets are registered widget types, not embeds.** Use `droplet.<scope>.<dropletId>.<Name>` with real props. Do not point an `EmbedWidget` at a droplet's `/embed` route — that is the `?dri=`-gated admin surface and will render blank.
|
|
36
|
+
- **Do not infer that a widget type does not exist because an API did not list it.** Widget-related endpoints are scoped differently and legitimately return empty. When unsure of a `type` or prop shape, ask the human to drag the widget onto a scratch screen in the admin builder, then `pnpm pull` and read the truth.
|
|
33
37
|
- Keep portal JSON valid and references consistent.
|
|
34
38
|
- Preserve stable IDs and slugs unless the change intentionally renames them.
|
|
35
39
|
- Keep the portal shell thin; do not fork SDK internals into this app.
|
|
@@ -60,15 +60,191 @@ Guidelines:
|
|
|
60
60
|
- Do not hand-edit `.portal-sync/`; it is sync metadata, not source content.
|
|
61
61
|
- Do not invent unsupported fields. Match the shapes produced by `pnpm pull`.
|
|
62
62
|
|
|
63
|
+
## Screen structure: every screen needs a container root
|
|
64
|
+
|
|
65
|
+
**Rule: a screen's `component_tree` must hold exactly one container node at the top, and every other widget must be inside its `props.children`.**
|
|
66
|
+
|
|
67
|
+
The container types are `ContainerWidget`, `LayoutWidget`, and `CardWidget`. `ContainerWidget` is the default choice for a page root; the admin builder writes one on every screen it authors.
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"name": "UGC",
|
|
72
|
+
"component_tree": [
|
|
73
|
+
{
|
|
74
|
+
"id": "ContainerWidget-ugc-root",
|
|
75
|
+
"type": "ContainerWidget",
|
|
76
|
+
"props": {
|
|
77
|
+
"gapSize": "md",
|
|
78
|
+
"padding": 4,
|
|
79
|
+
"children": [{ "id": "…", "type": "…", "columnIndex": 0, "props": {} }]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
A screen with bare top-level widgets and no container root **renders correctly in preview and in the live portal**, so nothing you can see locally will tell you it is wrong. What breaks is the admin visual builder: only container widgets receive the edit-mode child-management callbacks (`onAddChild`, `onWidgetSelect`, child delete/duplicate), so a screen without a container root offers **no drop zones**. A human opening that screen in the builder cannot drag anything onto it.
|
|
87
|
+
|
|
88
|
+
This is the single most likely defect in an agent-authored screen, because the failure is invisible from every surface an agent can check.
|
|
89
|
+
|
|
90
|
+
- Put a container root on every screen you create, without exception.
|
|
91
|
+
- Only these three types are containers by default. A `NestedWidget`, `SpacerWidget`, or any other widget at the root does not satisfy this rule.
|
|
92
|
+
- Containers nest. A column layout inside the root is `LayoutWidget` in the root's `children`.
|
|
93
|
+
- When mirroring an existing screen, copy its root container rather than lifting its children out.
|
|
94
|
+
|
|
63
95
|
## Add a new page and put it in the menu
|
|
64
96
|
|
|
65
97
|
A page = a **screen** (a `component_tree` of widgets) + a **navigation item** that points at it. A screen with no nav item is unreachable; a nav item whose screen reference doesn't resolve is silently dropped in local preview and refused at push. Both live in `portal/`.
|
|
66
98
|
|
|
67
|
-
1. Create `portal/screens/<slug>.json`: `{ "name": "Rewards", "component_tree": [ ... ] }`. Give every node an `id` and a widget `type` from the catalog below.
|
|
68
|
-
2. Add a navigation item to each profile that should see it: `{ "label": "Rewards", "slug": "rewards", "
|
|
69
|
-
3. Link to it with a `
|
|
99
|
+
1. Create `portal/screens/<slug>.json`: `{ "name": "Rewards", "component_tree": [ ... ] }`. Give every node an `id` and a widget `type` from the catalog below. The **filename is the resource slug**; an inline `slug` field is optional and is not what cross-resource resolution uses.
|
|
100
|
+
2. Add a navigation item to each profile that should see it. Local JSON uses the screen **slug**, not a numeric backend id: `{ "label": "Rewards", "slug": "rewards", "screen": "rewards", "source": "user", "position": 2, "children": [] }`. Use `children[]` for a nested group. Add it to the profile's `mobile_navigation` too for mobile.
|
|
101
|
+
3. Link to it with a `LinkWidget` in screen mode. Use the screen slug rather
|
|
102
|
+
than a URL path so the portal shell handles navigation. `LinkWidget` props for the example above:
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"linkType": "screen",
|
|
107
|
+
"screenSlug": "rewards"
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Carousel slides use their separate `buttonLink` property; set that to the
|
|
112
|
+
path `/rewards`.
|
|
113
|
+
|
|
70
114
|
4. Preview on the profile local dev serves (default/first), then push and activate a version to go live.
|
|
71
115
|
|
|
116
|
+
### Bootstrapping a brand-new empty definition
|
|
117
|
+
|
|
118
|
+
`fluid portal pull` legitimately returns zero screens/themes/navigations/profiles for a newly created portal. Do not search for hidden starter resources and do not move the routes into `src/`. Create a complete minimal definition locally:
|
|
119
|
+
|
|
120
|
+
`portal/screens/home.json`
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"name": "Home",
|
|
125
|
+
"component_tree": [
|
|
126
|
+
{
|
|
127
|
+
"id": "ContainerWidget-home-root",
|
|
128
|
+
"type": "ContainerWidget",
|
|
129
|
+
"props": {
|
|
130
|
+
"background": {
|
|
131
|
+
"type": "solid",
|
|
132
|
+
"color": "background"
|
|
133
|
+
},
|
|
134
|
+
"gapSize": "md",
|
|
135
|
+
"padding": 4,
|
|
136
|
+
"children": [
|
|
137
|
+
{
|
|
138
|
+
"id": "TextWidget-home-welcome",
|
|
139
|
+
"type": "TextWidget",
|
|
140
|
+
"columnIndex": 0,
|
|
141
|
+
"props": {
|
|
142
|
+
"title": "Welcome",
|
|
143
|
+
"titleColor": "foreground",
|
|
144
|
+
"description": "Your portal is ready.",
|
|
145
|
+
"descriptionColor": "foreground",
|
|
146
|
+
"background": {
|
|
147
|
+
"type": "solid",
|
|
148
|
+
"color": "background"
|
|
149
|
+
},
|
|
150
|
+
"padding": 4,
|
|
151
|
+
"borderRadius": "md"
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`portal/navigations/main.json` (create a parallel `mobile.json` with `"platform": "mobile"`)
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"name": "Main Navigation",
|
|
166
|
+
"platform": "web",
|
|
167
|
+
"navigation_items": [
|
|
168
|
+
{
|
|
169
|
+
"label": "Home",
|
|
170
|
+
"slug": "home",
|
|
171
|
+
"icon": "home",
|
|
172
|
+
"position": 1,
|
|
173
|
+
"screen": "home",
|
|
174
|
+
"source": "user",
|
|
175
|
+
"parent_id": null,
|
|
176
|
+
"children": []
|
|
177
|
+
}
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`portal/profiles/default.json`
|
|
183
|
+
|
|
184
|
+
```json
|
|
185
|
+
{
|
|
186
|
+
"name": "Default Profile",
|
|
187
|
+
"default": true,
|
|
188
|
+
"permissions": {
|
|
189
|
+
"countries": [],
|
|
190
|
+
"ranks": [],
|
|
191
|
+
"roles": [],
|
|
192
|
+
"platform": []
|
|
193
|
+
},
|
|
194
|
+
"navigation": "main",
|
|
195
|
+
"mobile_navigation": "mobile",
|
|
196
|
+
"themes": []
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Create additional screen files and add their slug to both navigations before previewing. `fluid portal push` creates files listed as new, records their backend mappings, then creates navigations and profiles in dependency order. “No mapping found” applies to a file incorrectly treated as changed/deleted, not to a valid new file.
|
|
201
|
+
|
|
202
|
+
If you create `portal/themes/<slug>.json`, it must contain the complete
|
|
203
|
+
`{ "name", "active", "config" }` shape. A theme with only `name` and
|
|
204
|
+
`active` can appear harmless in local preview but the API rejects it. Copy a
|
|
205
|
+
pulled theme's full `config` object and then change its `id`, `name`, and token
|
|
206
|
+
values; do not invent a partial config.
|
|
207
|
+
|
|
208
|
+
## Decide first: portal widget, or Mist app?
|
|
209
|
+
|
|
210
|
+
Before building anything custom, work out which system the feature belongs to. This is the most expensive decision to get wrong in portal work, and the deciding question is not "internal or external data" — it is **does this need a server that can hold a secret.**
|
|
211
|
+
|
|
212
|
+
### A company portal widget cannot reach the network
|
|
213
|
+
|
|
214
|
+
Portal widgets run as Remote DOM packages inside a locked-down Web Worker. `fetch`, `XMLHttpRequest`, `WebSocket`, `EventSource`, `localStorage`, `indexedDB` and more are removed before widget code runs, and no capability re-grants them. A portal widget's data comes from exactly three places:
|
|
215
|
+
|
|
216
|
+
- props written into the screen JSON
|
|
217
|
+
- built-in host capabilities — `account`, `store`, `products`, `content`, `mySite`, `todos`, `calendar`, `points`, `localization`, and friends
|
|
218
|
+
- **data sources**, which the host resolves and passes into props (`api` with a Fluid preset, `custom` for hand-picked Fluid resources, `static` for literal data)
|
|
219
|
+
|
|
220
|
+
### A Mist app is the app; the droplet is its identity
|
|
221
|
+
|
|
222
|
+
A Mist app is a hosted application with its own backend and a `public_url`. It can hold secrets, call any external API, receive webhooks, and do server-side work. Fluid surfaces it through integration records that **all point at that same `public_url`**:
|
|
223
|
+
|
|
224
|
+
| Record | What it is | Per mist |
|
|
225
|
+
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------- |
|
|
226
|
+
| **Droplet** | Identity and credentials — `FLUID_DROPLET_UUID` / `_SECRET` / `_WEBHOOK_AUTH_TOKEN`, OAuth scopes, install lifecycle and webhooks | one |
|
|
227
|
+
| **Mobile embed** | A placement in the mobile app: `embed_url` plus a required cover image and height | many |
|
|
228
|
+
| **Drop zone** | A placement in an admin page/zone slot | many |
|
|
229
|
+
|
|
230
|
+
**The droplet is not the app.** It is the app's identity and permission record. The Mist app is the thing that runs, and it is what an embed points at.
|
|
231
|
+
|
|
232
|
+
### The decision
|
|
233
|
+
|
|
234
|
+
1. **Needs an API key, OAuth, webhooks, background jobs, or any server-side work** → build a **Mist app**. Attach a droplet for credentials and install lifecycle, add a placement surface, and embed the **Mist's public URL**.
|
|
235
|
+
2. **Presents Fluid's own data** (orders, products, account, shares, subscriptions, content) → build a **company portal widget** and read it through a capability or a preset data source. No hosting, no secrets, no deploy pipeline.
|
|
236
|
+
3. **Presents public, unauthenticated, CORS-enabled JSON and nothing more** → a portal widget with an `api` data source pointing at an absolute endpoint works. The host fetches it, so it runs in the browser and cannot hold a credential — the moment authentication enters, this becomes case 1.
|
|
237
|
+
|
|
238
|
+
Ask the user which of these their feature is when it is not obvious from the request. A feature that "pulls in data from <third-party service>" is case 1 essentially every time.
|
|
239
|
+
|
|
240
|
+
### Embedding a Mist app in a portal screen
|
|
241
|
+
|
|
242
|
+
Point the embed at the **Mist's public URL**, or at a purpose-built widget route the Mist app serves. Those routes are built to be iframed and need no installation parameter.
|
|
243
|
+
|
|
244
|
+
**Do not point an embed at the droplet's `/embed` route.** That is the admin dashboard surface and requires a `?dri=` installation parameter; without one it renders "Missing installation" or a blank frame.
|
|
245
|
+
|
|
246
|
+
If the Mist app already publishes registered widget types (see below), prefer those over an iframe embed — they compose properly with the screen and the builder.
|
|
247
|
+
|
|
72
248
|
## Choosing a widget (recommend built-ins before custom code)
|
|
73
249
|
|
|
74
250
|
Use the `type` value in a `component_tree` node. Reach for what already exists:
|
|
@@ -77,11 +253,47 @@ Use the `type` value in a `component_tree` node. Reach for what already exists:
|
|
|
77
253
|
- Hero/media: `CarouselWidget` (rotating hero slides w/ CTA), `ImageWidget`, `VideoWidget`
|
|
78
254
|
- Content: `TextWidget`, `BulletListWidget`, `CardWidget`, `AlertWidget`, `TableWidget`, `ChartWidget`, `CalendarWidget`
|
|
79
255
|
- Commerce/member: `ShopWidget`, `PointsWidget` (rewards balance), `RecentActivityWidget`, `ToDoWidget`
|
|
80
|
-
- Links/sharing: `LinkWidget`, `
|
|
81
|
-
- Platform: `EmbedWidget` (
|
|
256
|
+
- Links/sharing: `LinkWidget`, `QuickLinksWidget`, `QuickShareWidget`, `ListWidget`
|
|
257
|
+
- Platform: `EmbedWidget` (iframes an arbitrary URL into a screen), `MySiteWidget`
|
|
258
|
+
|
|
259
|
+
`TextWidget` renders its `title` and `description` as plain text. Do not put
|
|
260
|
+
HTML in either field; markup is escaped and displayed literally.
|
|
82
261
|
|
|
83
262
|
An unregistered `type` renders nothing. Data-driven UI a built-in can't express (a member-specific dashboard) needs a custom widget package — a separate concern from this definition-editing workflow; note it to the user rather than hand-rolling code here.
|
|
84
263
|
|
|
264
|
+
### Droplet widgets are real widget types, not embeds
|
|
265
|
+
|
|
266
|
+
An installed droplet (UGC, and others) contributes **registered widget types**, addressed exactly like any other widget:
|
|
267
|
+
|
|
268
|
+
```json
|
|
269
|
+
{
|
|
270
|
+
"id": "…",
|
|
271
|
+
"type": "droplet.ugc.drp_fuwamfg3licz1l4yocpkjos12t9vhcrh.MakeAVideoCta",
|
|
272
|
+
"props": {
|
|
273
|
+
"headline": "…",
|
|
274
|
+
"eyebrow": "…",
|
|
275
|
+
"ctaLabel": "…",
|
|
276
|
+
"dri": "",
|
|
277
|
+
"apiBaseUrl": "…"
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
The shape is `droplet.<scope>.<dropletId>.<WidgetName>`, and the props are ordinary JSON the builder writes.
|
|
283
|
+
|
|
284
|
+
**Do not reach for `EmbedWidget` to render a droplet's UI.** `EmbedWidget` iframes a URL, and a droplet's `/embed` route is the _admin dashboard_ surface, which requires a `?dri=` installation parameter — pointing an `EmbedWidget` at it renders "Missing installation" or a blank frame. That mistake looks reasonable and fails quietly.
|
|
285
|
+
|
|
286
|
+
**Absence from an API is not evidence a widget type does not exist.** These endpoints are scoped differently and will each come back empty or 404 for a droplet widget that is installed and working:
|
|
287
|
+
|
|
288
|
+
| Endpoint | Why it looks empty |
|
|
289
|
+
| ---------------------------------- | -------------------------------------------------------- |
|
|
290
|
+
| `/api/droplets` (`app_extensions`) | Does not list contributed widget types |
|
|
291
|
+
| `/__widget-packages__` | Dev-server route for _unpublished company_ packages only |
|
|
292
|
+
| `/api/app/widget-packages` | Portal-session scoped; 404s for a CLI token |
|
|
293
|
+
| `/api/company/mobile_widgets` | **This one lists installed droplet widgets** |
|
|
294
|
+
|
|
295
|
+
**When you cannot confirm a widget's type or prop shape from an API, ask the human to drag it onto a scratch screen in the admin builder and then `pnpm pull`.** The pulled JSON is ground truth for both the `type` string and the exact props. Do this instead of inferring from absence, and do it early — it costs one message and replaces a chain of confident guesses.
|
|
296
|
+
|
|
85
297
|
## System screens and how to reach them
|
|
86
298
|
|
|
87
299
|
The portal ships built-in screens addressed by nav slug: `profile` (alias `account`), `orders`, `subscriptions`, `messaging`, `contacts`, `shop`, `customers`, `my-site`, `share/*`, `app-download`. To expose one, add a navigation item with that slug to the profile — `messaging` and `contacts` only appear when a nav item includes them, and `messages`/`my-site` are rep-only (a non-rep member sees the fallback). These are core surfaces the admin builder keeps; author your own pages alongside them in `portal/` and let the definition stay the source of truth so drag-and-drop editing keeps working.
|
|
@@ -111,8 +323,10 @@ Expected result: the portal CLI pulls the definition when `portal/` is missing,
|
|
|
111
323
|
|
|
112
324
|
Two local-preview rules that read as bugs if you do not know them:
|
|
113
325
|
|
|
326
|
+
- `fluid portal dev` injects `portalDevPlugin()` and `backendDevPlugin()` programmatically. Do not add either plugin to `vite.config.ts`; doing so creates scaffold drift and is unnecessary. If the command falls back to bare Vite, fix the dependency/config error that triggered the fallback instead of permanently wiring the plugin.
|
|
327
|
+
- With no `VITE_API_URL` override, the CLI resolves the signed-in company and proxies `/api` to `https://<subdomain>.portal.fluid.app`. Portal member endpoints do **not** live on `api.fluid.app` or `<subdomain>.fluid.app`; repeated 404s from those hosts are a proxy-target error.
|
|
114
328
|
- The local manifest always serves the profile with `default: true` (or the first profile file if none is default). Profile `permissions` are **not** evaluated locally — edits to any other profile will never appear in the local preview. Permission matching only happens on the deployed portal against the real logged-in member.
|
|
115
|
-
-
|
|
329
|
+
- A direct localhost preview does **not** have a real portal-member session. The tenant BFF authenticates with an HttpOnly `portal_tenant_user_id` cookie created by the production/Rails handoff, not the Fluid CLI token. Local custom pages, navigation, and definition content still work, but Shop, Orders, Contacts, rep-only screens, and other member-data surfaces may return 401 or remain unauthenticated. A host fix that merely changes an error boundary into permanent skeletons is not successful authentication. Verify signed-in behavior through a real tenant handoff environment.
|
|
116
330
|
|
|
117
331
|
## Push definition changes
|
|
118
332
|
|
|
@@ -147,6 +361,11 @@ pnpm exec fluid portal version create --activate
|
|
|
147
361
|
|
|
148
362
|
Use this only when the pushed definition is ready for users. If activation should be coordinated with a release or content review, stop and ask the project owner before running it.
|
|
149
363
|
|
|
364
|
+
For an explicitly approved non-interactive release, use
|
|
365
|
+
`fluid portal push --yes --activate`. It performs the same two stages but
|
|
366
|
+
activates only after every push phase succeeds; malformed or partially-pushed
|
|
367
|
+
definitions exit non-zero and are not activated.
|
|
368
|
+
|
|
150
369
|
## Distinguish the three deploy/sync paths
|
|
151
370
|
|
|
152
371
|
Do not mix these up:
|
|
@@ -175,10 +394,14 @@ Work these in order and report which one failed:
|
|
|
175
394
|
|
|
176
395
|
1. Logged in? Auth errors say `Run fluid login first`.
|
|
177
396
|
2. Right directory? Push errors about missing `portal/` or `.portal-sync/` mean you are not in the pulled project (or never pulled).
|
|
178
|
-
3.
|
|
179
|
-
4.
|
|
180
|
-
5.
|
|
181
|
-
6.
|
|
397
|
+
3. Built-in screens returning 404? Confirm the proxy target is `<subdomain>.portal.fluid.app`, not `api.fluid.app` or `<subdomain>.fluid.app`.
|
|
398
|
+
4. Built-in screens returning 401 or permanent skeletons? Localhost lacks the portal handoff cookie; use a real tenant handoff environment for authenticated-member verification.
|
|
399
|
+
5. Local preview showing stale/missing definition content? Check for malformed JSON or broken references in the file you last edited (local preview drops them silently), and confirm you edited the profile the local preview serves (`default: true`).
|
|
400
|
+
6. Push refused? Read the cross-reference validation errors — they name the file and the missing slug.
|
|
401
|
+
7. Push partially failed? Fix the reported phase error and rerun; the snapshot only advanced for files that succeeded.
|
|
402
|
+
8. Users do not see the change? Push updates the draft only — create/activate a version.
|
|
403
|
+
9. Screen renders fine but the admin builder shows no drop zones on it? The screen is missing its container root. See "Screen structure: every screen needs a container root".
|
|
404
|
+
10. A widget renders blank or says "Missing installation"? You likely used `EmbedWidget` against a droplet's `/embed` route instead of the droplet's registered widget `type`.
|
|
182
405
|
|
|
183
406
|
## Widget work inside a portal project
|
|
184
407
|
|
|
@@ -209,4 +432,9 @@ Before considering portal work complete:
|
|
|
209
432
|
- [ ] Kept portal structure, routes, and content changes in pulled Fluid OS JSON under `portal/`.
|
|
210
433
|
- [ ] Ran typecheck/lint/build or the closest available checks.
|
|
211
434
|
- [ ] Ran push only when the local `portal/` diff was understood.
|
|
435
|
+
- [ ] **Every screen created or modified has a single container root (`ContainerWidget` / `LayoutWidget` / `CardWidget`) with all other widgets in its `props.children`.** Preview cannot detect this; check the JSON.
|
|
436
|
+
- [ ] Chose portal widget vs Mist app deliberately — anything needing a secret, OAuth, webhooks, or server-side work is a Mist app, not a portal widget.
|
|
437
|
+
- [ ] Any embed points at a Mist's public URL or a widget route, never at a droplet's `?dri=`-gated `/embed`.
|
|
438
|
+
- [ ] Used registered widget `type` values for droplet/company widgets, not an `EmbedWidget` pointed at a droplet route.
|
|
439
|
+
- [ ] Confirmed any uncertain widget `type` or prop shape against builder-authored JSON rather than inferring it.
|
|
212
440
|
- [ ] Created/activated a version only when the definition should go live.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Fluid API host. LEAVE UNSET for local development: `fluid portal dev`
|
|
2
|
-
# forces requests same-origin and
|
|
3
|
-
# https
|
|
4
|
-
#
|
|
2
|
+
# forces requests same-origin and resolves the signed-in company's tenant BFF
|
|
3
|
+
# (`https://<subdomain>.portal.fluid.app`). Setting an absolute URL here only
|
|
4
|
+
# changes which API the dev proxy targets; it does not create an authenticated
|
|
5
|
+
# portal-member handoff session.
|
|
5
6
|
# VITE_API_URL=
|
|
6
7
|
|
|
7
8
|
# Optional: override the Fluid CLI auth token for this project.
|
|
@@ -35,7 +35,20 @@ pnpm push # fluid portal push
|
|
|
35
35
|
pnpm widget:create <name> # scaffold a company-owned portal widget
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
When `VITE_API_URL` is unset, `fluid portal dev` resolves the signed-in
|
|
39
|
+
company and proxies `/api` to its tenant BFF at
|
|
40
|
+
`https://<subdomain>.portal.fluid.app`. Set `VITE_API_URL` in `.env` only when
|
|
41
|
+
you need a different API host. The override changes routing, not
|
|
42
|
+
authentication.
|
|
43
|
+
|
|
44
|
+
Local preview always serves your local `portal/` manifest, custom pages, and
|
|
45
|
+
navigation. Built-in screens that load member data also require the
|
|
46
|
+
`portal_tenant_user_id` session cookie created by the production/Rails handoff.
|
|
47
|
+
A direct localhost preview cannot create or read that HttpOnly tenant cookie,
|
|
48
|
+
so Shop, Orders, Contacts, and other member-data screens may return HTTP 401 or
|
|
49
|
+
remain in an unauthenticated state. Do not treat persistent skeletons as proof
|
|
50
|
+
that authenticated data works; verify those screens through a real tenant
|
|
51
|
+
handoff environment.
|
|
39
52
|
|
|
40
53
|
## Project structure
|
|
41
54
|
|