tina4-nodejs 3.10.32 → 3.10.38

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.
@@ -17,10 +17,11 @@ import type { Router } from "./router.js";
17
17
  import type { RouteHandler } from "./types.js";
18
18
  import { DevMailbox } from "./devMailbox.js";
19
19
  import { isTruthy } from "./dotenv.js";
20
+ import { quickMetrics, fullAnalysis, fileDetail } from "./metrics.js";
20
21
 
21
22
  const cpuCount = osCpus().length;
22
23
 
23
- const TINA4_VERSION = "3.10.30";
24
+ const TINA4_VERSION = "3.10.34";
24
25
 
25
26
  // ---------------------------------------------------------------------------
26
27
  // Types
@@ -343,6 +344,7 @@ export class DevAdmin {
343
344
  const routes: Array<{ method: string; pattern: string; handler: RouteHandler }> = [
344
345
  // Dashboard
345
346
  { method: "GET", pattern: "/__dev", handler: handleDashboard },
347
+ { method: "GET", pattern: "/__dev/", handler: handleDashboard },
346
348
  // Status & system
347
349
  { method: "GET", pattern: "/__dev/api/status", handler: handleStatus(router) },
348
350
  { method: "GET", pattern: "/__dev/api/system", handler: handleSystem },
@@ -388,6 +390,12 @@ export class DevAdmin {
388
390
  // Gallery
389
391
  { method: "GET", pattern: "/__dev/api/gallery", handler: handleGalleryList },
390
392
  { method: "POST", pattern: "/__dev/api/gallery/deploy", handler: handleGalleryDeploy(router) },
393
+ // Metrics
394
+ { method: "GET", pattern: "/__dev/api/metrics", handler: (_req: any, res: any) => { res.json(quickMetrics()); } },
395
+ { method: "GET", pattern: "/__dev/api/metrics/full", handler: (_req: any, res: any) => { res.json(fullAnalysis()); } },
396
+ { method: "GET", pattern: "/__dev/api/metrics/file", handler: (req: any, res: any) => { const url = new URL(req.url ?? "/", "http://localhost"); const p = (url.searchParams.get("path") || "").toString(); res.json(fileDetail(p)); } },
397
+ // Version check (proxy to avoid CORS)
398
+ { method: "GET", pattern: "/__dev/api/version-check", handler: handleVersionCheck },
391
399
  // JS asset
392
400
  { method: "GET", pattern: "/__dev/js/tina4-dev-admin.min.js", handler: handleDevAdminJs },
393
401
  ];
@@ -1040,6 +1048,30 @@ function handleGalleryDeploy(router: Router): RouteHandler {
1040
1048
  };
1041
1049
  }
1042
1050
 
1051
+ // ---------------------------------------------------------------------------
1052
+ // Version check — proxy to npm registry to avoid browser CORS errors
1053
+ // ---------------------------------------------------------------------------
1054
+
1055
+ const handleVersionCheck: RouteHandler = async (_req, res) => {
1056
+ const current = TINA4_VERSION;
1057
+ let latest = current;
1058
+ try {
1059
+ const controller = new AbortController();
1060
+ const timeout = setTimeout(() => controller.abort(), 5000);
1061
+ const resp = await fetch("https://registry.npmjs.org/tina4-nodejs/latest", {
1062
+ signal: controller.signal,
1063
+ });
1064
+ clearTimeout(timeout);
1065
+ if (resp.ok) {
1066
+ const data = (await resp.json()) as Record<string, unknown>;
1067
+ if (typeof data.version === "string") latest = data.version;
1068
+ }
1069
+ } catch {
1070
+ // Offline or timeout — return current as latest
1071
+ }
1072
+ res.json({ current, latest });
1073
+ };
1074
+
1043
1075
  // ---------------------------------------------------------------------------
1044
1076
  // Dev Admin JS handler — serves the shared JS file
1045
1077
  // ---------------------------------------------------------------------------
@@ -1467,6 +1499,7 @@ body { font-family: var(--font); background: var(--bg); color: var(--text); font
1467
1499
  .dev-header {
1468
1500
  background: var(--surface); border-bottom: 1px solid var(--border);
1469
1501
  padding: 0.75rem 1.5rem; display: flex; align-items: center; gap: 1rem;
1502
+ position: sticky; top: 0; z-index: 100;
1470
1503
  }
1471
1504
  .dev-header h1 { font-size: 1rem; font-weight: 600; }
1472
1505
  .dev-header .badge {
@@ -1476,6 +1509,7 @@ body { font-family: var(--font); background: var(--bg); color: var(--text); font
1476
1509
  .dev-tabs {
1477
1510
  display: flex; gap: 0; background: var(--surface);
1478
1511
  border-bottom: 1px solid var(--border); overflow-x: auto;
1512
+ position: sticky; top: 2.75rem; z-index: 100;
1479
1513
  }
1480
1514
  .dev-tab {
1481
1515
  padding: 0.6rem 1rem; cursor: pointer; font-size: 0.8rem;
@@ -1489,10 +1523,10 @@ body { font-family: var(--font); background: var(--bg); color: var(--text); font
1489
1523
  background: var(--border); color: var(--muted); padding: 0.1rem 0.4rem;
1490
1524
  border-radius: 0.75rem; font-size: 0.65rem; margin-left: 0.25rem;
1491
1525
  }
1492
- .dev-content { padding: 1rem; max-width: 1400px; }
1526
+ .dev-content { padding: 0.25rem; }
1493
1527
  .dev-panel {
1494
1528
  background: var(--surface); border: 1px solid var(--border);
1495
- border-radius: var(--radius); overflow: hidden;
1529
+ border-radius: var(--radius); overflow: visible;
1496
1530
  }
1497
1531
  .dev-panel-header {
1498
1532
  padding: 0.75rem 1rem; border-bottom: 1px solid var(--border);
@@ -1614,6 +1648,7 @@ code, .mono { font-family: var(--mono); font-size: 0.82rem; }
1614
1648
  <button class="dev-tab" onclick="showTab('system', event)">System</button>
1615
1649
  <button class="dev-tab" onclick="showTab('tools', event)">Tools</button>
1616
1650
  <button class="dev-tab" onclick="showTab('connections', event)">Connections</button>
1651
+ <button class="dev-tab" onclick="showTab('metrics', event)">Metrics</button>
1617
1652
  <button class="dev-tab" onclick="showTab('chat', event)">Tina4</button>
1618
1653
  </div>
1619
1654
 
@@ -1952,6 +1987,30 @@ document.addEventListener('DOMContentLoaded', function() {
1952
1987
  });
1953
1988
  </script>
1954
1989
 
1990
+ <!-- Metrics Panel -->
1991
+ <div id="panel-metrics" class="dev-panel hidden">
1992
+ <div class="dev-panel-header">
1993
+ <h2>Code Metrics</h2>
1994
+ <div>
1995
+ <button class="btn btn-sm" onclick="loadAllMetrics()">Refresh</button>
1996
+ </div>
1997
+ </div>
1998
+ <div id="metrics-bubble" style="margin:1rem;"></div>
1999
+ <div id="metrics-drilldown" style="margin:0 1rem;display:none;"></div>
2000
+ <div id="metrics-quick" class="sys-grid"></div>
2001
+ <div id="metrics-largest" style="margin-top:1rem;"></div>
2002
+ <div id="metrics-tables" style="margin-top:1rem;padding:0 1rem 1rem;overflow-x:auto;">
2003
+ <h3 style="margin:1rem 0 0.5rem;color:var(--primary);">File Analysis</h3>
2004
+ <div id="metrics-heatmap"></div>
2005
+ <h3 style="margin:1rem 0 0.5rem;color:var(--primary);">Most Complex Functions</h3>
2006
+ <div id="metrics-complex"></div>
2007
+ <h3 style="margin:1rem 0 0.5rem;color:var(--primary);">Coupling Analysis</h3>
2008
+ <div id="metrics-coupling"></div>
2009
+ <h3 style="margin:1rem 0 0.5rem;color:var(--primary);">Violations</h3>
2010
+ <div id="metrics-violations"></div>
2011
+ </div>
2012
+ </div>
2013
+
1955
2014
  <!-- Chat Panel (Tina4) -->
1956
2015
  <div id="panel-chat" class="dev-panel hidden">
1957
2016
  <div class="dev-panel-header">
@@ -1981,6 +2040,225 @@ document.addEventListener('DOMContentLoaded', function() {
1981
2040
 
1982
2041
  <script src="/__dev/js/tina4-dev-admin.min.js"></script>
1983
2042
  <script>
2043
+ // ── Metrics Panel JS ──
2044
+ var _metricsFullData=null;
2045
+ function miColor(mi){
2046
+ if(mi>=60) return 'rgb('+(Math.round(34+(1-((mi-60)/40))*186))+','+(Math.round(197-(1-((mi-60)/40))*50))+',0)';
2047
+ if(mi>=30) return 'rgb('+(Math.round(220+((60-mi)/30)*19))+','+(Math.round(180-((60-mi)/30)*112))+',0)';
2048
+ return 'rgb(239,'+(Math.round(68-mi*2))+',0)';
2049
+ }
2050
+ function renderBubbleChart(files){
2051
+ var container=document.getElementById('metrics-bubble');
2052
+ if(!files||!files.length){container.innerHTML='<p style="color:var(--muted);padding:1rem">No files to analyze</p>';return;}
2053
+ var W=container.offsetWidth||900,H=Math.max(450,Math.min(650,W*0.45));
2054
+ var maxLoc=Math.max.apply(null,files.map(function(f){return f.loc}))||1;
2055
+ var minR=14,maxR=Math.min(70,W/10);
2056
+ var sorted=files.slice().sort(function(a,b){return a.loc-b.loc});
2057
+ var cx=W/2,cy=H/2;
2058
+ var bubbles=[];
2059
+ var angle=0,spiralR=0;
2060
+ for(var i=0;i<sorted.length;i++){
2061
+ var f=sorted[i];
2062
+ var r=minR+Math.sqrt(f.loc/maxLoc)*(maxR-minR);
2063
+ var color=miColor(f.maintainability||0);
2064
+ var placed=false;
2065
+ for(var attempt=0;attempt<800;attempt++){
2066
+ var px=cx+spiralR*Math.cos(angle);
2067
+ var py=cy+spiralR*Math.sin(angle);
2068
+ var collides=false;
2069
+ for(var j=0;j<bubbles.length;j++){
2070
+ var dx=px-bubbles[j].x,dy=py-bubbles[j].y;
2071
+ if(Math.sqrt(dx*dx+dy*dy)<r+bubbles[j].r+2){collides=true;break;}
2072
+ }
2073
+ if(!collides&&px>r+2&&px<W-r-2&&py>r+25&&py<H-r-2){
2074
+ bubbles.push({x:px,y:py,r:r,color:color,f:f,angle:Math.random()*Math.PI*2,speed:0.3+Math.random()*0.5,drift:2+Math.random()*3});
2075
+ placed=true;break;
2076
+ }
2077
+ angle+=0.2;spiralR+=0.04;
2078
+ }
2079
+ if(!placed){bubbles.push({x:cx+(Math.random()-0.5)*W*0.3,y:cy+(Math.random()-0.5)*H*0.3,r:r,color:color,f:f,angle:Math.random()*Math.PI*2,speed:0.3+Math.random()*0.5,drift:2+Math.random()*3});}
2080
+ }
2081
+ var canvas=document.createElement('canvas');
2082
+ canvas.width=W;canvas.height=H;
2083
+ canvas.style.cssText='display:block;border:1px solid var(--border);border-radius:8px;cursor:pointer;background:#0f172a';
2084
+ container.innerHTML='<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.5rem"><h3 style="margin:0;color:var(--primary)">Code Landscape</h3><span style="font-size:0.7rem;color:var(--muted)">Click a bubble to drill down | Size=LOC | <span style="color:#22c55e">Green</span>=maintainable <span style="color:#eab308">Yellow</span>=moderate <span style="color:#ef4444">Red</span>=needs work</span></div>';
2085
+ container.appendChild(canvas);
2086
+ var ctx=canvas.getContext('2d');
2087
+ var hoveredIdx=-1;
2088
+ var t=0;
2089
+ function draw(){
2090
+ t+=0.016;
2091
+ ctx.clearRect(0,0,W,H);
2092
+ ctx.strokeStyle='rgba(255,255,255,0.03)';ctx.lineWidth=1;
2093
+ for(var gx=0;gx<W;gx+=50){ctx.beginPath();ctx.moveTo(gx,0);ctx.lineTo(gx,H);ctx.stroke();}
2094
+ for(var gy=0;gy<H;gy+=50){ctx.beginPath();ctx.moveTo(0,gy);ctx.lineTo(W,gy);ctx.stroke();}
2095
+ bubbles.forEach(function(b,idx){
2096
+ var ox=Math.sin(t*b.speed+b.angle)*b.drift;
2097
+ var oy=Math.cos(t*b.speed*0.7+b.angle+1)*b.drift*0.6;
2098
+ var bx=b.x+ox,by=b.y+oy;
2099
+ var isHovered=(idx===hoveredIdx);
2100
+ var drawR=isHovered?b.r+4:b.r;
2101
+ if(isHovered){
2102
+ ctx.beginPath();ctx.arc(bx,by,drawR+8,0,Math.PI*2);
2103
+ ctx.fillStyle='rgba(255,255,255,0.08)';ctx.fill();
2104
+ }
2105
+ ctx.beginPath();ctx.arc(bx,by,drawR,0,Math.PI*2);
2106
+ ctx.fillStyle=b.color;ctx.globalAlpha=isHovered?0.95:0.7;ctx.fill();
2107
+ ctx.globalAlpha=1;ctx.strokeStyle=b.color;ctx.lineWidth=isHovered?2.5:1.5;ctx.stroke();
2108
+ var name=b.f.path.split('/').pop().replace('.ts','').replace('.js','');
2109
+ if(drawR>16){
2110
+ var fs=Math.max(8,Math.min(13,drawR*0.38));
2111
+ ctx.fillStyle='#fff';ctx.font='600 '+fs+'px monospace';ctx.textAlign='center';
2112
+ ctx.fillText(name,bx,by-2);
2113
+ ctx.fillStyle='rgba(255,255,255,0.65)';ctx.font=(fs-1)+'px monospace';
2114
+ ctx.fillText(b.f.loc+' LOC',bx,by+fs);
2115
+ if(isHovered&&drawR>25){
2116
+ ctx.fillStyle='rgba(255,255,255,0.5)';ctx.font=(fs-2)+'px monospace';
2117
+ ctx.fillText('CC:'+b.f.complexity+' MI:'+b.f.maintainability,bx,by+fs*2);
2118
+ }
2119
+ }
2120
+ b._drawX=bx;b._drawY=by;b._drawR=drawR;
2121
+ });
2122
+ var totalLoc=0,totalFiles=bubbles.length;
2123
+ bubbles.forEach(function(b){totalLoc+=b.f.loc});
2124
+ var avgMI=bubbles.reduce(function(s,b){return s+b.f.maintainability},0)/totalFiles;
2125
+ ctx.fillStyle='rgba(255,255,255,0.35)';ctx.font='11px monospace';ctx.textAlign='right';
2126
+ ctx.fillText(totalFiles+' files | '+totalLoc.toLocaleString()+' LOC | Avg MI: '+avgMI.toFixed(1),W-12,H-10);
2127
+ window._metricsAnimFrame=requestAnimationFrame(draw);
2128
+ }
2129
+ draw();
2130
+ canvas.addEventListener('mousemove',function(e){
2131
+ var rect=canvas.getBoundingClientRect();
2132
+ var mx=e.clientX-rect.left,my=e.clientY-rect.top;
2133
+ hoveredIdx=-1;
2134
+ for(var i=bubbles.length-1;i>=0;i--){
2135
+ var b=bubbles[i];
2136
+ var dx=mx-b._drawX,dy=my-b._drawY;
2137
+ if(Math.sqrt(dx*dx+dy*dy)<=b._drawR){hoveredIdx=i;break;}
2138
+ }
2139
+ canvas.style.cursor=hoveredIdx>=0?'pointer':'default';
2140
+ });
2141
+ canvas.addEventListener('mouseleave',function(){hoveredIdx=-1;});
2142
+ canvas.addEventListener('click',function(e){
2143
+ if(hoveredIdx<0)return;
2144
+ var f=bubbles[hoveredIdx].f;
2145
+ drillDownFile(f.path);
2146
+ });
2147
+ }
2148
+ function drillDownFile(path){
2149
+ var dd=document.getElementById('metrics-drilldown');
2150
+ dd.style.display='block';
2151
+ dd.innerHTML='<div class="dev-panel" style="margin-bottom:1rem"><div class="dev-panel-header"><h2>'+path+'</h2><button class="btn btn-sm" onclick="document.getElementById(&#39;metrics-drilldown&#39;).style.display=&#39;none&#39;">Close</button></div><div class="p-md"><p style="color:var(--muted)">Loading file analysis...</p></div></div>';
2152
+ fetch('/__dev/api/metrics/file?path='+encodeURIComponent(path)).then(function(r){return r.json()}).then(function(d){
2153
+ if(d.error){dd.querySelector('.p-md').innerHTML='<p style="color:var(--danger)">'+d.error+'</p>';return;}
2154
+ var html='<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:0.5rem;margin-bottom:1rem">';
2155
+ html+='<div class="sys-card"><div class="label">LOC</div><div class="value">'+d.loc+'</div></div>';
2156
+ html+='<div class="sys-card"><div class="label">Total Lines</div><div class="value">'+d.total_lines+'</div></div>';
2157
+ html+='<div class="sys-card"><div class="label">Classes</div><div class="value">'+d.classes+'</div></div>';
2158
+ html+='<div class="sys-card"><div class="label">Functions</div><div class="value">'+(d.functions?d.functions.length:0)+'</div></div>';
2159
+ html+='<div class="sys-card"><div class="label">Imports</div><div class="value">'+(d.imports?d.imports.length:0)+'</div></div>';
2160
+ html+='</div>';
2161
+ if(d.functions&&d.functions.length){
2162
+ html+='<h3 style="margin:0.5rem 0;color:var(--primary);font-size:0.85rem">Cyclomatic Complexity by Function</h3>';
2163
+ var maxCC=Math.max.apply(null,d.functions.map(function(f){return f.complexity}))||1;
2164
+ html+='<div style="display:flex;flex-direction:column;gap:4px">';
2165
+ d.functions.forEach(function(f){
2166
+ var pct=Math.max(3,f.complexity/maxCC*100);
2167
+ var color=f.complexity>20?'#ef4444':f.complexity>10?'#eab308':f.complexity>5?'#3b82f6':'#22c55e';
2168
+ html+='<div style="display:flex;align-items:center;gap:8px;font-size:0.75rem;font-family:var(--mono)">';
2169
+ html+='<span style="width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)" title="'+f.name+'">'+f.name+'</span>';
2170
+ html+='<div style="flex:1;height:16px;background:var(--bg);border-radius:3px;overflow:hidden;position:relative">';
2171
+ html+='<div style="width:'+pct+'%;height:100%;background:'+color+';border-radius:3px;transition:width 0.3s"></div>';
2172
+ html+='</div>';
2173
+ html+='<span style="width:70px;text-align:right;color:'+color+';font-weight:600">CC:'+f.complexity+'</span>';
2174
+ html+='<span style="width:60px;text-align:right;color:var(--muted)">'+f.loc+' LOC</span>';
2175
+ html+='<span style="width:30px;text-align:right;color:var(--muted)">L'+f.line+'</span>';
2176
+ html+='</div>';
2177
+ });
2178
+ html+='</div>';
2179
+ }
2180
+ if(d.imports&&d.imports.length){
2181
+ html+='<h3 style="margin:0.75rem 0 0.25rem;color:var(--primary);font-size:0.85rem">Dependencies</h3>';
2182
+ html+='<div style="display:flex;flex-wrap:wrap;gap:4px">';
2183
+ d.imports.forEach(function(imp){
2184
+ html+='<span style="padding:2px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;font-size:0.7rem;font-family:var(--mono)">'+imp+'</span>';
2185
+ });
2186
+ html+='</div>';
2187
+ }
2188
+ dd.querySelector('.p-md').innerHTML=html;
2189
+ }).catch(function(e){
2190
+ dd.querySelector('.p-md').innerHTML='<p style="color:var(--danger)">Error: '+e.message+'</p>';
2191
+ });
2192
+ dd.scrollIntoView({behavior:'smooth',block:'start'});
2193
+ }
2194
+ function loadAllMetrics(){
2195
+ if(window._metricsAnimFrame)cancelAnimationFrame(window._metricsAnimFrame);
2196
+ var el=document.getElementById('metrics-quick');
2197
+ el.innerHTML='<div class="sys-card"><div class="value">Loading...</div></div>';
2198
+ fetch('/__dev/api/metrics').then(function(r){return r.json()}).then(function(d){
2199
+ if(d.error){el.innerHTML='<div class="sys-card"><div class="value" style="color:var(--danger)">'+d.error+'</div></div>';return;}
2200
+ el.innerHTML=
2201
+ '<div class="sys-card"><div class="label">TS/JS Files</div><div class="value">'+d.file_count+'</div></div>'+
2202
+ '<div class="sys-card"><div class="label">Lines of Code</div><div class="value">'+d.total_loc.toLocaleString()+'</div></div>'+
2203
+ '<div class="sys-card"><div class="label">Comment Lines</div><div class="value">'+d.total_comment.toLocaleString()+'</div></div>'+
2204
+ '<div class="sys-card"><div class="label">Blank Lines</div><div class="value">'+d.total_blank.toLocaleString()+'</div></div>'+
2205
+ '<div class="sys-card"><div class="label">Classes</div><div class="value">'+d.classes+'</div></div>'+
2206
+ '<div class="sys-card"><div class="label">Functions</div><div class="value">'+d.functions+'</div></div>'+
2207
+ '<div class="sys-card"><div class="label">Routes</div><div class="value">'+d.route_count+'</div></div>'+
2208
+ '<div class="sys-card"><div class="label">ORM Models</div><div class="value">'+d.orm_count+'</div></div>'+
2209
+ '<div class="sys-card"><div class="label">Templates</div><div class="value">'+d.template_count+'</div></div>'+
2210
+ '<div class="sys-card"><div class="label">Migrations</div><div class="value">'+d.migration_count+'</div></div>';
2211
+ }).catch(function(e){el.innerHTML='<div class="sys-card"><div class="value" style="color:var(--danger)">Error: '+e.message+'</div></div>';});
2212
+ document.getElementById('metrics-bubble').innerHTML='<p style="color:var(--muted);padding:1rem">Analyzing codebase...</p>';
2213
+ fetch('/__dev/api/metrics/full').then(function(r){return r.json()}).then(function(d){
2214
+ _metricsFullData=d;
2215
+ if(d.error){document.getElementById('metrics-bubble').innerHTML='<p style="color:var(--danger);padding:1rem">'+d.error+'</p>';return;}
2216
+ renderBubbleChart(d.file_metrics);
2217
+ var hm=document.getElementById('metrics-heatmap');
2218
+ var rows=d.file_metrics.map(function(f){
2219
+ var color=miColor(f.maintainability);
2220
+ var barW=Math.max(2,Math.min(100,f.maintainability));
2221
+ return '<tr style="cursor:pointer" onclick="drillDownFile(&#39;'+f.path+'&#39;)"><td><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:'+color+';margin-right:6px"></span>'+f.path+'</td><td>'+f.loc+'</td><td>'+f.complexity+'</td><td>'+f.avg_complexity+'</td><td><div style="display:flex;align-items:center;gap:6px"><div style="width:'+barW+'px;height:6px;border-radius:3px;background:'+color+'"></div><span>'+f.maintainability+'</span></div></td><td>'+f.instability+'</td></tr>';
2222
+ }).join('');
2223
+ hm.innerHTML='<table style="width:100%"><thead><tr><th>File</th><th>LOC</th><th>CC</th><th>Avg CC</th><th>MI</th><th>Instab.</th></tr></thead><tbody>'+rows+'</tbody></table>';
2224
+ var cf=document.getElementById('metrics-complex');
2225
+ var frows=d.most_complex_functions.map(function(f){
2226
+ var color=f.complexity>20?'#ef4444':f.complexity>10?'#eab308':'#22c55e';
2227
+ return '<tr style="cursor:pointer" onclick="drillDownFile(&#39;'+f.file+'&#39;)"><td><span style="color:'+color+';font-weight:bold">'+f.complexity+'</span></td><td>'+f.name+'</td><td>'+f.file+':'+f.line+'</td><td>'+f.loc+'</td></tr>';
2228
+ }).join('');
2229
+ cf.innerHTML='<table style="width:100%"><thead><tr><th>CC</th><th>Function</th><th>File</th><th>LOC</th></tr></thead><tbody>'+frows+'</tbody></table>';
2230
+ var cp=document.getElementById('metrics-coupling');
2231
+ var crows=d.file_metrics.filter(function(f){return f.coupling_afferent>0||f.coupling_efferent>0}).map(function(f){
2232
+ return '<tr style="cursor:pointer" onclick="drillDownFile(&#39;'+f.path+'&#39;)"><td>'+f.path+'</td><td>'+f.coupling_afferent+'</td><td>'+f.coupling_efferent+'</td><td>'+f.instability+'</td></tr>';
2233
+ }).join('');
2234
+ cp.innerHTML=crows?'<table style="width:100%"><thead><tr><th>File</th><th>Ca (in)</th><th>Ce (out)</th><th>Instability</th></tr></thead><tbody>'+crows+'</tbody></table>':'<p style="color:var(--muted)">No coupling data</p>';
2235
+ var vl=document.getElementById('metrics-violations');
2236
+ if(d.violations&&d.violations.length){
2237
+ var vrows=d.violations.map(function(v){
2238
+ var icon=v.type==='error'?'&#9888;':'&#9432;';
2239
+ var color=v.type==='error'?'#ef4444':'#eab308';
2240
+ return '<tr style="cursor:pointer" onclick="drillDownFile(&#39;'+v.file+'&#39;)"><td style="color:'+color+'">'+icon+'</td><td>'+v.message+'</td><td>'+v.file+(v.line?':'+v.line:'')+'</td></tr>';
2241
+ }).join('');
2242
+ vl.innerHTML='<table style="width:100%"><thead><tr><th></th><th>Issue</th><th>Location</th></tr></thead><tbody>'+vrows+'</tbody></table>';
2243
+ }else{
2244
+ vl.innerHTML='<p style="color:#22c55e">&#10003; No violations found</p>';
2245
+ }
2246
+ }).catch(function(e){
2247
+ document.getElementById('metrics-bubble').innerHTML='<p style="color:var(--danger);padding:1rem">Error: '+e.message+'</p>';
2248
+ });
2249
+ }
2250
+ var _metricsLoaded=false;
2251
+ var _origShowTab=typeof showTab==='function'?showTab:null;
2252
+ if(_origShowTab){
2253
+ showTab=function(name){
2254
+ _origShowTab(name);
2255
+ if(name==='metrics'&&!_metricsLoaded){_metricsLoaded=true;loadAllMetrics();}
2256
+ };
2257
+ }
2258
+ var metricsTab=document.querySelector('[onclick*="metrics"]');
2259
+ if(metricsTab)metricsTab.addEventListener('click',function(){if(!_metricsLoaded){_metricsLoaded=true;loadAllMetrics();}});
2260
+ </script>
2261
+ <script>
1984
2262
  // Self-diagnostic — detect if the external JS failed to load
1985
2263
  (function() {
1986
2264
  if (typeof showTab !== 'function') {
@@ -2009,14 +2287,69 @@ function renderToolbarHtml(ctx: {
2009
2287
  }): string {
2010
2288
  const nodeVersion = process.version;
2011
2289
  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;">
2012
- <span style="color:#2e7d32;font-weight:bold;">Tina4 v${ctx.version}</span>
2290
+ <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>
2291
+ <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;">
2292
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
2293
+ <strong style="color:#89b4fa;">Version Info</strong>
2294
+ <span onclick="document.getElementById('tina4-ver-modal').style.display='none'" style="cursor:pointer;color:#888;">&times;</span>
2295
+ </div>
2296
+ <div id="tina4-ver-body" style="line-height:1.8;">
2297
+ <div>Current: <strong style="color:#a6e3a1;">v${ctx.version}</strong></div>
2298
+ <div id="tina4-ver-latest" style="color:#888;">Checking for updates...</div>
2299
+ </div>
2300
+ </div>
2013
2301
  <span style="color:#4caf50;">${ctx.method}</span>
2014
2302
  <span>${ctx.path}</span>
2015
2303
  <span style="color:#666;">&rarr; ${ctx.matchedPattern}</span>
2016
2304
  <span style="color:#ffeb3b;">req:${ctx.requestId}</span>
2017
2305
  <span style="color:#90caf9;">${ctx.routeCount} routes</span>
2018
2306
  <span style="color:#888;">Node.js ${nodeVersion}</span>
2019
- <a href="#" onclick="(function(e){e.preventDefault();var p=document.getElementById('tina4-dev-panel');if(p){p.style.display=p.style.display==='none'?'block':'none';return;}var c=document.createElement('div');c.id='tina4-dev-panel';c.style.cssText='position:fixed;bottom:2rem;right:1rem;width:min(90vw,1200px);height:min(80vh,700px);z-index:99998;transition:all 0.2s';var f=document.createElement('iframe');f.src='/__dev';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';c.appendChild(f);document.body.appendChild(c);})(event)" style="color:#ef9a9a;margin-left:auto;text-decoration:none;cursor:pointer;">Dashboard &#8599;</a>
2307
+ <a href="#" onclick="(function(e){e.preventDefault();var p=document.getElementById('tina4-dev-panel');if(p){p.style.display=p.style.display==='none'?'block':'none';return;}var c=document.createElement('div');c.id='tina4-dev-panel';c.style.cssText='position:fixed;top:3rem;left:0;right:0;bottom:2rem;z-index:99998;transition:all 0.2s';var f=document.createElement('iframe');f.src='/__dev';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';c.appendChild(f);document.body.appendChild(c);})(event)" style="color:#ef9a9a;margin-left:auto;text-decoration:none;cursor:pointer;">Dashboard &#8599;</a>
2020
2308
  <span onclick="this.parentElement.style.display='none'" style="cursor:pointer;color:#888;margin-left:8px;">&#10005;</span>
2021
- </div>`;
2309
+ </div>
2310
+ <script>
2311
+ function tina4VersionModal(){
2312
+ var m=document.getElementById('tina4-ver-modal');
2313
+ if(m.style.display==='block'){m.style.display='none';return;}
2314
+ m.style.display='block';
2315
+ var el=document.getElementById('tina4-ver-latest');
2316
+ el.innerHTML='Checking for updates...';
2317
+ el.style.color='#888';
2318
+ fetch('/__dev/api/version-check')
2319
+ .then(function(r){return r.json()})
2320
+ .then(function(d){
2321
+ var latest=d.latest;
2322
+ var current=d.current;
2323
+ if(latest===current){
2324
+ el.innerHTML='Latest: <strong style="color:#a6e3a1;">v'+latest+'</strong> &mdash; You are up to date!';
2325
+ el.style.color='#a6e3a1';
2326
+ }else{
2327
+ var cParts=current.split('.').map(Number);
2328
+ var lParts=latest.split('.').map(Number);
2329
+ var isNewer=false;
2330
+ for(var i=0;i<Math.max(cParts.length,lParts.length);i++){
2331
+ var c=cParts[i]||0,l=lParts[i]||0;
2332
+ if(l>c){isNewer=true;break;}
2333
+ if(l<c)break;
2334
+ }
2335
+ if(isNewer){
2336
+ var breaking=(lParts[0]!==cParts[0]||lParts[1]!==cParts[1]);
2337
+ el.innerHTML='Latest: <strong style="color:#f9e2af;">v'+latest+'</strong>';
2338
+ if(breaking){
2339
+ 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>';
2340
+ }else{
2341
+ 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>';
2342
+ }
2343
+ }else{
2344
+ el.innerHTML='Latest: <strong style="color:#a6e3a1;">v'+latest+'</strong> &mdash; You are up to date!';
2345
+ el.style.color='#a6e3a1';
2346
+ }
2347
+ }
2348
+ })
2349
+ .catch(function(){
2350
+ el.innerHTML='Could not check for updates (offline?)';
2351
+ el.style.color='#f38ba8';
2352
+ });
2353
+ }
2354
+ </script>`;
2022
2355
  }
@@ -20,7 +20,7 @@ export { discoverRoutes } from "./routeDiscovery.js";
20
20
  export { MiddlewareChain, MiddlewareRunner, cors, requestLogger, CorsMiddleware, RateLimiterMiddleware, RequestLogger, SecurityHeadersMiddleware, CsrfMiddleware } from "./middleware.js";
21
21
  export type { CorsConfig } from "./middleware.js";
22
22
  export { createRequest, parseBody } from "./request.js";
23
- export { createResponse, errorResponse } from "./response.js";
23
+ export { createResponse, errorResponse, setDefaultTemplatesDir } from "./response.js";
24
24
  export { tryServeStatic } from "./static.js";
25
25
  export { loadEnv, getEnv, requireEnv, hasEnv, allEnv, resetEnv, isTruthy } from "./dotenv.js";
26
26
  export { Log } from "./logger.js";
@@ -76,8 +76,8 @@ export { WSDLService, WSDLOp } from "./wsdl.js";
76
76
  export type { WSDLOperation } from "./wsdl.js";
77
77
  export { HtmlElement, htmlElement, addHtmlHelpers } from "./htmlElement.js";
78
78
  export { renderErrorOverlay, renderProductionError, isDebugMode } from "./errorOverlay.js";
79
- export { detectAi, detectAiNames, generateContext, installAiContext, installAllAiContext, aiStatusReport } from "./ai.js";
80
- export type { AiTool, AiDetection } from "./ai.js";
79
+ export { AI_TOOLS, isInstalled, showMenu, installSelected, installAll, generateContext } from "./ai.js";
80
+ export type { AiTool } from "./ai.js";
81
81
  export type { ImapMessage, ImapFullMessage } from "./messenger.js";
82
82
  export { RabbitMQBackend } from "./queueBackends/rabbitmqBackend.js";
83
83
  export type { RabbitMQConfig } from "./queueBackends/rabbitmqBackend.js";