@aaqu/fromcubes-portal-react 0.1.0-alpha.5 → 0.1.0-alpha.7

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 CHANGED
@@ -5,23 +5,26 @@ React portal node for Node-RED. Server-side JSX transpilation via esbuild. Tailw
5
5
  ## How it works
6
6
 
7
7
  ```
8
- ┌─ Deploy time (Node-RED server) ─────────────────────────┐
8
+ ┌─ Deploy time (Node-RED server) ──────────────────────────┐
9
9
  │ │
10
- │ JSX (editor) ──► esbuild transpile ──► cached JS
10
+ │ JSX (editor) ──► esbuild transpile ──► cached JS
11
11
  │ (hash-keyed cache) │
12
12
  │ │
13
+ │ Tailwind classes ──► server-side compile ──► CSS file │
14
+ │ (hash-keyed cache) │
15
+ │ │
13
16
  │ Unchanged code on redeploy = cache hit, 0ms │
14
17
  │ Changed code = retranspile, ~5ms │
15
18
  └──────────────────────────────────────────────────────────┘
16
19
 
17
- ┌─ Runtime (browser) ─────────────────────────────────────┐
20
+ ┌─ Runtime (browser) ──────────────────────────────────────┐
18
21
  │ │
19
22
  │ GET /endpoint ──► HTML + pre-compiled JS │
20
23
  │ react-19.production.min.js │
21
- │ Tailwind CSS (CDN)
24
+ │ Tailwind CSS (server-compiled)
22
25
  │ NO Babel, NO Sucrase, NO compiler │
23
26
  │ │
24
- │ WebSocket /endpoint/_ws ◄──► Node-RED msg I/O
27
+ │ WebSocket /endpoint/_ws ◄──► Node-RED msg I/O
25
28
  └──────────────────────────────────────────────────────────┘
26
29
  ```
27
30
 
@@ -49,9 +52,8 @@ Dependencies install automatically. No build step needed.
49
52
  | Field | Purpose |
50
53
  |---|---|
51
54
  | Endpoint | HTTP path, e.g. `/dashboard` |
52
- | Title | Browser tab title |
53
- | Input Schema | JSON documents expected `msg.payload` shape |
54
- | Output Schema | JSON — documents emitted `msg.payload` shape |
55
+ | Page Title | Browser tab title |
56
+ | Portal Auth | Enable portal user header extraction |
55
57
  | Head HTML | Extra `<head>` tags (CDN, fonts, CSS) |
56
58
  | Code Editor | Monaco with JSX — must define `<App />` |
57
59
 
@@ -72,13 +74,30 @@ Components are auto-injected into every portal-react page at transpile time.
72
74
 
73
75
  ```jsx
74
76
  function App() {
75
- const { data, send } = useNodeRed();
77
+ const { data, send, user } = useNodeRed();
76
78
  // data = last msg.payload from input wire (reactive)
77
79
  // send(payload, topic?) = emit msg on output wire
80
+ // user = portal user object (when Portal Auth enabled), or null
78
81
  return <div className="p-4 text-lg">{JSON.stringify(data)}</div>;
79
82
  }
80
83
  ```
81
84
 
85
+ ## Portal Authentication
86
+
87
+ When **Portal Auth** is checked, the node extracts user identity from incoming request headers:
88
+
89
+ | Header | Field |
90
+ |---|---|
91
+ | `x-portal-user-id` | `userId` |
92
+ | `x-portal-user-name` | `userName` |
93
+ | `x-portal-user-username` | `username` |
94
+ | `x-portal-user-email` | `email` |
95
+ | `x-portal-user-role` | `role` |
96
+ | `x-portal-user-groups` | `groups` (JSON array) |
97
+
98
+ - In the browser, `useNodeRed().user` returns the extracted user object (or `null` if auth is disabled or no headers present).
99
+ - On outgoing messages, user info is attached as `msg._client` so downstream nodes can identify the sender.
100
+
82
101
  ## Deploy lifecycle
83
102
 
