tina4ruby 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.
- checksums.yaml +4 -4
- data/lib/tina4/ai.rb +237 -211
- data/lib/tina4/cli.rb +5 -13
- data/lib/tina4/dev_admin.rb +281 -2
- data/lib/tina4/frond.rb +15 -3
- data/lib/tina4/metrics.rb +673 -0
- data/lib/tina4/rack_app.rb +57 -2
- data/lib/tina4/request.rb +40 -2
- data/lib/tina4/response.rb +7 -2
- data/lib/tina4/router.rb +5 -1
- data/lib/tina4/version.rb +1 -1
- metadata +8 -4
data/lib/tina4/dev_admin.rb
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "digest"
|
|
5
5
|
require "tmpdir"
|
|
6
|
+
require "net/http"
|
|
7
|
+
require "uri"
|
|
8
|
+
require_relative "metrics"
|
|
6
9
|
|
|
7
10
|
module Tina4
|
|
8
11
|
# Thread-safe in-memory message log for dev dashboard
|
|
@@ -423,6 +426,15 @@ module Tina4
|
|
|
423
426
|
body = read_json_body(env)
|
|
424
427
|
name = (body && body["name"]) || ""
|
|
425
428
|
json_response(gallery_deploy(name))
|
|
429
|
+
when ["GET", "/__dev/api/version-check"]
|
|
430
|
+
json_response(version_check_payload)
|
|
431
|
+
when ["GET", "/__dev/api/metrics"]
|
|
432
|
+
json_response(Tina4::Metrics.quick_metrics)
|
|
433
|
+
when ["GET", "/__dev/api/metrics/full"]
|
|
434
|
+
json_response(Tina4::Metrics.full_analysis)
|
|
435
|
+
when ["GET", "/__dev/api/metrics/file"]
|
|
436
|
+
file_path = (query_param(env, "path") || "").to_s
|
|
437
|
+
json_response(Tina4::Metrics.file_detail(file_path))
|
|
426
438
|
else
|
|
427
439
|
nil
|
|
428
440
|
end
|
|
@@ -451,6 +463,27 @@ module Tina4
|
|
|
451
463
|
[200, { "content-type" => "application/json; charset=utf-8" }, [body]]
|
|
452
464
|
end
|
|
453
465
|
|
|
466
|
+
def version_check_payload
|
|
467
|
+
current = Tina4::VERSION
|
|
468
|
+
latest = current
|
|
469
|
+
begin
|
|
470
|
+
uri = URI.parse("https://rubygems.org/api/v1/versions/tina4ruby/latest.json")
|
|
471
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
472
|
+
http.use_ssl = true
|
|
473
|
+
http.open_timeout = 5
|
|
474
|
+
http.read_timeout = 5
|
|
475
|
+
req = Net::HTTP::Get.new(uri)
|
|
476
|
+
resp = http.request(req)
|
|
477
|
+
if resp.is_a?(Net::HTTPSuccess)
|
|
478
|
+
data = JSON.parse(resp.body)
|
|
479
|
+
latest = data["version"] || current
|
|
480
|
+
end
|
|
481
|
+
rescue StandardError
|
|
482
|
+
# Offline or timeout — return current as latest
|
|
483
|
+
end
|
|
484
|
+
{ current: current, latest: latest }
|
|
485
|
+
end
|
|
486
|
+
|
|
454
487
|
def serve_dashboard
|
|
455
488
|
[200, { "content-type" => "text/html; charset=utf-8" }, [render_dashboard]]
|
|
456
489
|
end
|
|
@@ -810,6 +843,7 @@ module Tina4
|
|
|
810
843
|
.dev-header {
|
|
811
844
|
background: var(--surface); border-bottom: 1px solid var(--border);
|
|
812
845
|
padding: 0.75rem 1.5rem; display: flex; align-items: center; gap: 1rem;
|
|
846
|
+
position: sticky; top: 0; z-index: 100;
|
|
813
847
|
}
|
|
814
848
|
.dev-header h1 { font-size: 1rem; font-weight: 600; }
|
|
815
849
|
.dev-header .badge {
|
|
@@ -819,6 +853,7 @@ module Tina4
|
|
|
819
853
|
.dev-tabs {
|
|
820
854
|
display: flex; gap: 0; background: var(--surface);
|
|
821
855
|
border-bottom: 1px solid var(--border); overflow-x: auto;
|
|
856
|
+
position: sticky; top: 2.75rem; z-index: 100;
|
|
822
857
|
}
|
|
823
858
|
.dev-tab {
|
|
824
859
|
padding: 0.6rem 1rem; cursor: pointer; font-size: 0.8rem;
|
|
@@ -832,10 +867,10 @@ module Tina4
|
|
|
832
867
|
background: var(--border); color: var(--muted); padding: 0.1rem 0.4rem;
|
|
833
868
|
border-radius: 0.75rem; font-size: 0.65rem; margin-left: 0.25rem;
|
|
834
869
|
}
|
|
835
|
-
.dev-content { padding:
|
|
870
|
+
.dev-content { padding: 0.25rem; }
|
|
836
871
|
.dev-panel {
|
|
837
872
|
background: var(--surface); border: 1px solid var(--border);
|
|
838
|
-
border-radius: var(--radius); overflow:
|
|
873
|
+
border-radius: var(--radius); overflow: visible;
|
|
839
874
|
}
|
|
840
875
|
.dev-panel-header {
|
|
841
876
|
padding: 0.75rem 1rem; border-bottom: 1px solid var(--border);
|
|
@@ -952,6 +987,7 @@ module Tina4
|
|
|
952
987
|
<button class="dev-tab" onclick="showTab('system', event)">System</button>
|
|
953
988
|
<button class="dev-tab" onclick="showTab('tools', event)">Tools</button>
|
|
954
989
|
<button class="dev-tab" onclick="showTab('connections', event)">Connections</button>
|
|
990
|
+
<button class="dev-tab" onclick="showTab('metrics', event)">Metrics</button>
|
|
955
991
|
<button class="dev-tab" onclick="showTab('chat', event)">Tina4</button>
|
|
956
992
|
</div>
|
|
957
993
|
|
|
@@ -1289,6 +1325,30 @@ module Tina4
|
|
|
1289
1325
|
});
|
|
1290
1326
|
</script>
|
|
1291
1327
|
|
|
1328
|
+
<!-- Metrics Panel -->
|
|
1329
|
+
<div id="panel-metrics" class="dev-panel hidden">
|
|
1330
|
+
<div class="dev-panel-header">
|
|
1331
|
+
<h2>Code Metrics</h2>
|
|
1332
|
+
<div>
|
|
1333
|
+
<button class="btn btn-sm" onclick="loadAllMetrics()">Refresh</button>
|
|
1334
|
+
</div>
|
|
1335
|
+
</div>
|
|
1336
|
+
<div id="metrics-bubble" style="margin:1rem;"></div>
|
|
1337
|
+
<div id="metrics-drilldown" style="margin:0 1rem;display:none;"></div>
|
|
1338
|
+
<div id="metrics-quick" class="sys-grid"></div>
|
|
1339
|
+
<div id="metrics-largest" style="margin-top:1rem;"></div>
|
|
1340
|
+
<div id="metrics-tables" style="margin-top:1rem;padding:0 1rem 1rem;overflow-x:auto;">
|
|
1341
|
+
<h3 style="margin:1rem 0 0.5rem;color:var(--primary);">File Analysis</h3>
|
|
1342
|
+
<div id="metrics-heatmap"></div>
|
|
1343
|
+
<h3 style="margin:1rem 0 0.5rem;color:var(--primary);">Most Complex Functions</h3>
|
|
1344
|
+
<div id="metrics-complex"></div>
|
|
1345
|
+
<h3 style="margin:1rem 0 0.5rem;color:var(--primary);">Coupling Analysis</h3>
|
|
1346
|
+
<div id="metrics-coupling"></div>
|
|
1347
|
+
<h3 style="margin:1rem 0 0.5rem;color:var(--primary);">Violations</h3>
|
|
1348
|
+
<div id="metrics-violations"></div>
|
|
1349
|
+
</div>
|
|
1350
|
+
</div>
|
|
1351
|
+
|
|
1292
1352
|
<!-- Chat Panel (Tina4) -->
|
|
1293
1353
|
<div id="panel-chat" class="dev-panel hidden">
|
|
1294
1354
|
<div class="dev-panel-header">
|
|
@@ -1318,6 +1378,225 @@ module Tina4
|
|
|
1318
1378
|
|
|
1319
1379
|
<script src="/__dev/js/tina4-dev-admin.min.js"></script>
|
|
1320
1380
|
<script>
|
|
1381
|
+
// ── Metrics Panel JS ──
|
|
1382
|
+
var _metricsFullData=null;
|
|
1383
|
+
function miColor(mi){
|
|
1384
|
+
if(mi>=60) return 'rgb('+(Math.round(34+(1-((mi-60)/40))*186))+','+(Math.round(197-(1-((mi-60)/40))*50))+',0)';
|
|
1385
|
+
if(mi>=30) return 'rgb('+(Math.round(220+((60-mi)/30)*19))+','+(Math.round(180-((60-mi)/30)*112))+',0)';
|
|
1386
|
+
return 'rgb(239,'+(Math.round(68-mi*2))+',0)';
|
|
1387
|
+
}
|
|
1388
|
+
function renderBubbleChart(files){
|
|
1389
|
+
var container=document.getElementById('metrics-bubble');
|
|
1390
|
+
if(!files||!files.length){container.innerHTML='<p style="color:var(--muted);padding:1rem">No files to analyze</p>';return;}
|
|
1391
|
+
var W=container.offsetWidth||900,H=Math.max(450,Math.min(650,W*0.45));
|
|
1392
|
+
var maxLoc=Math.max.apply(null,files.map(function(f){return f.loc}))||1;
|
|
1393
|
+
var minR=14,maxR=Math.min(70,W/10);
|
|
1394
|
+
var sorted=files.slice().sort(function(a,b){return a.loc-b.loc});
|
|
1395
|
+
var cx=W/2,cy=H/2;
|
|
1396
|
+
var bubbles=[];
|
|
1397
|
+
var angle=0,spiralR=0;
|
|
1398
|
+
for(var i=0;i<sorted.length;i++){
|
|
1399
|
+
var f=sorted[i];
|
|
1400
|
+
var r=minR+Math.sqrt(f.loc/maxLoc)*(maxR-minR);
|
|
1401
|
+
var color=miColor(f.maintainability||0);
|
|
1402
|
+
var placed=false;
|
|
1403
|
+
for(var attempt=0;attempt<800;attempt++){
|
|
1404
|
+
var px=cx+spiralR*Math.cos(angle);
|
|
1405
|
+
var py=cy+spiralR*Math.sin(angle);
|
|
1406
|
+
var collides=false;
|
|
1407
|
+
for(var j=0;j<bubbles.length;j++){
|
|
1408
|
+
var dx=px-bubbles[j].x,dy=py-bubbles[j].y;
|
|
1409
|
+
if(Math.sqrt(dx*dx+dy*dy)<r+bubbles[j].r+2){collides=true;break;}
|
|
1410
|
+
}
|
|
1411
|
+
if(!collides&&px>r+2&&px<W-r-2&&py>r+25&&py<H-r-2){
|
|
1412
|
+
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});
|
|
1413
|
+
placed=true;break;
|
|
1414
|
+
}
|
|
1415
|
+
angle+=0.2;spiralR+=0.04;
|
|
1416
|
+
}
|
|
1417
|
+
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});}
|
|
1418
|
+
}
|
|
1419
|
+
var canvas=document.createElement('canvas');
|
|
1420
|
+
canvas.width=W;canvas.height=H;
|
|
1421
|
+
canvas.style.cssText='display:block;border:1px solid var(--border);border-radius:8px;cursor:pointer;background:#0f172a';
|
|
1422
|
+
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>';
|
|
1423
|
+
container.appendChild(canvas);
|
|
1424
|
+
var ctx=canvas.getContext('2d');
|
|
1425
|
+
var hoveredIdx=-1;
|
|
1426
|
+
var t=0;
|
|
1427
|
+
function draw(){
|
|
1428
|
+
t+=0.016;
|
|
1429
|
+
ctx.clearRect(0,0,W,H);
|
|
1430
|
+
ctx.strokeStyle='rgba(255,255,255,0.03)';ctx.lineWidth=1;
|
|
1431
|
+
for(var gx=0;gx<W;gx+=50){ctx.beginPath();ctx.moveTo(gx,0);ctx.lineTo(gx,H);ctx.stroke();}
|
|
1432
|
+
for(var gy=0;gy<H;gy+=50){ctx.beginPath();ctx.moveTo(0,gy);ctx.lineTo(W,gy);ctx.stroke();}
|
|
1433
|
+
bubbles.forEach(function(b,idx){
|
|
1434
|
+
var ox=Math.sin(t*b.speed+b.angle)*b.drift;
|
|
1435
|
+
var oy=Math.cos(t*b.speed*0.7+b.angle+1)*b.drift*0.6;
|
|
1436
|
+
var bx=b.x+ox,by=b.y+oy;
|
|
1437
|
+
var isHovered=(idx===hoveredIdx);
|
|
1438
|
+
var drawR=isHovered?b.r+4:b.r;
|
|
1439
|
+
if(isHovered){
|
|
1440
|
+
ctx.beginPath();ctx.arc(bx,by,drawR+8,0,Math.PI*2);
|
|
1441
|
+
ctx.fillStyle='rgba(255,255,255,0.08)';ctx.fill();
|
|
1442
|
+
}
|
|
1443
|
+
ctx.beginPath();ctx.arc(bx,by,drawR,0,Math.PI*2);
|
|
1444
|
+
ctx.fillStyle=b.color;ctx.globalAlpha=isHovered?0.95:0.7;ctx.fill();
|
|
1445
|
+
ctx.globalAlpha=1;ctx.strokeStyle=b.color;ctx.lineWidth=isHovered?2.5:1.5;ctx.stroke();
|
|
1446
|
+
var name=b.f.path.split('/').pop().replace('.rb','');
|
|
1447
|
+
if(drawR>16){
|
|
1448
|
+
var fs=Math.max(8,Math.min(13,drawR*0.38));
|
|
1449
|
+
ctx.fillStyle='#fff';ctx.font='600 '+fs+'px monospace';ctx.textAlign='center';
|
|
1450
|
+
ctx.fillText(name,bx,by-2);
|
|
1451
|
+
ctx.fillStyle='rgba(255,255,255,0.65)';ctx.font=(fs-1)+'px monospace';
|
|
1452
|
+
ctx.fillText(b.f.loc+' LOC',bx,by+fs);
|
|
1453
|
+
if(isHovered&&drawR>25){
|
|
1454
|
+
ctx.fillStyle='rgba(255,255,255,0.5)';ctx.font=(fs-2)+'px monospace';
|
|
1455
|
+
ctx.fillText('CC:'+b.f.complexity+' MI:'+b.f.maintainability,bx,by+fs*2);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
b._drawX=bx;b._drawY=by;b._drawR=drawR;
|
|
1459
|
+
});
|
|
1460
|
+
var totalLoc=0,totalFiles=bubbles.length;
|
|
1461
|
+
bubbles.forEach(function(b){totalLoc+=b.f.loc});
|
|
1462
|
+
var avgMI=bubbles.reduce(function(s,b){return s+b.f.maintainability},0)/totalFiles;
|
|
1463
|
+
ctx.fillStyle='rgba(255,255,255,0.35)';ctx.font='11px monospace';ctx.textAlign='right';
|
|
1464
|
+
ctx.fillText(totalFiles+' files | '+totalLoc.toLocaleString()+' LOC | Avg MI: '+avgMI.toFixed(1),W-12,H-10);
|
|
1465
|
+
window._metricsAnimFrame=requestAnimationFrame(draw);
|
|
1466
|
+
}
|
|
1467
|
+
draw();
|
|
1468
|
+
canvas.addEventListener('mousemove',function(e){
|
|
1469
|
+
var rect=canvas.getBoundingClientRect();
|
|
1470
|
+
var mx=e.clientX-rect.left,my=e.clientY-rect.top;
|
|
1471
|
+
hoveredIdx=-1;
|
|
1472
|
+
for(var i=bubbles.length-1;i>=0;i--){
|
|
1473
|
+
var b=bubbles[i];
|
|
1474
|
+
var dx=mx-b._drawX,dy=my-b._drawY;
|
|
1475
|
+
if(Math.sqrt(dx*dx+dy*dy)<=b._drawR){hoveredIdx=i;break;}
|
|
1476
|
+
}
|
|
1477
|
+
canvas.style.cursor=hoveredIdx>=0?'pointer':'default';
|
|
1478
|
+
});
|
|
1479
|
+
canvas.addEventListener('mouseleave',function(){hoveredIdx=-1;});
|
|
1480
|
+
canvas.addEventListener('click',function(e){
|
|
1481
|
+
if(hoveredIdx<0)return;
|
|
1482
|
+
var f=bubbles[hoveredIdx].f;
|
|
1483
|
+
drillDownFile(f.path);
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
function drillDownFile(path){
|
|
1487
|
+
var dd=document.getElementById('metrics-drilldown');
|
|
1488
|
+
dd.style.display='block';
|
|
1489
|
+
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('metrics-drilldown').style.display='none'">Close</button></div><div class="p-md"><p style="color:var(--muted)">Loading file analysis...</p></div></div>';
|
|
1490
|
+
fetch('/__dev/api/metrics/file?path='+encodeURIComponent(path)).then(function(r){return r.json()}).then(function(d){
|
|
1491
|
+
if(d.error){dd.querySelector('.p-md').innerHTML='<p style="color:var(--danger)">'+d.error+'</p>';return;}
|
|
1492
|
+
var html='<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:0.5rem;margin-bottom:1rem">';
|
|
1493
|
+
html+='<div class="sys-card"><div class="label">LOC</div><div class="value">'+d.loc+'</div></div>';
|
|
1494
|
+
html+='<div class="sys-card"><div class="label">Total Lines</div><div class="value">'+d.total_lines+'</div></div>';
|
|
1495
|
+
html+='<div class="sys-card"><div class="label">Classes</div><div class="value">'+d.classes+'</div></div>';
|
|
1496
|
+
html+='<div class="sys-card"><div class="label">Functions</div><div class="value">'+(d.functions?d.functions.length:0)+'</div></div>';
|
|
1497
|
+
html+='<div class="sys-card"><div class="label">Imports</div><div class="value">'+(d.imports?d.imports.length:0)+'</div></div>';
|
|
1498
|
+
html+='</div>';
|
|
1499
|
+
if(d.functions&&d.functions.length){
|
|
1500
|
+
html+='<h3 style="margin:0.5rem 0;color:var(--primary);font-size:0.85rem">Cyclomatic Complexity by Function</h3>';
|
|
1501
|
+
var maxCC=Math.max.apply(null,d.functions.map(function(f){return f.complexity}))||1;
|
|
1502
|
+
html+='<div style="display:flex;flex-direction:column;gap:4px">';
|
|
1503
|
+
d.functions.forEach(function(f){
|
|
1504
|
+
var pct=Math.max(3,f.complexity/maxCC*100);
|
|
1505
|
+
var color=f.complexity>20?'#ef4444':f.complexity>10?'#eab308':f.complexity>5?'#3b82f6':'#22c55e';
|
|
1506
|
+
html+='<div style="display:flex;align-items:center;gap:8px;font-size:0.75rem;font-family:var(--mono)">';
|
|
1507
|
+
html+='<span style="width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)" title="'+f.name+'">'+f.name+'</span>';
|
|
1508
|
+
html+='<div style="flex:1;height:16px;background:var(--bg);border-radius:3px;overflow:hidden;position:relative">';
|
|
1509
|
+
html+='<div style="width:'+pct+'%;height:100%;background:'+color+';border-radius:3px;transition:width 0.3s"></div>';
|
|
1510
|
+
html+='</div>';
|
|
1511
|
+
html+='<span style="width:70px;text-align:right;color:'+color+';font-weight:600">CC:'+f.complexity+'</span>';
|
|
1512
|
+
html+='<span style="width:60px;text-align:right;color:var(--muted)">'+f.loc+' LOC</span>';
|
|
1513
|
+
html+='<span style="width:30px;text-align:right;color:var(--muted)">L'+f.line+'</span>';
|
|
1514
|
+
html+='</div>';
|
|
1515
|
+
});
|
|
1516
|
+
html+='</div>';
|
|
1517
|
+
}
|
|
1518
|
+
if(d.imports&&d.imports.length){
|
|
1519
|
+
html+='<h3 style="margin:0.75rem 0 0.25rem;color:var(--primary);font-size:0.85rem">Dependencies</h3>';
|
|
1520
|
+
html+='<div style="display:flex;flex-wrap:wrap;gap:4px">';
|
|
1521
|
+
d.imports.forEach(function(imp){
|
|
1522
|
+
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>';
|
|
1523
|
+
});
|
|
1524
|
+
html+='</div>';
|
|
1525
|
+
}
|
|
1526
|
+
dd.querySelector('.p-md').innerHTML=html;
|
|
1527
|
+
}).catch(function(e){
|
|
1528
|
+
dd.querySelector('.p-md').innerHTML='<p style="color:var(--danger)">Error: '+e.message+'</p>';
|
|
1529
|
+
});
|
|
1530
|
+
dd.scrollIntoView({behavior:'smooth',block:'start'});
|
|
1531
|
+
}
|
|
1532
|
+
function loadAllMetrics(){
|
|
1533
|
+
if(window._metricsAnimFrame)cancelAnimationFrame(window._metricsAnimFrame);
|
|
1534
|
+
var el=document.getElementById('metrics-quick');
|
|
1535
|
+
el.innerHTML='<div class="sys-card"><div class="value">Loading...</div></div>';
|
|
1536
|
+
fetch('/__dev/api/metrics').then(function(r){return r.json()}).then(function(d){
|
|
1537
|
+
if(d.error){el.innerHTML='<div class="sys-card"><div class="value" style="color:var(--danger)">'+d.error+'</div></div>';return;}
|
|
1538
|
+
el.innerHTML=
|
|
1539
|
+
'<div class="sys-card"><div class="label">Ruby Files</div><div class="value">'+d.file_count+'</div></div>'+
|
|
1540
|
+
'<div class="sys-card"><div class="label">Lines of Code</div><div class="value">'+d.total_loc.toLocaleString()+'</div></div>'+
|
|
1541
|
+
'<div class="sys-card"><div class="label">Comment Lines</div><div class="value">'+d.total_comment.toLocaleString()+'</div></div>'+
|
|
1542
|
+
'<div class="sys-card"><div class="label">Blank Lines</div><div class="value">'+d.total_blank.toLocaleString()+'</div></div>'+
|
|
1543
|
+
'<div class="sys-card"><div class="label">Classes</div><div class="value">'+d.classes+'</div></div>'+
|
|
1544
|
+
'<div class="sys-card"><div class="label">Functions</div><div class="value">'+d.functions+'</div></div>'+
|
|
1545
|
+
'<div class="sys-card"><div class="label">Routes</div><div class="value">'+d.route_count+'</div></div>'+
|
|
1546
|
+
'<div class="sys-card"><div class="label">ORM Models</div><div class="value">'+d.orm_count+'</div></div>'+
|
|
1547
|
+
'<div class="sys-card"><div class="label">Templates</div><div class="value">'+d.template_count+'</div></div>'+
|
|
1548
|
+
'<div class="sys-card"><div class="label">Migrations</div><div class="value">'+d.migration_count+'</div></div>';
|
|
1549
|
+
}).catch(function(e){el.innerHTML='<div class="sys-card"><div class="value" style="color:var(--danger)">Error: '+e.message+'</div></div>';});
|
|
1550
|
+
document.getElementById('metrics-bubble').innerHTML='<p style="color:var(--muted);padding:1rem">Analyzing codebase...</p>';
|
|
1551
|
+
fetch('/__dev/api/metrics/full').then(function(r){return r.json()}).then(function(d){
|
|
1552
|
+
_metricsFullData=d;
|
|
1553
|
+
if(d.error){document.getElementById('metrics-bubble').innerHTML='<p style="color:var(--danger);padding:1rem">'+d.error+'</p>';return;}
|
|
1554
|
+
renderBubbleChart(d.file_metrics);
|
|
1555
|
+
var hm=document.getElementById('metrics-heatmap');
|
|
1556
|
+
var rows=d.file_metrics.map(function(f){
|
|
1557
|
+
var color=miColor(f.maintainability);
|
|
1558
|
+
var barW=Math.max(2,Math.min(100,f.maintainability));
|
|
1559
|
+
return '<tr style="cursor:pointer" onclick="drillDownFile(''+f.path+'')"><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>';
|
|
1560
|
+
}).join('');
|
|
1561
|
+
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>';
|
|
1562
|
+
var cf=document.getElementById('metrics-complex');
|
|
1563
|
+
var frows=d.most_complex_functions.map(function(f){
|
|
1564
|
+
var color=f.complexity>20?'#ef4444':f.complexity>10?'#eab308':'#22c55e';
|
|
1565
|
+
return '<tr style="cursor:pointer" onclick="drillDownFile(''+f.file+'')"><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>';
|
|
1566
|
+
}).join('');
|
|
1567
|
+
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>';
|
|
1568
|
+
var cp=document.getElementById('metrics-coupling');
|
|
1569
|
+
var crows=d.file_metrics.filter(function(f){return f.coupling_afferent>0||f.coupling_efferent>0}).map(function(f){
|
|
1570
|
+
return '<tr style="cursor:pointer" onclick="drillDownFile(''+f.path+'')"><td>'+f.path+'</td><td>'+f.coupling_afferent+'</td><td>'+f.coupling_efferent+'</td><td>'+f.instability+'</td></tr>';
|
|
1571
|
+
}).join('');
|
|
1572
|
+
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>';
|
|
1573
|
+
var vl=document.getElementById('metrics-violations');
|
|
1574
|
+
if(d.violations&&d.violations.length){
|
|
1575
|
+
var vrows=d.violations.map(function(v){
|
|
1576
|
+
var icon=v.type==='error'?'⚠':'ⓘ';
|
|
1577
|
+
var color=v.type==='error'?'#ef4444':'#eab308';
|
|
1578
|
+
return '<tr style="cursor:pointer" onclick="drillDownFile(''+v.file+'')"><td style="color:'+color+'">'+icon+'</td><td>'+v.message+'</td><td>'+v.file+(v.line?':'+v.line:'')+'</td></tr>';
|
|
1579
|
+
}).join('');
|
|
1580
|
+
vl.innerHTML='<table style="width:100%"><thead><tr><th></th><th>Issue</th><th>Location</th></tr></thead><tbody>'+vrows+'</tbody></table>';
|
|
1581
|
+
}else{
|
|
1582
|
+
vl.innerHTML='<p style="color:#22c55e">✓ No violations found</p>';
|
|
1583
|
+
}
|
|
1584
|
+
}).catch(function(e){
|
|
1585
|
+
document.getElementById('metrics-bubble').innerHTML='<p style="color:var(--danger);padding:1rem">Error: '+e.message+'</p>';
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
var _metricsLoaded=false;
|
|
1589
|
+
var _origShowTab=typeof showTab==='function'?showTab:null;
|
|
1590
|
+
if(_origShowTab){
|
|
1591
|
+
showTab=function(name){
|
|
1592
|
+
_origShowTab(name);
|
|
1593
|
+
if(name==='metrics'&&!_metricsLoaded){_metricsLoaded=true;loadAllMetrics();}
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
var metricsTab=document.querySelector('[onclick*="metrics"]');
|
|
1597
|
+
if(metricsTab)metricsTab.addEventListener('click',function(){if(!_metricsLoaded){_metricsLoaded=true;loadAllMetrics();}});
|
|
1598
|
+
</script>
|
|
1599
|
+
<script>
|
|
1321
1600
|
// Self-diagnostic — detect if the external JS failed to load
|
|
1322
1601
|
(function() {
|
|
1323
1602
|
if (typeof showTab !== 'function') {
|
data/lib/tina4/frond.rb
CHANGED
|
@@ -414,11 +414,23 @@ module Tina4
|
|
|
414
414
|
end
|
|
415
415
|
|
|
416
416
|
def render_with_blocks(parent_source, context, child_blocks)
|
|
417
|
+
engine = self
|
|
417
418
|
result = parent_source.gsub(BLOCK_RE) do
|
|
418
419
|
name = Regexp.last_match(1)
|
|
419
|
-
|
|
420
|
-
block_source = child_blocks.fetch(name,
|
|
421
|
-
|
|
420
|
+
parent_content = Regexp.last_match(2)
|
|
421
|
+
block_source = child_blocks.fetch(name, parent_content)
|
|
422
|
+
|
|
423
|
+
# Make parent() and super() available inside child blocks
|
|
424
|
+
rendered_parent = nil
|
|
425
|
+
get_parent = lambda do
|
|
426
|
+
rendered_parent ||= Tina4::SafeString.new(
|
|
427
|
+
engine.send(:render_tokens, tokenize(parent_content), context)
|
|
428
|
+
)
|
|
429
|
+
rendered_parent
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
block_ctx = context.merge("parent" => get_parent, "super" => get_parent)
|
|
433
|
+
render_tokens(tokenize(block_source), block_ctx)
|
|
422
434
|
end
|
|
423
435
|
render_tokens(tokenize(result), context)
|
|
424
436
|
end
|