tina4-nodejs 3.13.111 → 3.13.112

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.
@@ -706,6 +706,11 @@ export class DevAdmin {
706
706
  { method: "GET", pattern: "/__dev/api/docs/.well-known.json", handler: handleDocsWellKnown },
707
707
  // JS asset
708
708
  { method: "GET", pattern: "/__dev/js/tina4-dev-admin.min.js", handler: handleDevAdminJs },
709
+ // Dev-toolbar assets — served as external CSS/JS so the injected toolbar is
710
+ // CSP-clean (no inline style=, onclick=, or <script>) under a strict
711
+ // `default-src 'self'`. Parity with PHP's /__dev/toolbar.css + toolbar.js.
712
+ { method: "GET", pattern: "/__dev/toolbar.css", handler: handleToolbarCss },
713
+ { method: "GET", pattern: "/__dev/toolbar.js", handler: handleToolbarJs },
709
714
  ];
710
715
 
711
716
  for (const route of routes) {
@@ -767,6 +772,7 @@ export class DevAdmin {
767
772
  matchedPattern: string;
768
773
  requestId: string;
769
774
  routeCount: number;
775
+ reload?: boolean;
770
776
  }): string {
771
777
  return renderToolbarHtml(ctx);
772
778
  }
@@ -2978,6 +2984,7 @@ function renderToolbarHtml(ctx: {
2978
2984
  matchedPattern: string;
2979
2985
  requestId: string;
2980
2986
  routeCount: number;
2987
+ reload?: boolean;
2981
2988
  }): string {
2982
2989
  const nodeVersion = process.version;
2983
2990
  // DEVADMIN-DEC-04: the toolbar is injected into every text/html response
@@ -2987,47 +2994,140 @@ function renderToolbarHtml(ctx: {
2987
2994
  const method = escapeHtml(ctx.method);
2988
2995
  const path = escapeHtml(ctx.path);
2989
2996
  const matchedPattern = escapeHtml(ctx.matchedPattern);
2990
- return `<div id="tina4-dev-toolbar" style="position:fixed;bottom:0;left:0;right:0;background:#333;color:#fff;font-family:monospace;font-size:12px;padding:6px 16px;z-index:99999;display:flex;align-items:center;gap:16px;">
2991
- <span id="tina4-ver-btn" style="color:#2e7d32;font-weight:bold;cursor:pointer;text-decoration:underline dotted;" onclick="tina4VersionModal()" title="Click to check for updates">Tina4 v${ctx.version}</span>
2992
- <div id="tina4-ver-modal" style="display:none;position:fixed;bottom:3rem;left:1rem;background:#1e1e2e;border:1px solid #2e7d32;border-radius:8px;padding:16px 20px;z-index:100000;min-width:320px;box-shadow:0 8px 32px rgba(0,0,0,0.5);font-family:monospace;font-size:13px;color:#cdd6f4;">
2993
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
2994
- <strong style="color:#89b4fa;">Version Info</strong>
2995
- <span onclick="document.getElementById('tina4-ver-modal').style.display='none'" style="cursor:pointer;color:#888;">&times;</span>
2997
+ // CSP-clean toolbar: the framework serves a strict `default-src 'self'`, so the
2998
+ // toolbar carries NO inline style=, NO onclick=, and NO inline <script>. The CSS
2999
+ // and JS load as external assets from /__dev/toolbar.css and /__dev/toolbar.js.
3000
+ // The reloader is suppressed on the AI/stable port by data-reload="0" (the JS
3001
+ // early-returns when it is not "1"). Mirrors PHP DevAdmin::renderToolbar().
3002
+ const reload = ctx.reload === false ? "0" : "1";
3003
+ return `<link rel="stylesheet" href="/__dev/toolbar.css">
3004
+ <div id="tina4-dev-toolbar" data-reload="${reload}">
3005
+ <span id="tina4-ver-btn" title="Click to check for updates">Tina4 v${ctx.version}</span>
3006
+ <div id="tina4-ver-modal">
3007
+ <div class="t4-modal-head">
3008
+ <strong class="t4-modal-title">Version Info</strong>
3009
+ <span id="tina4-ver-close" class="t4-x">&times;</span>
2996
3010
  </div>
2997
- <div id="tina4-ver-body" style="line-height:1.8;">
2998
- <div>Current: <strong style="color:#a6e3a1;">v${ctx.version}</strong></div>
2999
- <div id="tina4-ver-latest" style="color:#888;">Checking for updates...</div>
3011
+ <div id="tina4-ver-body">
3012
+ <div>Current: <strong class="t4-ok">v${ctx.version}</strong></div>
3013
+ <div id="tina4-ver-latest" class="t4-dim">Checking for updates...</div>
3000
3014
  </div>
3001
3015
  </div>
3002
- <span style="color:#4caf50;">${method}</span>
3016
+ <span class="t4-green">${method}</span>
3003
3017
  <span>${path}</span>
3004
- <span style="color:#666;">&rarr; ${matchedPattern}</span>
3005
- <span style="color:#ffeb3b;">req:${ctx.requestId}</span>
3006
- <span style="color:#90caf9;">${ctx.routeCount} routes</span>
3007
- <span style="color:#888;">Node.js ${nodeVersion}</span>
3008
- <a href="#" onclick="window.__tina4ToggleOverlay(event)" style="color:#ef9a9a;margin-left:auto;text-decoration:none;cursor:pointer;">Dashboard &#8599;</a>
3009
- <span onclick="this.parentElement.style.display='none'" style="cursor:pointer;color:#888;margin-left:8px;">&#10005;</span>
3018
+ <span class="t4-arrow">&rarr; ${matchedPattern}</span>
3019
+ <span class="t4-yellow">req:${ctx.requestId}</span>
3020
+ <span class="t4-blue">${ctx.routeCount} routes</span>
3021
+ <span class="t4-dim">Node.js ${nodeVersion}</span>
3022
+ <a href="#" id="tina4-dash-link" class="t4-dash">Dashboard &#8599;</a>
3023
+ <span id="tina4-bar-close" class="t4-x t4-bar-close">&#10005;</span>
3010
3024
  </div>
3011
- <script>
3012
- // Overlay open/toggle helper + auto-restore. Persist the dev-admin
3013
- // iframe's open/closed state across parent reloads so saving a file
3014
- // doesn't lose the user's dev-admin context. Cross-framework parity
3015
- // with PHP / Python / Ruby same localStorage key.
3016
- (function(){
3025
+ <script src="/__dev/toolbar.js"></script>`;
3026
+ }
3027
+
3028
+ /**
3029
+ * CSS for the injected dev toolbar. Served as an external stylesheet (see
3030
+ * register()) so the toolbar carries no inline `style=` and stays CSP-clean
3031
+ * under a strict `default-src 'self'`. Mirrors PHP DevAdmin::toolbarCss().
3032
+ */
3033
+ function toolbarCss(): string {
3034
+ return `#tina4-dev-toolbar{position:fixed;bottom:0;left:0;right:0;background:#333;color:#fff;font-family:monospace;font-size:12px;padding:6px 16px;z-index:99999;display:flex;align-items:center;gap:16px}
3035
+ #tina4-dev-toolbar a{text-decoration:none}
3036
+ #tina4-ver-btn{color:#2e7d32;font-weight:bold;cursor:pointer;text-decoration:underline dotted}
3037
+ #tina4-ver-modal{display:none;position:fixed;bottom:3rem;left:1rem;background:#1e1e2e;border:1px solid #2e7d32;border-radius:8px;padding:16px 20px;z-index:100000;min-width:320px;box-shadow:0 8px 32px rgba(0,0,0,.5);font-family:monospace;font-size:13px;color:#cdd6f4}
3038
+ .t4-modal-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}
3039
+ .t4-modal-title{color:#89b4fa}
3040
+ #tina4-ver-body{line-height:1.8}
3041
+ .t4-x{cursor:pointer;color:#888}
3042
+ .t4-bar-close{margin-left:8px}
3043
+ .t4-green{color:#4caf50}
3044
+ .t4-dim{color:#888}
3045
+ .t4-arrow{color:#666}
3046
+ .t4-yellow{color:#ffeb3b}
3047
+ .t4-blue{color:#90caf9}
3048
+ .t4-ok{color:#a6e3a1}
3049
+ .t4-warn{color:#f9e2af}
3050
+ .t4-err{color:#f38ba8}
3051
+ .t4-purple{color:#cba6f7}
3052
+ .t4-link{color:#89b4fa}
3053
+ .t4-code{background:#313244;padding:2px 6px;border-radius:3px}
3054
+ .t4-note{margin-top:6px}
3055
+ .t4-dash{color:#ef9a9a;margin-left:auto;cursor:pointer}
3056
+ #tina4-dev-panel{position:fixed;top:3rem;left:0;right:0;bottom:2rem;z-index:99998;transition:all .2s}
3057
+ #tina4-dev-panel iframe{width:100%;height:100%;border:1px solid #2e7d32;border-radius:.5rem;box-shadow:0 8px 32px rgba(0,0,0,.5);background:#0f172a}`;
3058
+ }
3059
+
3060
+ /**
3061
+ * JS for the injected dev toolbar — the version-check modal, the dashboard
3062
+ * overlay, and the WebSocket-primary live reloader. Served as an external
3063
+ * script so the toolbar carries no inline handlers or `<script>` and stays
3064
+ * CSP-clean. Every interaction is wired via addEventListener. The reloader only
3065
+ * starts when the toolbar's `data-reload` is "1" (reload not suppressed for this
3066
+ * request/port). Mirrors PHP DevAdmin::toolbarJs().
3067
+ */
3068
+ function toolbarJs(): string {
3069
+ return `(function () {
3070
+ var bar = document.getElementById('tina4-dev-toolbar');
3071
+ if (!bar) { return; }
3072
+
3073
+ var modal = document.getElementById('tina4-ver-modal');
3074
+ function upToDate(el, latest) {
3075
+ el.className = 't4-ok';
3076
+ el.innerHTML = 'Latest: <strong class="t4-ok">v' + latest + '</strong> &mdash; You are up to date!';
3077
+ }
3078
+ function checkVersion() {
3079
+ if (modal.style.display === 'block') { modal.style.display = 'none'; return; }
3080
+ modal.style.display = 'block';
3081
+ var el = document.getElementById('tina4-ver-latest');
3082
+ el.className = 't4-dim';
3083
+ el.textContent = 'Checking for updates...';
3084
+ fetch('/__dev/api/version-check').then(function (r) { return r.json(); }).then(function (d) {
3085
+ var latest = d.latest, current = d.current;
3086
+ if (latest === current) { upToDate(el, latest); return; }
3087
+ var cP = current.split('.').map(Number), lP = latest.split('.').map(Number);
3088
+ var isNewer = false, i, c, l;
3089
+ for (i = 0; i < Math.max(cP.length, lP.length); i++) { c = cP[i] || 0; l = lP[i] || 0; if (l > c) { isNewer = true; break; } if (l < c) { break; } }
3090
+ var isAhead = false;
3091
+ if (!isNewer) { for (i = 0; i < Math.max(cP.length, lP.length); i++) { var c2 = cP[i] || 0, l2 = lP[i] || 0; if (c2 > l2) { isAhead = true; break; } if (c2 < l2) { break; } } }
3092
+ if (isNewer) {
3093
+ var breaking = (lP[0] !== cP[0] || lP[1] !== cP[1]);
3094
+ el.className = '';
3095
+ el.innerHTML = 'Latest: <strong class="t4-warn">v' + latest + '</strong>';
3096
+ if (breaking) {
3097
+ el.innerHTML += '<div class="t4-err t4-note">&#9888; Major/minor version change &mdash; check the <a href="https://github.com/tina4stack/tina4-nodejs/releases" target="_blank" class="t4-link">changelog</a> for breaking changes before upgrading.</div>';
3098
+ } else {
3099
+ el.innerHTML += '<div class="t4-warn t4-note">Patch update available. Run: <code class="t4-code">npm install tina4-nodejs@latest</code></div>';
3100
+ }
3101
+ } else if (isAhead) {
3102
+ el.className = 't4-purple';
3103
+ el.innerHTML = 'You are running <strong class="t4-purple">v' + current + '</strong> (ahead of npm <strong>v' + latest + '</strong> &mdash; not yet published).';
3104
+ } else {
3105
+ upToDate(el, latest);
3106
+ }
3107
+ }).catch(function () {
3108
+ el.className = 't4-err';
3109
+ el.textContent = 'Could not check for updates (offline?)';
3110
+ });
3111
+ }
3112
+ var verBtn = document.getElementById('tina4-ver-btn');
3113
+ if (verBtn) { verBtn.addEventListener('click', checkVersion); }
3114
+ var verClose = document.getElementById('tina4-ver-close');
3115
+ if (verClose) { verClose.addEventListener('click', function () { modal.style.display = 'none'; }); }
3116
+ var barClose = document.getElementById('tina4-bar-close');
3117
+ if (barClose) { barClose.addEventListener('click', function () { bar.style.display = 'none'; }); }
3118
+
3017
3119
  var STATE_KEY = 'tina4_dev_overlay_open';
3018
3120
  function buildOverlay() {
3019
3121
  var c = document.createElement('div');
3020
3122
  c.id = 'tina4-dev-panel';
3021
- c.style.cssText = 'position:fixed;top:3rem;left:0;right:0;bottom:2rem;z-index:99998;transition:all 0.2s';
3022
3123
  var f = document.createElement('iframe');
3023
3124
  f.src = '/__dev';
3024
- f.style.cssText = 'width:100%;height:100%;border:1px solid #2e7d32;border-radius:0.5rem;box-shadow:0 8px 32px rgba(0,0,0,0.5);background:#0f172a';
3025
3125
  c.appendChild(f);
3026
3126
  document.body.appendChild(c);
3027
3127
  return c;
3028
3128
  }
3029
- window.__tina4ToggleOverlay = function(e) {
3030
- if (e) e.preventDefault();
3129
+ function toggleOverlay(e) {
3130
+ if (e) { e.preventDefault(); }
3031
3131
  var p = document.getElementById('tina4-dev-panel');
3032
3132
  if (p) {
3033
3133
  var hide = p.style.display !== 'none';
@@ -3037,133 +3137,69 @@ function renderToolbarHtml(ctx: {
3037
3137
  }
3038
3138
  buildOverlay();
3039
3139
  try { localStorage.setItem(STATE_KEY, '1'); } catch (_) {}
3040
- };
3041
- function restoreIfOpen() {
3042
- try {
3043
- if (location.pathname.indexOf('/__dev') === 0) return;
3044
- if (localStorage.getItem(STATE_KEY) === '1' && !document.getElementById('tina4-dev-panel')) {
3045
- buildOverlay();
3046
- }
3047
- } catch (_) {}
3048
- }
3049
- if (document.readyState === 'loading') {
3050
- document.addEventListener('DOMContentLoaded', restoreIfOpen);
3051
- } else {
3052
- restoreIfOpen();
3053
3140
  }
3054
- })();
3055
- </script>
3056
- <script>
3057
- function tina4VersionModal(){
3058
- var m=document.getElementById('tina4-ver-modal');
3059
- if(m.style.display==='block'){m.style.display='none';return;}
3060
- m.style.display='block';
3061
- var el=document.getElementById('tina4-ver-latest');
3062
- el.innerHTML='Checking for updates...';
3063
- el.style.color='#888';
3064
- fetch('/__dev/api/version-check')
3065
- .then(function(r){return r.json()})
3066
- .then(function(d){
3067
- var latest=d.latest;
3068
- var current=d.current;
3069
- if(latest===current){
3070
- el.innerHTML='Latest: <strong style="color:#a6e3a1;">v'+latest+'</strong> &mdash; You are up to date!';
3071
- el.style.color='#a6e3a1';
3072
- }else{
3073
- var cParts=current.split('.').map(Number);
3074
- var lParts=latest.split('.').map(Number);
3075
- var isNewer=false;
3076
- for(var i=0;i<Math.max(cParts.length,lParts.length);i++){
3077
- var c=cParts[i]||0,l=lParts[i]||0;
3078
- if(l>c){isNewer=true;break;}
3079
- if(l<c)break;
3080
- }
3081
- var isAhead=false;
3082
- if(!isNewer){for(var i=0;i<Math.max(cParts.length,lParts.length);i++){var c2=cParts[i]||0,l2=lParts[i]||0;if(c2>l2){isAhead=true;break;}if(c2<l2)break;}}
3083
- if(isNewer){
3084
- var breaking=(lParts[0]!==cParts[0]||lParts[1]!==cParts[1]);
3085
- el.innerHTML='Latest: <strong style="color:#f9e2af;">v'+latest+'</strong>';
3086
- if(breaking){
3087
- el.innerHTML+='<div style="color:#f38ba8;margin-top:6px;">&#9888; Major/minor version change &mdash; check the <a href="https://github.com/tina4stack/tina4-nodejs/releases" target="_blank" style="color:#89b4fa;">changelog</a> for breaking changes before upgrading.</div>';
3088
- }else{
3089
- el.innerHTML+='<div style="color:#f9e2af;margin-top:6px;">Patch update available. Run: <code style="background:#313244;padding:2px 6px;border-radius:3px;">npm install tina4-nodejs@latest</code></div>';
3090
- }
3091
- }else if(isAhead){
3092
- el.innerHTML='You are running <strong style="color:#cba6f7;">v'+current+'</strong> (ahead of npm <strong>v'+latest+'</strong> &mdash; not yet published).';
3093
- el.style.color='#cba6f7';
3094
- }else{
3095
- el.innerHTML='Latest: <strong style="color:#a6e3a1;">v'+latest+'</strong> &mdash; You are up to date!';
3096
- el.style.color='#a6e3a1';
3097
- }
3141
+ var dash = document.getElementById('tina4-dash-link');
3142
+ if (dash) { dash.addEventListener('click', toggleOverlay); }
3143
+ try {
3144
+ if (location.pathname.indexOf('/__dev') !== 0
3145
+ && localStorage.getItem(STATE_KEY) === '1'
3146
+ && !document.getElementById('tina4-dev-panel')) {
3147
+ buildOverlay();
3098
3148
  }
3099
- })
3100
- .catch(function(){
3101
- el.innerHTML='Could not check for updates (offline?)';
3102
- el.style.color='#f38ba8';
3103
- });
3104
- }
3105
- </script>
3106
- <script>
3107
- (function(){
3108
- // WebSocket-primary dev reloader. The running server re-imports changed
3109
- // src/ routes in-process and pushes a {type,file,mtime} message over
3110
- // /__dev_reload no respawn, instant refresh. The mtime poll below is a
3111
- // FALLBACK only, started when the socket is down and stopped on connect.
3112
- var _t4_css_exts=['.css','.scss'],_t4_debounce=null;
3113
- var _t4_interval=3000;
3114
- var _t4_ws=null,_t4_poll_timer=null,_t4_mtime=null;
3115
- function _t4_apply(d){
3116
- d=d||{};
3117
- var f=d.file||'',t=d.type||'';
3118
- var isCss=t==='css'||_t4_css_exts.some(function(e){return f.endsWith(e)});
3119
- if(isCss){
3120
- var links=document.querySelectorAll('link[rel="stylesheet"]');
3121
- links.forEach(function(l){
3122
- var href=l.getAttribute('href');
3123
- if(href){l.setAttribute('href',href.split('?')[0]+'?_t4='+(d.mtime||Date.now()))}
3149
+ } catch (_) {}
3150
+
3151
+ if (bar.getAttribute('data-reload') !== '1') { return; }
3152
+ var cssExts = ['.css', '.scss'], debounce = null, interval = 3000;
3153
+ var ws = null, pollTimer = null, mtime = null;
3154
+ function apply(d) {
3155
+ d = d || {};
3156
+ var f = d.file || '', t = d.type || '';
3157
+ var isCss = t === 'css' || cssExts.some(function (e) { return f.endsWith(e); });
3158
+ if (isCss) {
3159
+ document.querySelectorAll('link[rel="stylesheet"]').forEach(function (l) {
3160
+ var href = l.getAttribute('href');
3161
+ if (href) { l.setAttribute('href', href.split('?')[0] + '?_t4=' + (d.mtime || Date.now())); }
3124
3162
  });
3125
- }else{
3163
+ } else {
3126
3164
  location.reload();
3127
3165
  }
3128
3166
  }
3129
- function _t4_poll(){
3130
- fetch('/__dev/api/mtime').then(function(r){return r.json()}).then(function(d){
3131
- // Sentinel: first poll only records the baseline. Use !== (not >) so
3132
- // the first change after load is not swallowed and a counter reset on
3133
- // server restart still triggers a reload.
3134
- if(_t4_mtime===null){_t4_mtime=d.mtime;return;}
3135
- if(d.mtime!==_t4_mtime){
3136
- _t4_mtime=d.mtime;
3137
- if(_t4_debounce)clearTimeout(_t4_debounce);
3138
- _t4_debounce=setTimeout(function(){_t4_apply(d);},500);
3139
- }
3140
- }).catch(function(){});
3141
- }
3142
- function _t4_startPoll(){
3143
- if(_t4_poll_timer)return;
3144
- _t4_mtime=null;
3145
- _t4_poll_timer=setInterval(_t4_poll,_t4_interval);
3146
- }
3147
- function _t4_stopPoll(){
3148
- if(_t4_poll_timer){clearInterval(_t4_poll_timer);_t4_poll_timer=null;}
3167
+ function poll() {
3168
+ fetch('/__dev/api/mtime').then(function (r) { return r.json(); }).then(function (d) {
3169
+ if (mtime === null) { mtime = d.mtime; return; }
3170
+ if (d.mtime !== mtime) { mtime = d.mtime; if (debounce) { clearTimeout(debounce); } debounce = setTimeout(function () { apply(d); }, 500); }
3171
+ }).catch(function () {});
3149
3172
  }
3150
- function _t4_connect(){
3151
- var url=(location.protocol==='https:'?'wss':'ws')+'://'+location.host+'/__dev_reload';
3152
- try{_t4_ws=new WebSocket(url);}catch(_){_t4_startPoll();return;}
3153
- _t4_ws.addEventListener('open',function(){_t4_stopPoll();});
3154
- _t4_ws.addEventListener('message',function(ev){
3155
- var d=null;
3156
- try{d=typeof ev.data==='string'?JSON.parse(ev.data):null;}catch(_){}
3157
- if(!d)return;
3158
- if(d.type==='reload'||d.type==='change'||d.type==='css'){
3159
- if(_t4_debounce)clearTimeout(_t4_debounce);
3160
- _t4_debounce=setTimeout(function(){_t4_apply(d);},150);
3173
+ function startPoll() { if (pollTimer) { return; } mtime = null; pollTimer = setInterval(poll, interval); }
3174
+ function stopPoll() { if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } }
3175
+ function connect() {
3176
+ var url = (location.protocol === 'https:' ? 'wss' : 'ws') + '://' + location.host + '/__dev_reload';
3177
+ try { ws = new WebSocket(url); } catch (_) { startPoll(); return; }
3178
+ ws.addEventListener('open', function () { stopPoll(); });
3179
+ ws.addEventListener('message', function (ev) {
3180
+ var d = null;
3181
+ try { d = typeof ev.data === 'string' ? JSON.parse(ev.data) : null; } catch (_) {}
3182
+ if (!d) { return; }
3183
+ if (d.type === 'reload' || d.type === 'change' || d.type === 'css') {
3184
+ if (debounce) { clearTimeout(debounce); }
3185
+ debounce = setTimeout(function () { apply(d); }, 150);
3161
3186
  }
3162
3187
  });
3163
- _t4_ws.addEventListener('close',function(){_t4_ws=null;_t4_startPoll();setTimeout(_t4_connect,2000);});
3164
- _t4_ws.addEventListener('error',function(){try{_t4_ws&&_t4_ws.close();}catch(_){}});
3188
+ ws.addEventListener('close', function () { ws = null; startPoll(); setTimeout(connect, 2000); });
3189
+ ws.addEventListener('error', function () { try { ws && ws.close(); } catch (_) {} });
3165
3190
  }
3166
- _t4_connect();
3167
- })();
3168
- </script>`;
3191
+ connect();
3192
+ })();`;
3169
3193
  }
3194
+
3195
+ const handleToolbarCss: RouteHandler = (_req, res) => {
3196
+ // text()+header rather than html() so the toolbar is never injected into its
3197
+ // own asset (parity with PHP's DevAdmin route registration).
3198
+ res.raw.writeHead(200, { "Content-Type": "text/css; charset=utf-8", "Cache-Control": "no-cache" });
3199
+ res.raw.end(toolbarCss());
3200
+ };
3201
+
3202
+ const handleToolbarJs: RouteHandler = (_req, res) => {
3203
+ res.raw.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8", "Cache-Control": "no-cache" });
3204
+ res.raw.end(toolbarJs());
3205
+ };
@@ -495,6 +495,8 @@ interface DevToolbarContext {
495
495
  matchedPattern: string;
496
496
  requestId: string;
497
497
  routeCount: number;
498
+ /** When false, the toolbar sets data-reload="0" and its JS suppresses the reloader (AI/stable port). */
499
+ reload?: boolean;
498
500
  }
499
501
 
500
502
  function injectDevToolbar(html: string, ctx: DevToolbarContext): string {
@@ -1250,6 +1252,10 @@ function injectIntoHtml(ctx: ResponseWrapContext, devToolbar: boolean, html: str
1250
1252
  matchedPattern: ctx.matchedPattern.value || ctx.pathname,
1251
1253
  requestId: ctx.requestId,
1252
1254
  routeCount: ctx.router.getRoutes().length,
1255
+ // Suppress the live reloader on the AI/stable port (data-reload="0"); the
1256
+ // toolbar JS early-returns when data-reload !== "1". Mirrors PHP's
1257
+ // suppressReload flag.
1258
+ reload: !ctx.isAiPortRequest,
1253
1259
  };
1254
1260
  return injectFeedbackWidget(ctx.req, injectDevToolbar(html, toolbarCtx));
1255
1261
  }