84
103
  What happens on each deploy:
@@ -92,7 +111,7 @@ What happens on each deploy:
92
111
  - Cache hit → reuse (0ms)
93
112
  - Cache miss → esbuild transpile (~5ms)
94
113
  6. New HTTP route and WS handler registered
95
- 7. Reconnecting clients get fresh page + current `lastPayload`
114
+ 7. Reconnecting clients soft-reconnect (no page reload) and receive current `lastPayload`
96
115
 
97
116
  Rapid deploys (user clicking deploy repeatedly) are safe:
98
117
  - `isClosing` flag prevents accepting new WS connections during teardown
@@ -110,7 +129,7 @@ Transpile errors:
110
129
  |---|---|
111
130
  | react-19.production.min.js | ~45 KB |
112
131
  | Your transpiled JS | ~1-5 KB |
113
- | Tailwind CSS (CDN) | cached |
132
+ | Tailwind CSS (server-compiled) | cached per content hash |
114
133
  | WebSocket bridge | <1 KB |
115
134
 
116
135
  No Babel, no Sucrase client, no Vue, no Vuetify, no Socket.IO.
@@ -291,8 +291,11 @@ module.exports = function (RED) {
291
291
  })
292
292
  : Promise.resolve("");
293
293
 
294
+ const contentHash = compiled.js ? hash(compiled.js) : "";
295
+
294
296
  pageState[endpoint] = {
295
297
  compiled,
298
+ contentHash,
296
299
  cssHashReady,
297
300
  pageTitle,
298
301
  wsPath,
@@ -389,6 +392,10 @@ module.exports = function (RED) {
389
392
  wsSend(ws, { type: "data", payload: lastPayload });
390
393
  }
391
394
 
395
+ // Send content version for deploy-reload detection
396
+ const contentHash = pageState[endpoint]?.contentHash || "";
397
+ wsSend(ws, { type: "version", hash: contentHash });
398
+
392
399
  ws.on("message", (raw) => {
393
400
  try {
394
401
  const msg = JSON.parse(raw.toString());
@@ -590,7 +597,7 @@ body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a0a;color:#e0
590
597
  <div id="__cs" class="err">disconnected</div>
591
598
  <script>
592
599
  window.__NR={
593
- _ws:null,_listeners:new Set(),_lastData:null,_retries:0,_wasConnected:false,_user:${user ? escScript(JSON.stringify(user)) : 'null'},
600
+ _ws:null,_listeners:new Set(),_lastData:null,_retries:0,_wasConnected:false,_version:null,_user:${user ? escScript(JSON.stringify(user)) : 'null'},
594
601
  connect(){
595
602
  const p=location.protocol==='https:'?'wss:':'ws:';
596
603
  const ws=new WebSocket(p+'//'+location.host+'${wsPath}');
@@ -601,8 +608,10 @@ window.__NR={
601
608
  s.textContent='connected';s.className='ok';this._retries=0;this._wasConnected=true;
602
609
  };
603
610
  ws.onmessage=(e)=>{
604
- try{const m=JSON.parse(e.data);if(m.type==='data'){this._lastData=m.payload;this._listeners.forEach(fn=>fn(m.payload));}}
605
- catch(err){console.error('WS parse',err);}
611
+ try{const m=JSON.parse(e.data);
612
+ if(m.type==='version'){if(this._version&&this._version!==m.hash){location.reload();return;}this._version=m.hash;}
613
+ if(m.type==='data'){this._lastData=m.payload;this._listeners.forEach(fn=>fn(m.payload));}
614
+ }catch(err){console.error('WS parse',err);}
606
615
  };
607
616
  ws.onclose=(e)=>{
608
617
  s.textContent='disconnected';s.className='err';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aaqu/fromcubes-portal-react",
3
- "version": "0.1.0-alpha.5",
3
+ "version": "0.1.0-alpha.7",
4
4
  "description": "Fromcubes Portal - React for Node-RED with Tailwind CSS and auto complete",
5
5
  "keywords": [
6
6
  "node-red",