tina4-nodejs 3.10.34 → 3.10.40

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,6 +17,7 @@ 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
 
@@ -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') {
@@ -2026,7 +2304,7 @@ function renderToolbarHtml(ctx: {
2026
2304
  <span style="color:#ffeb3b;">req:${ctx.requestId}</span>
2027
2305
  <span style="color:#90caf9;">${ctx.routeCount} routes</span>
2028
2306
  <span style="color:#888;">Node.js ${nodeVersion}</span>
2029
- <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>
2030
2308
  <span onclick="this.parentElement.style.display='none'" style="cursor:pointer;color:#888;margin-left:8px;">&#10005;</span>
2031
2309
  </div>
2032
2310
  <script>
@@ -2037,11 +2315,11 @@ function tina4VersionModal(){
2037
2315
  var el=document.getElementById('tina4-ver-latest');
2038
2316
  el.innerHTML='Checking for updates...';
2039
2317
  el.style.color='#888';
2040
- fetch('https://registry.npmjs.org/tina4-nodejs/latest')
2318
+ fetch('/__dev/api/version-check')
2041
2319
  .then(function(r){return r.json()})
2042
2320
  .then(function(d){
2043
- var latest=d.version;
2044
- var current='${ctx.version}';
2321
+ var latest=d.latest;
2322
+ var current=d.current;
2045
2323
  if(latest===current){
2046
2324
  el.innerHTML='Latest: <strong style="color:#a6e3a1;">v'+latest+'</strong> &mdash; You are up to date!';
2047
2325
  el.style.color='#a6e3a1';
@@ -2054,6 +2332,8 @@ function tina4VersionModal(){
2054
2332
  if(l>c){isNewer=true;break;}
2055
2333
  if(l<c)break;
2056
2334
  }
2335
+ var isAhead=false;
2336
+ 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;}}
2057
2337
  if(isNewer){
2058
2338
  var breaking=(lParts[0]!==cParts[0]||lParts[1]!==cParts[1]);
2059
2339
  el.innerHTML='Latest: <strong style="color:#f9e2af;">v'+latest+'</strong>';
@@ -2062,6 +2342,9 @@ function tina4VersionModal(){
2062
2342
  }else{
2063
2343
  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>';
2064
2344
  }
2345
+ }else if(isAhead){
2346
+ el.innerHTML='You are running <strong style="color:#cba6f7;">v'+current+'</strong> (ahead of npm <strong>v'+latest+'</strong> &mdash; not yet published).';
2347
+ el.style.color='#cba6f7';
2065
2348
  }else{
2066
2349
  el.innerHTML='Latest: <strong style="color:#a6e3a1;">v'+latest+'</strong> &mdash; You are up to date!';
2067
2350
  el.style.color='#a6e3a1';
@@ -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";