tina4ruby 3.10.42 → 3.10.44
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 +492 -134
- data/lib/tina4/cli.rb +784 -74
- data/lib/tina4/database.rb +12 -5
- data/lib/tina4/dev_admin.rb +266 -66
- data/lib/tina4/events.rb +19 -0
- data/lib/tina4/field_types.rb +6 -1
- data/lib/tina4/frond.rb +112 -81
- data/lib/tina4/metrics.rb +43 -2
- data/lib/tina4/orm.rb +17 -7
- data/lib/tina4/public/js/tina4-dev-admin.min.js +167 -28
- data/lib/tina4/query_builder.rb +8 -2
- data/lib/tina4/rack_app.rb +17 -1
- data/lib/tina4/template.rb +57 -15
- data/lib/tina4/version.rb +1 -1
- metadata +1 -1
data/lib/tina4/database.rb
CHANGED
|
@@ -281,12 +281,19 @@ module Tina4
|
|
|
281
281
|
end
|
|
282
282
|
|
|
283
283
|
def execute_many(sql, params_list = [])
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
284
|
+
results = []
|
|
285
|
+
drv = current_driver
|
|
286
|
+
drv.begin_transaction
|
|
287
|
+
begin
|
|
288
|
+
params_list.each do |params|
|
|
289
|
+
results << drv.execute(sql, params)
|
|
290
|
+
end
|
|
291
|
+
drv.commit
|
|
292
|
+
rescue => e
|
|
293
|
+
drv.rollback
|
|
294
|
+
raise e
|
|
288
295
|
end
|
|
289
|
-
|
|
296
|
+
results
|
|
290
297
|
end
|
|
291
298
|
|
|
292
299
|
def transaction
|
data/lib/tina4/dev_admin.rb
CHANGED
|
@@ -498,6 +498,14 @@ module Tina4
|
|
|
498
498
|
end
|
|
499
499
|
|
|
500
500
|
def status_payload
|
|
501
|
+
db_table_count = 0
|
|
502
|
+
begin
|
|
503
|
+
db = Tina4.database
|
|
504
|
+
db_table_count = db.tables.size if db
|
|
505
|
+
rescue
|
|
506
|
+
# ignore
|
|
507
|
+
end
|
|
508
|
+
|
|
501
509
|
{
|
|
502
510
|
framework: "tina4-ruby",
|
|
503
511
|
version: Tina4::VERSION,
|
|
@@ -505,6 +513,8 @@ module Tina4
|
|
|
505
513
|
platform: RUBY_PLATFORM,
|
|
506
514
|
debug: ENV["TINA4_DEBUG"] || "false",
|
|
507
515
|
log_level: ENV["TINA4_LOG_LEVEL"] || "ERROR",
|
|
516
|
+
database: ENV["DATABASE_URL"] || "not configured",
|
|
517
|
+
db_tables: db_table_count,
|
|
508
518
|
uptime: (Time.now - (defined?(@boot_time) && @boot_time ? @boot_time : (@boot_time = Time.now))).round(1),
|
|
509
519
|
route_count: Tina4::Router.routes.size,
|
|
510
520
|
request_stats: request_inspector.stats,
|
|
@@ -574,7 +584,9 @@ module Tina4
|
|
|
574
584
|
},
|
|
575
585
|
pid: Process.pid,
|
|
576
586
|
thread_count: Thread.list.size,
|
|
577
|
-
env: ENV["TINA4_ENV"] || ENV["RACK_ENV"] || ENV["RUBY_ENV"] || "development"
|
|
587
|
+
env: ENV["TINA4_ENV"] || ENV["RACK_ENV"] || ENV["RUBY_ENV"] || "development",
|
|
588
|
+
db_tables: (begin; db = Tina4.database; db ? db.tables.size : 0; rescue; 0; end),
|
|
589
|
+
db_connected: (begin; db = Tina4.database; !db.nil?; rescue; false; end)
|
|
578
590
|
}
|
|
579
591
|
end
|
|
580
592
|
|
|
@@ -599,19 +611,38 @@ module Tina4
|
|
|
599
611
|
sql = sql.to_s.strip
|
|
600
612
|
return { error: "No SQL provided" } if sql.empty?
|
|
601
613
|
|
|
602
|
-
first_word = sql.split(/[\s\t\n\r]+/, 2).first.to_s.upcase
|
|
603
|
-
unless %w[SELECT PRAGMA EXPLAIN SHOW DESCRIBE].include?(first_word)
|
|
604
|
-
return { error: "Only SELECT queries are allowed in the dev dashboard" }
|
|
605
|
-
end
|
|
606
|
-
|
|
607
614
|
db = Tina4.database
|
|
608
615
|
return { error: "No database configured" } unless db
|
|
609
616
|
|
|
617
|
+
# Split multiple statements on semicolons
|
|
618
|
+
statements = sql.split(";").map(&:strip).reject(&:empty?)
|
|
619
|
+
|
|
610
620
|
begin
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
621
|
+
if statements.size == 1
|
|
622
|
+
first_word = statements[0].split(/[\s\t\n\r]+/, 2).first.to_s.upcase
|
|
623
|
+
if %w[SELECT PRAGMA EXPLAIN SHOW DESCRIBE].include?(first_word)
|
|
624
|
+
result = db.fetch(statements[0])
|
|
625
|
+
rows = result.respond_to?(:to_a) ? result.to_a : (result.is_a?(Array) ? result : [])
|
|
626
|
+
columns = rows.first.is_a?(Hash) ? rows.first.keys.map(&:to_s) : []
|
|
627
|
+
return { columns: columns, rows: rows, count: rows.size }
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
# Execute all statements (single write or multi-statement batch)
|
|
632
|
+
total_affected = 0
|
|
633
|
+
db.start_transaction if db.respond_to?(:start_transaction)
|
|
634
|
+
begin
|
|
635
|
+
statements.each do |stmt|
|
|
636
|
+
result = db.execute(stmt)
|
|
637
|
+
total_affected += (result.respond_to?(:affected_rows) ? result.affected_rows : 0)
|
|
638
|
+
end
|
|
639
|
+
db.commit if db.respond_to?(:commit)
|
|
640
|
+
rescue => e
|
|
641
|
+
db.rollback if db.respond_to?(:rollback)
|
|
642
|
+
return { error: e.message }
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
{ affected: total_affected, success: true }
|
|
615
646
|
rescue => e
|
|
616
647
|
{ error: e.message }
|
|
617
648
|
end
|
|
@@ -1069,32 +1100,45 @@ module Tina4
|
|
|
1069
1100
|
<h2>Database</h2>
|
|
1070
1101
|
<button class="btn btn-sm" onclick="loadTables()">Refresh</button>
|
|
1071
1102
|
</div>
|
|
1072
|
-
<div
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1103
|
+
<div style="display:flex;height:calc(100vh - 140px);overflow:hidden">
|
|
1104
|
+
<!-- Left: Tables navigation -->
|
|
1105
|
+
<div style="width:200px;min-width:200px;border-right:1px solid var(--border);padding:0.5rem;overflow-y:auto;display:flex;flex-direction:column;gap:0.5rem">
|
|
1106
|
+
<div class="text-sm text-muted" style="font-weight:600">Tables</div>
|
|
1107
|
+
<div id="table-list" class="text-sm"></div>
|
|
1108
|
+
<div style="border-top:1px solid var(--border);padding-top:0.5rem;margin-top:auto">
|
|
1109
|
+
<div class="text-sm text-muted" style="font-weight:600;margin-bottom:0.25rem">Seed Data</div>
|
|
1110
|
+
<select id="seed-table" class="input" style="width:100%;margin-bottom:0.25rem;font-size:0.75rem"><option value="">Pick table...</option></select>
|
|
1111
|
+
<div class="flex gap-sm items-center">
|
|
1112
|
+
<input type="number" id="seed-count" class="input" value="10" min="1" max="1000" style="width:60px;font-size:0.75rem">
|
|
1113
|
+
<button class="btn btn-sm btn-success" onclick="seedTable()">Seed</button>
|
|
1114
|
+
</div>
|
|
1115
|
+
</div>
|
|
1116
|
+
</div>
|
|
1117
|
+
<!-- Right: Query + Results -->
|
|
1118
|
+
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;padding:0.5rem">
|
|
1119
|
+
<div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:0.25rem">
|
|
1120
|
+
<select id="query-type" class="input" style="width:auto;font-size:0.75rem">
|
|
1076
1121
|
<option value="sql">SQL</option>
|
|
1077
1122
|
</select>
|
|
1123
|
+
<span class="text-sm text-muted">Limit</span>
|
|
1124
|
+
<select id="query-limit" class="input" style="width:70px;font-size:0.75rem">
|
|
1125
|
+
<option value="20">20</option>
|
|
1126
|
+
<option value="50">50</option>
|
|
1127
|
+
<option value="100">100</option>
|
|
1128
|
+
<option value="500">500</option>
|
|
1129
|
+
<option value="0">All</option>
|
|
1130
|
+
</select>
|
|
1078
1131
|
<button class="btn btn-sm btn-primary" onclick="runQuery()">Run</button>
|
|
1132
|
+
<button class="btn btn-sm" id="btn-csv" onclick="copyResults('csv',this)" title="Copy results as CSV">Copy CSV</button>
|
|
1133
|
+
<button class="btn btn-sm" id="btn-json" onclick="copyResults('json',this)" title="Copy results as JSON">Copy JSON</button>
|
|
1134
|
+
<button class="btn btn-sm" onclick="pasteData()" title="Paste tab-separated data as INSERTs">Paste</button>
|
|
1079
1135
|
<span class="text-sm text-muted">Ctrl+Enter</span>
|
|
1080
1136
|
</div>
|
|
1081
|
-
<textarea id="query-input" rows="
|
|
1137
|
+
<textarea id="query-input" rows="3" placeholder="SELECT * FROM users LIMIT 20" class="input input-mono" style="width:100%;font-size:0.75rem;resize:vertical"></textarea>
|
|
1082
1138
|
<div id="query-error" class="hidden" style="color:var(--danger);font-size:0.75rem;margin-top:0.25rem"></div>
|
|
1083
|
-
|
|
1084
|
-
<div style="width:180px">
|
|
1085
|
-
<div class="text-sm text-muted" style="font-weight:600;margin-bottom:0.5rem">Tables</div>
|
|
1086
|
-
<div id="table-list" class="text-sm"></div>
|
|
1087
|
-
<div style="margin-top:0.75rem;border-top:1px solid var(--border);padding-top:0.75rem">
|
|
1088
|
-
<div class="text-sm text-muted" style="font-weight:600;margin-bottom:0.5rem">Seed Data</div>
|
|
1089
|
-
<select id="seed-table" class="input" style="width:100%;margin-bottom:0.25rem"><option value="">Pick table...</option></select>
|
|
1090
|
-
<div class="flex gap-sm items-center">
|
|
1091
|
-
<input type="number" id="seed-count" class="input" value="10" min="1" max="1000" style="width:60px">
|
|
1092
|
-
<button class="btn btn-sm btn-success" onclick="seedTable()">Seed</button>
|
|
1093
|
-
</div>
|
|
1094
|
-
</div>
|
|
1139
|
+
<div id="query-results" style="flex:1;overflow:auto;margin-top:0.25rem;font-size:0.75rem"></div>
|
|
1095
1140
|
</div>
|
|
1096
1141
|
</div>
|
|
1097
|
-
<div id="query-results" style="overflow-x:auto"></div>
|
|
1098
1142
|
</div>
|
|
1099
1143
|
|
|
1100
1144
|
<!-- Requests Panel -->
|
|
@@ -1385,20 +1429,38 @@ function miColor(mi){
|
|
|
1385
1429
|
if(mi>=30) return 'rgb('+(Math.round(220+((60-mi)/30)*19))+','+(Math.round(180-((60-mi)/30)*112))+',0)';
|
|
1386
1430
|
return 'rgb(239,'+(Math.round(68-mi*2))+',0)';
|
|
1387
1431
|
}
|
|
1388
|
-
function renderBubbleChart(files){
|
|
1432
|
+
function renderBubbleChart(files,depGraph,scanMode){
|
|
1389
1433
|
var container=document.getElementById('metrics-bubble');
|
|
1390
1434
|
if(!files||!files.length){container.innerHTML='<p style="color:var(--muted);padding:1rem">No files to analyze</p>';return;}
|
|
1435
|
+
depGraph=depGraph||{};
|
|
1436
|
+
scanMode=scanMode||'project';
|
|
1391
1437
|
var W=container.offsetWidth||900,H=Math.max(450,Math.min(650,W*0.45));
|
|
1392
1438
|
var maxLoc=Math.max.apply(null,files.map(function(f){return f.loc}))||1;
|
|
1439
|
+
var maxDeps=Math.max.apply(null,files.map(function(f){return f.dep_count||0}))||1;
|
|
1440
|
+
var maxCC=Math.max.apply(null,files.map(function(f){return f.complexity||0}))||1;
|
|
1393
1441
|
var minR=14,maxR=Math.min(70,W/10);
|
|
1394
|
-
|
|
1442
|
+
function healthColor(f){
|
|
1443
|
+
var cc=Math.min((f.avg_complexity||0)/10,1);
|
|
1444
|
+
var untested=f.has_tests?0:1;
|
|
1445
|
+
var deps=Math.min((f.dep_count||0)/5,1);
|
|
1446
|
+
var score=cc*0.4+untested*0.4+deps*0.2;
|
|
1447
|
+
score=Math.max(0,Math.min(1,score));
|
|
1448
|
+
var hue=Math.round(120*(1-score));
|
|
1449
|
+
var sat=Math.round(70+score*30);
|
|
1450
|
+
var lit=Math.round(42+18*(1-score));
|
|
1451
|
+
return 'hsl('+hue+','+sat+'%,'+lit+'%)';
|
|
1452
|
+
}
|
|
1453
|
+
var pathIdx={};
|
|
1454
|
+
files.forEach(function(f,i){pathIdx[f.path]=i;});
|
|
1455
|
+
function sizeScore(f){return (f.loc/maxLoc)*0.4+((f.avg_complexity||0)/10)*0.4+((f.dep_count||0)/maxDeps)*0.2;}
|
|
1456
|
+
var sorted=files.slice().sort(function(a,b){return sizeScore(a)-sizeScore(b)});
|
|
1395
1457
|
var cx=W/2,cy=H/2;
|
|
1396
1458
|
var bubbles=[];
|
|
1397
1459
|
var angle=0,spiralR=0;
|
|
1398
1460
|
for(var i=0;i<sorted.length;i++){
|
|
1399
1461
|
var f=sorted[i];
|
|
1400
|
-
var r=minR+Math.sqrt(f
|
|
1401
|
-
var color=
|
|
1462
|
+
var r=minR+Math.sqrt(sizeScore(f))*(maxR-minR);
|
|
1463
|
+
var color=healthColor(f);
|
|
1402
1464
|
var placed=false;
|
|
1403
1465
|
for(var attempt=0;attempt<800;attempt++){
|
|
1404
1466
|
var px=cx+spiralR*Math.cos(angle);
|
|
@@ -1409,79 +1471,217 @@ function renderBubbleChart(files){
|
|
|
1409
1471
|
if(Math.sqrt(dx*dx+dy*dy)<r+bubbles[j].r+2){collides=true;break;}
|
|
1410
1472
|
}
|
|
1411
1473
|
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
|
|
1474
|
+
bubbles.push({x:px,y:py,vx:0,vy:0,r:r,color:color,f:f});
|
|
1413
1475
|
placed=true;break;
|
|
1414
1476
|
}
|
|
1415
1477
|
angle+=0.2;spiralR+=0.04;
|
|
1416
1478
|
}
|
|
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
|
|
1479
|
+
if(!placed){bubbles.push({x:cx+(Math.random()-0.5)*W*0.3,y:cy+(Math.random()-0.5)*H*0.3,vx:0,vy:0,r:r,color:color,f:f});}
|
|
1418
1480
|
}
|
|
1481
|
+
var edges=[];
|
|
1482
|
+
function basename(p){var n=p.split('/').pop();var d=n.lastIndexOf('.');return(d>0?n.substring(0,d):n).toLowerCase();}
|
|
1483
|
+
var nameIdx={};
|
|
1484
|
+
bubbles.forEach(function(b,i){nameIdx[basename(b.f.path)]=i;});
|
|
1485
|
+
Object.keys(depGraph).forEach(function(src){
|
|
1486
|
+
var srcIdx=null;
|
|
1487
|
+
bubbles.forEach(function(b,i){if(b.f.path===src)srcIdx=i;});
|
|
1488
|
+
if(srcIdx===null)return;
|
|
1489
|
+
(depGraph[src]||[]).forEach(function(tgt){
|
|
1490
|
+
var tgtName=tgt.split('.').pop().toLowerCase();
|
|
1491
|
+
var tgtIdx=nameIdx[tgtName];
|
|
1492
|
+
if(tgtIdx!==undefined&&srcIdx!==tgtIdx)edges.push([srcIdx,tgtIdx]);
|
|
1493
|
+
});
|
|
1494
|
+
});
|
|
1419
1495
|
var canvas=document.createElement('canvas');
|
|
1420
1496
|
canvas.width=W;canvas.height=H;
|
|
1421
1497
|
canvas.style.cssText='display:block;border:1px solid var(--border);border-radius:8px;cursor:pointer;background:#0f172a';
|
|
1422
|
-
|
|
1498
|
+
var modeLabel=scanMode==='framework'?'<span style="color:#cba6f7;font-weight:600"> (Framework)</span> Add code to src/ to see your project':'';
|
|
1499
|
+
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'+modeLabel+'</h3><span style="font-size:0.7rem;color:var(--muted)">Drag bubbles | Click to drill down | Size=LOC | Colour=health | T=tested | D=deps</span></div>';
|
|
1423
1500
|
container.appendChild(canvas);
|
|
1424
1501
|
var ctx=canvas.getContext('2d');
|
|
1425
|
-
var hoveredIdx=-1;
|
|
1426
|
-
|
|
1502
|
+
var hoveredIdx=-1,dragIdx=-1,dragOX=0,dragOY=0;
|
|
1503
|
+
function simulate(){
|
|
1504
|
+
var damping=0.65,springK=0.002,repulse=40,gravity=0.008;
|
|
1505
|
+
var cx=W/2,cy=H/2;
|
|
1506
|
+
bubbles.forEach(function(b,idx){
|
|
1507
|
+
if(idx===dragIdx)return;
|
|
1508
|
+
var dx=cx-b.x,dy=cy-b.y;
|
|
1509
|
+
var sizeFactor=0.3+(b.r/maxR)*0.7;
|
|
1510
|
+
var pull=gravity*sizeFactor*sizeFactor;
|
|
1511
|
+
b.vx+=dx*pull;b.vy+=dy*pull;
|
|
1512
|
+
});
|
|
1513
|
+
edges.forEach(function(e){
|
|
1514
|
+
var a=bubbles[e[0]],b=bubbles[e[1]];
|
|
1515
|
+
var dx=b.x-a.x,dy=b.y-a.y;
|
|
1516
|
+
var dist=Math.sqrt(dx*dx+dy*dy)||1;
|
|
1517
|
+
var rest=a.r+b.r+20;
|
|
1518
|
+
var force=(dist-rest)*springK;
|
|
1519
|
+
var fx=dx/dist*force,fy=dy/dist*force;
|
|
1520
|
+
if(e[0]!==dragIdx){a.vx+=fx;a.vy+=fy;}
|
|
1521
|
+
if(e[1]!==dragIdx){b.vx-=fx;b.vy-=fy;}
|
|
1522
|
+
});
|
|
1523
|
+
for(var i=0;i<bubbles.length;i++){
|
|
1524
|
+
for(var j=i+1;j<bubbles.length;j++){
|
|
1525
|
+
var a=bubbles[i],b=bubbles[j];
|
|
1526
|
+
var dx=b.x-a.x,dy=b.y-a.y;
|
|
1527
|
+
var dist=Math.sqrt(dx*dx+dy*dy)||1;
|
|
1528
|
+
var minDist=a.r+b.r+20;
|
|
1529
|
+
if(dist<minDist){
|
|
1530
|
+
var force=repulse*(minDist-dist)/minDist;
|
|
1531
|
+
var fx=dx/dist*force,fy=dy/dist*force;
|
|
1532
|
+
if(i!==dragIdx){a.vx-=fx;a.vy-=fy;}
|
|
1533
|
+
if(j!==dragIdx){b.vx+=fx;b.vy+=fy;}
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
bubbles.forEach(function(b,idx){
|
|
1538
|
+
if(idx===dragIdx)return;
|
|
1539
|
+
b.vx*=damping;b.vy*=damping;
|
|
1540
|
+
var maxV=2;
|
|
1541
|
+
if(b.vx>maxV)b.vx=maxV;if(b.vx<-maxV)b.vx=-maxV;
|
|
1542
|
+
if(b.vy>maxV)b.vy=maxV;if(b.vy<-maxV)b.vy=-maxV;
|
|
1543
|
+
b.x+=b.vx;b.y+=b.vy;
|
|
1544
|
+
b.x=Math.max(b.r+2,Math.min(W-b.r-2,b.x));
|
|
1545
|
+
b.y=Math.max(b.r+25,Math.min(H-b.r-2,b.y));
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1427
1548
|
function draw(){
|
|
1428
|
-
|
|
1549
|
+
simulate();
|
|
1429
1550
|
ctx.clearRect(0,0,W,H);
|
|
1430
|
-
ctx.
|
|
1431
|
-
|
|
1432
|
-
for(var
|
|
1551
|
+
ctx.save();ctx.translate(panX,panY);ctx.scale(zoom,zoom);
|
|
1552
|
+
ctx.strokeStyle='rgba(255,255,255,0.03)';ctx.lineWidth=1/zoom;
|
|
1553
|
+
for(var gx=0;gx<W/zoom;gx+=50){ctx.beginPath();ctx.moveTo(gx,0);ctx.lineTo(gx,H/zoom);ctx.stroke();}
|
|
1554
|
+
for(var gy=0;gy<H/zoom;gy+=50){ctx.beginPath();ctx.moveTo(0,gy);ctx.lineTo(W/zoom,gy);ctx.stroke();}
|
|
1555
|
+
edges.forEach(function(e){
|
|
1556
|
+
var a=bubbles[e[0]],b=bubbles[e[1]];
|
|
1557
|
+
var dx=b.x-a.x,dy=b.y-a.y;
|
|
1558
|
+
var dist=Math.sqrt(dx*dx+dy*dy)||1;
|
|
1559
|
+
var highlighted=(hoveredIdx===e[0]||hoveredIdx===e[1]);
|
|
1560
|
+
ctx.beginPath();
|
|
1561
|
+
ctx.moveTo(a.x+dx/dist*a.r,a.y+dy/dist*a.r);
|
|
1562
|
+
var ex=b.x-dx/dist*b.r,ey=b.y-dy/dist*b.r;
|
|
1563
|
+
ctx.lineTo(ex,ey);
|
|
1564
|
+
ctx.strokeStyle=highlighted?'rgba(139,180,250,0.9)':'rgba(255,255,255,0.3)';
|
|
1565
|
+
ctx.lineWidth=highlighted?3:1.5;ctx.stroke();
|
|
1566
|
+
var aLen=highlighted?14:8;
|
|
1567
|
+
var aAngle=Math.atan2(dy,dx);
|
|
1568
|
+
ctx.beginPath();
|
|
1569
|
+
ctx.moveTo(ex,ey);
|
|
1570
|
+
ctx.lineTo(ex-aLen*Math.cos(aAngle-0.4),ey-aLen*Math.sin(aAngle-0.4));
|
|
1571
|
+
ctx.lineTo(ex-aLen*Math.cos(aAngle+0.4),ey-aLen*Math.sin(aAngle+0.4));
|
|
1572
|
+
ctx.closePath();ctx.fillStyle=ctx.strokeStyle;ctx.fill();
|
|
1573
|
+
});
|
|
1433
1574
|
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
1575
|
var isHovered=(idx===hoveredIdx);
|
|
1438
1576
|
var drawR=isHovered?b.r+4:b.r;
|
|
1439
|
-
if(isHovered){
|
|
1440
|
-
|
|
1441
|
-
|
|
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();
|
|
1577
|
+
if(isHovered){ctx.beginPath();ctx.arc(b.x,b.y,drawR+8,0,Math.PI*2);ctx.fillStyle='rgba(255,255,255,0.08)';ctx.fill();}
|
|
1578
|
+
ctx.beginPath();ctx.arc(b.x,b.y,drawR,0,Math.PI*2);
|
|
1579
|
+
ctx.fillStyle=b.color;ctx.globalAlpha=isHovered?1.0:0.85;ctx.fill();
|
|
1580
|
+
ctx.globalAlpha=1;ctx.strokeStyle=isHovered?'rgba(255,255,255,0.6)':'rgba(255,255,255,0.25)';ctx.lineWidth=isHovered?2.5:1.5;ctx.stroke();
|
|
1446
1581
|
var name=b.f.path.split('/').pop().replace('.rb','');
|
|
1447
1582
|
if(drawR>16){
|
|
1448
1583
|
var fs=Math.max(8,Math.min(13,drawR*0.38));
|
|
1449
1584
|
ctx.fillStyle='#fff';ctx.font='600 '+fs+'px monospace';ctx.textAlign='center';
|
|
1450
|
-
ctx.fillText(name,
|
|
1585
|
+
ctx.fillText(name,b.x,b.y-2);
|
|
1451
1586
|
ctx.fillStyle='rgba(255,255,255,0.65)';ctx.font=(fs-1)+'px monospace';
|
|
1452
|
-
ctx.fillText(b.f.loc+' LOC',
|
|
1587
|
+
ctx.fillText(b.f.loc+' LOC',b.x,b.y+fs);
|
|
1453
1588
|
if(isHovered&&drawR>25){
|
|
1454
1589
|
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,
|
|
1590
|
+
ctx.fillText('CC:'+b.f.complexity+' MI:'+b.f.maintainability,b.x,b.y+fs*2);
|
|
1456
1591
|
}
|
|
1457
1592
|
}
|
|
1458
|
-
|
|
1593
|
+
var mfs=Math.max(9,drawR*0.3);
|
|
1594
|
+
var mrad=mfs*0.7;
|
|
1595
|
+
var mpad=mrad*2.4;
|
|
1596
|
+
var my=b.y-drawR+mrad+3;
|
|
1597
|
+
if(drawR>14&&b.f.has_tests){
|
|
1598
|
+
var mx=b.x-(b.f.dep_count>0?mpad*0.5:0);
|
|
1599
|
+
ctx.beginPath();ctx.arc(mx,my,mrad,0,Math.PI*2);
|
|
1600
|
+
ctx.fillStyle='#16a34a';ctx.fill();
|
|
1601
|
+
ctx.fillStyle='#fff';ctx.font='bold '+mfs+'px sans-serif';ctx.textAlign='center';
|
|
1602
|
+
ctx.fillText('T',mx,my+mfs*0.35);
|
|
1603
|
+
}
|
|
1604
|
+
if(drawR>14&&b.f.dep_count>0){
|
|
1605
|
+
var mx2=b.x+(b.f.has_tests?mpad*0.5:0);
|
|
1606
|
+
ctx.beginPath();ctx.arc(mx2,my,mrad,0,Math.PI*2);
|
|
1607
|
+
ctx.fillStyle='#ea580c';ctx.fill();
|
|
1608
|
+
ctx.fillStyle='#fff';ctx.font='bold '+mfs+'px sans-serif';ctx.textAlign='center';
|
|
1609
|
+
ctx.fillText('D',mx2,my+mfs*0.35);
|
|
1610
|
+
}
|
|
1611
|
+
b._drawX=b.x;b._drawY=b.y;b._drawR=drawR;
|
|
1459
1612
|
});
|
|
1460
|
-
var totalLoc=0,totalFiles=bubbles.length;
|
|
1461
|
-
bubbles.forEach(function(b){totalLoc+=b.f.loc});
|
|
1613
|
+
var totalLoc=0,totalFiles=bubbles.length,testedCount=0;
|
|
1614
|
+
bubbles.forEach(function(b){totalLoc+=b.f.loc;if(b.f.has_tests)testedCount++;});
|
|
1462
1615
|
var avgMI=bubbles.reduce(function(s,b){return s+b.f.maintainability},0)/totalFiles;
|
|
1463
1616
|
ctx.fillStyle='rgba(255,255,255,0.35)';ctx.font='11px monospace';ctx.textAlign='right';
|
|
1464
|
-
ctx.
|
|
1617
|
+
ctx.restore();
|
|
1618
|
+
ctx.fillStyle='rgba(255,255,255,0.35)';ctx.font='11px monospace';ctx.textAlign='right';
|
|
1619
|
+
ctx.fillText(totalFiles+' files | '+totalLoc.toLocaleString()+' LOC | MI:'+avgMI.toFixed(1)+' | Tested:'+testedCount+'/'+totalFiles,W-12,H-10);
|
|
1465
1620
|
window._metricsAnimFrame=requestAnimationFrame(draw);
|
|
1466
1621
|
}
|
|
1467
1622
|
draw();
|
|
1623
|
+
var panning=false,panStartX=0,panStartY=0;
|
|
1624
|
+
canvas.addEventListener('contextmenu',function(e){e.preventDefault();});
|
|
1468
1625
|
canvas.addEventListener('mousemove',function(e){
|
|
1469
1626
|
var rect=canvas.getBoundingClientRect();
|
|
1470
1627
|
var mx=e.clientX-rect.left,my=e.clientY-rect.top;
|
|
1628
|
+
if(panning){
|
|
1629
|
+
panX+=(mx-panStartX);panY+=(my-panStartY);
|
|
1630
|
+
panStartX=mx;panStartY=my;return;
|
|
1631
|
+
}
|
|
1632
|
+
if(dragIdx>=0){
|
|
1633
|
+
var wmx=(mx-panX)/zoom,wmy=(my-panY)/zoom;
|
|
1634
|
+
bubbles[dragIdx].x=wmx-dragOX;bubbles[dragIdx].y=wmy-dragOY;
|
|
1635
|
+
bubbles[dragIdx].vx=0;bubbles[dragIdx].vy=0;return;
|
|
1636
|
+
}
|
|
1637
|
+
var wmx2=(mx-panX)/zoom,wmy2=(my-panY)/zoom;
|
|
1471
1638
|
hoveredIdx=-1;
|
|
1472
1639
|
for(var i=bubbles.length-1;i>=0;i--){
|
|
1473
1640
|
var b=bubbles[i];
|
|
1474
|
-
var dx=
|
|
1475
|
-
if(Math.sqrt(dx*dx+dy*dy)<=b.
|
|
1641
|
+
var dx=wmx2-b.x,dy=wmy2-b.y;
|
|
1642
|
+
if(Math.sqrt(dx*dx+dy*dy)<=b.r){hoveredIdx=i;break;}
|
|
1476
1643
|
}
|
|
1477
|
-
canvas.style.cursor=hoveredIdx>=0?'
|
|
1644
|
+
canvas.style.cursor=panning?'move':hoveredIdx>=0?'grab':'default';
|
|
1478
1645
|
});
|
|
1479
|
-
canvas.addEventListener('
|
|
1480
|
-
|
|
1646
|
+
canvas.addEventListener('mousedown',function(e){
|
|
1647
|
+
var rect=canvas.getBoundingClientRect();
|
|
1648
|
+
var mx=e.clientX-rect.left,my=e.clientY-rect.top;
|
|
1649
|
+
if(e.button===2){
|
|
1650
|
+
panning=true;panStartX=mx;panStartY=my;
|
|
1651
|
+
canvas.style.cursor='move';return;
|
|
1652
|
+
}
|
|
1653
|
+
if(hoveredIdx>=0){
|
|
1654
|
+
dragIdx=hoveredIdx;
|
|
1655
|
+
var wmx=(mx-panX)/zoom;
|
|
1656
|
+
var wmy=(my-panY)/zoom;
|
|
1657
|
+
dragOX=wmx-bubbles[dragIdx].x;
|
|
1658
|
+
dragOY=wmy-bubbles[dragIdx].y;
|
|
1659
|
+
canvas.style.cursor='grabbing';
|
|
1660
|
+
}
|
|
1661
|
+
});
|
|
1662
|
+
canvas.addEventListener('mouseup',function(){
|
|
1663
|
+
if(panning){panning=false;canvas.style.cursor='default';}
|
|
1664
|
+
if(dragIdx>=0){canvas.style.cursor='grab';dragIdx=-1;}
|
|
1665
|
+
});
|
|
1666
|
+
canvas.addEventListener('mouseleave',function(){hoveredIdx=-1;dragIdx=-1;panning=false;});
|
|
1667
|
+
canvas.addEventListener('dblclick',function(e){
|
|
1481
1668
|
if(hoveredIdx<0)return;
|
|
1482
|
-
|
|
1483
|
-
drillDownFile(f.path);
|
|
1669
|
+
drillDownFile(bubbles[hoveredIdx].f.path);
|
|
1484
1670
|
});
|
|
1671
|
+
var zoom=1.0,panX=0,panY=0;
|
|
1672
|
+
canvas.addEventListener('wheel',function(e){
|
|
1673
|
+
e.preventDefault();
|
|
1674
|
+
var rect=canvas.getBoundingClientRect();
|
|
1675
|
+
var mx=(e.clientX-rect.left-panX)/zoom;
|
|
1676
|
+
var my=(e.clientY-rect.top-panY)/zoom;
|
|
1677
|
+
var oldZoom=zoom;
|
|
1678
|
+
zoom*=e.deltaY<0?1.08:0.93;
|
|
1679
|
+
zoom=Math.max(0.5,Math.min(2.5,zoom));
|
|
1680
|
+
panX+=(mx*oldZoom-mx*zoom);
|
|
1681
|
+
panY+=(my*oldZoom-my*zoom);
|
|
1682
|
+
bubbles.forEach(function(b){});
|
|
1683
|
+
},{passive:false});
|
|
1684
|
+
bubbles.forEach(function(b){b._baseR=b.r;});
|
|
1485
1685
|
}
|
|
1486
1686
|
function drillDownFile(path){
|
|
1487
1687
|
var dd=document.getElementById('metrics-drilldown');
|
|
@@ -1560,7 +1760,7 @@ function loadAllMetrics(){
|
|
|
1560
1760
|
fetch('/__dev/api/metrics/full').then(function(r){return r.json()}).then(function(d){
|
|
1561
1761
|
_metricsFullData=d;
|
|
1562
1762
|
if(d.error){document.getElementById('metrics-bubble').innerHTML='<p style="color:var(--danger);padding:1rem">'+d.error+'</p>';return;}
|
|
1563
|
-
renderBubbleChart(d.file_metrics);
|
|
1763
|
+
renderBubbleChart(d.file_metrics,d.dependency_graph,d.scan_mode);
|
|
1564
1764
|
var hm=document.getElementById('metrics-heatmap');
|
|
1565
1765
|
var rows=d.file_metrics.map(function(f){
|
|
1566
1766
|
var color=miColor(f.maintainability);
|
data/lib/tina4/events.rb
CHANGED
|
@@ -81,6 +81,25 @@ module Tina4
|
|
|
81
81
|
@listeners.keys
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
+
# Fire an event asynchronously. Each listener runs in its own thread.
|
|
85
|
+
# Errors in listeners are silently caught.
|
|
86
|
+
#
|
|
87
|
+
# Tina4::Events.emit_async("user.created", user_data)
|
|
88
|
+
#
|
|
89
|
+
def emit_async(event, *args)
|
|
90
|
+
return unless @listeners&.key?(event)
|
|
91
|
+
|
|
92
|
+
@listeners[event].sort_by { |l| -(l[:priority] || 0) }.each do |listener|
|
|
93
|
+
Thread.new do
|
|
94
|
+
begin
|
|
95
|
+
listener[:callback].call(*args)
|
|
96
|
+
rescue => e
|
|
97
|
+
# Async emit silently catches errors
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
84
103
|
# Remove all listeners for all events.
|
|
85
104
|
def clear
|
|
86
105
|
@listeners.clear
|
data/lib/tina4/field_types.rb
CHANGED
|
@@ -19,7 +19,12 @@ module Tina4
|
|
|
19
19
|
if name
|
|
20
20
|
@table_name = name
|
|
21
21
|
else
|
|
22
|
-
|
|
22
|
+
base = self.name.split("::").last.downcase
|
|
23
|
+
# Pluralize by default (add "s") unless ORM_PLURAL_TABLE_NAMES is explicitly disabled
|
|
24
|
+
unless ENV.fetch("ORM_PLURAL_TABLE_NAMES", "").match?(/\A(false|0|no)\z/i)
|
|
25
|
+
base += "s" unless base.end_with?("s")
|
|
26
|
+
end
|
|
27
|
+
@table_name || base
|
|
23
28
|
end
|
|
24
29
|
end
|
|
25
30
|
|