tina4ruby 3.11.5 → 3.11.8

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.
@@ -461,6 +461,17 @@ module Tina4
461
461
  when ["GET", "/__dev/api/metrics/file"]
462
462
  file_path = (query_param(env, "path") || "").to_s
463
463
  json_response(Tina4::Metrics.file_detail(file_path))
464
+ when ["GET", "/__dev/api/graphql/schema"]
465
+ begin
466
+ gql = Tina4::GraphQL.new
467
+ # Auto-discover and register all ORM subclasses
468
+ ObjectSpace.each_object(Class).select { |c| c < Tina4::ORM }.each do |model_class|
469
+ gql.from_orm(model_class.new)
470
+ end
471
+ json_response({ schema: gql.introspect, sdl: gql.schema_sdl })
472
+ rescue => e
473
+ json_response({ error: e.message }, 400)
474
+ end
464
475
  else
465
476
  nil
466
477
  end
@@ -555,12 +566,25 @@ module Tina4
555
566
  routes = Tina4::Router.routes
556
567
  .reject { |route| internal_prefixes.any? { |prefix| route.path.start_with?(prefix) } }
557
568
  .map do |route|
569
+ handler_name = ""
570
+ mod = ""
571
+ if route.handler.is_a?(Proc)
572
+ source = route.handler.source_location
573
+ if source
574
+ handler_name = "#{File.basename(source[0])}:#{source[1]}"
575
+ mod = File.dirname(source[0])
576
+ end
577
+ end
558
578
  {
559
579
  method: route.method,
560
580
  pattern: route.path,
581
+ path: route.path,
561
582
  middleware: route.respond_to?(:middleware_count) ? route.middleware_count : 0,
562
583
  cache: route.respond_to?(:cached?) ? route.cached? : false,
563
- secure: !route.auth_handler.nil?
584
+ secure: !route.auth_handler.nil?,
585
+ auth_required: !route.auth_handler.nil?,
586
+ handler: handler_name,
587
+ module: mod
564
588
  }
565
589
  end
566
590
  { routes: routes, count: routes.size }
@@ -873,976 +897,6 @@ module Tina4
873
897
 
874
898
  { deployed: name, files: copied }
875
899
  end
876
-
877
- def render_dashboard
878
- <<~'HTML'
879
- <!DOCTYPE html>
880
- <html lang="en">
881
- <head>
882
- <meta charset="UTF-8">
883
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
884
- <title>Tina4 Dev Admin</title>
885
- <style>
886
- :root {
887
- --bg: #0f172a; --surface: #1e293b; --border: #334155;
888
- --text: #e2e8f0; --muted: #94a3b8; --primary: #c62828;
889
- --success: #22c55e; --danger: #ef4444; --warn: #f59e0b;
890
- --info: #06b6d4; --radius: 0.5rem;
891
- --mono: 'SF Mono', 'Fira Code', 'Consolas', monospace;
892
- --font: system-ui, -apple-system, sans-serif;
893
- }
894
- * { box-sizing: border-box; margin: 0; padding: 0; }
895
-
896
- body { font-family: var(--font); background: var(--bg); color: var(--text); font-size: 0.875rem; }
897
- .dev-header {
898
- background: var(--surface); border-bottom: 1px solid var(--border);
899
- padding: 0.75rem 1.5rem; display: flex; align-items: center; gap: 1rem;
900
- position: sticky; top: 0; z-index: 100;
901
- }
902
- .dev-header h1 { font-size: 1rem; font-weight: 600; }
903
- .dev-header .badge {
904
- background: var(--primary); color: #fff; padding: 0.15rem 0.5rem;
905
- border-radius: 1rem; font-size: 0.7rem; font-weight: 600;
906
- }
907
- .dev-tabs {
908
- display: flex; gap: 0; background: var(--surface);
909
- border-bottom: 1px solid var(--border); overflow-x: auto;
910
- position: sticky; top: 2.75rem; z-index: 100;
911
- }
912
- .dev-tab {
913
- padding: 0.6rem 1rem; cursor: pointer; font-size: 0.8rem;
914
- border-bottom: 2px solid transparent; color: var(--muted);
915
- transition: all 0.15s; background: none; border-top: none;
916
- border-left: none; border-right: none; white-space: nowrap;
917
- }
918
- .dev-tab:hover { color: var(--text); }
919
- .dev-tab.active { color: var(--primary); border-bottom-color: var(--primary); }
920
- .dev-tab .count {
921
- background: var(--border); color: var(--muted); padding: 0.1rem 0.4rem;
922
- border-radius: 0.75rem; font-size: 0.65rem; margin-left: 0.25rem;
923
- }
924
- .dev-content { padding: 0.25rem; }
925
- .dev-panel {
926
- background: var(--surface); border: 1px solid var(--border);
927
- border-radius: var(--radius); overflow: visible;
928
- }
929
- .dev-panel-header {
930
- padding: 0.75rem 1rem; border-bottom: 1px solid var(--border);
931
- display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;
932
- }
933
- .dev-panel-header h2 { font-size: 0.9rem; font-weight: 600; }
934
- table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
935
- th { text-align: left; padding: 0.5rem 0.75rem; color: var(--muted); font-weight: 500; border-bottom: 1px solid var(--border); }
936
- td { padding: 0.4rem 0.75rem; border-bottom: 1px solid var(--border); }
937
- tr:hover { background: rgba(198, 40, 40, 0.05); }
938
- .method { font-family: var(--mono); font-size: 0.7rem; font-weight: 700; }
939
- .method-get { color: var(--success); }
940
- .method-post { color: var(--primary); }
941
- .method-put { color: var(--warn); }
942
- .method-delete { color: var(--danger); }
943
- .path { font-family: var(--mono); font-size: 0.75rem; }
944
- .badge-pill {
945
- display: inline-block; padding: 0.1rem 0.5rem; border-radius: 1rem;
946
- font-size: 0.65rem; font-weight: 600; text-transform: uppercase;
947
- }
948
- .bg-pending { background: rgba(245,158,11,0.15); color: var(--warn); }
949
- .bg-completed, .bg-success { background: rgba(34,197,94,0.15); color: var(--success); }
950
- .bg-failed, .bg-danger { background: rgba(239,68,68,0.15); color: var(--danger); }
951
- .bg-reserved, .bg-primary { background: rgba(198,40,40,0.15); color: var(--primary); }
952
- .bg-info { background: rgba(6,182,212,0.15); color: var(--info); }
953
- .btn {
954
- padding: 0.3rem 0.65rem; border: 1px solid var(--border); border-radius: var(--radius);
955
- background: var(--surface); color: var(--text); cursor: pointer; font-size: 0.75rem;
956
- transition: all 0.15s;
957
- }
958
- .btn:hover { border-color: var(--primary); color: var(--primary); }
959
- .btn-primary { background: var(--primary); color: #fff; border-color: var(--primary); }
960
- .btn-primary:hover { background: #d32f2f; }
961
- .btn-danger { border-color: var(--danger); color: var(--danger); }
962
- .btn-danger:hover { background: rgba(239,68,68,0.1); }
963
- .btn-success { border-color: var(--success); color: var(--success); }
964
- .btn-sm { padding: 0.2rem 0.5rem; font-size: 0.7rem; }
965
- .empty { padding: 2rem; text-align: center; color: var(--muted); }
966
- .input {
967
- background: var(--bg); color: var(--text); border: 1px solid var(--border);
968
- border-radius: var(--radius); padding: 0.35rem 0.5rem; font-size: 0.8rem;
969
- font-family: var(--font);
970
- }
971
- .input:focus { outline: none; border-color: var(--primary); }
972
- .input-mono { font-family: var(--mono); }
973
- select.input { padding: 0.3rem; }
974
- textarea.input { resize: vertical; font-family: var(--mono); }
975
- .flex { display: flex; }
976
- .gap-sm { gap: 0.5rem; }
977
- .gap-md { gap: 1rem; }
978
- .items-center { align-items: center; }
979
- .justify-between { justify-content: space-between; }
980
- .flex-1 { flex: 1; }
981
- .p-sm { padding: 0.5rem; }
982
- .p-md { padding: 1rem; }
983
- .mb-sm { margin-bottom: 0.5rem; }
984
- .text-sm { font-size: 0.75rem; }
985
- .text-muted { color: var(--muted); }
986
- .text-mono { font-family: var(--mono); }
987
- .mail-item { padding: 0.6rem 0.75rem; border-bottom: 1px solid var(--border); cursor: pointer; }
988
- .mail-item:hover { background: rgba(198,40,40,0.05); }
989
- .mail-item.unread { border-left: 3px solid var(--primary); }
990
- .msg-entry { padding: 0.4rem 0.75rem; border-bottom: 1px solid var(--border); font-size: 0.75rem; }
991
- .msg-entry .cat {
992
- font-family: var(--mono); font-size: 0.65rem; padding: 0.1rem 0.35rem;
993
- border-radius: 0.25rem; background: rgba(198,40,40,0.15); color: var(--primary);
994
- }
995
- .msg-entry .time { color: var(--muted); font-size: 0.7rem; font-family: var(--mono); }
996
- .level-error { color: var(--danger); }
997
- .level-warn { color: var(--warn); }
998
- .toolbar { display: flex; gap: 0.5rem; padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--border); flex-wrap: wrap; align-items: center; }
999
- .hidden { display: none; }
1000
- /* Chat panel */
1001
- .chat-container { display: flex; flex-direction: column; height: 500px; }
1002
- .chat-messages { flex: 1; overflow-y: auto; padding: 0.75rem; }
1003
- .chat-msg { margin-bottom: 0.75rem; padding: 0.5rem 0.75rem; border-radius: var(--radius); font-size: 0.8rem; max-width: 85%; }
1004
- .chat-user { background: var(--primary); color: #fff; margin-left: auto; }
1005
- .chat-bot { background: var(--bg); border: 1px solid var(--border); }
1006
- .chat-input-row { display: flex; gap: 0.5rem; padding: 0.75rem; border-top: 1px solid var(--border); }
1007
- .chat-input-row input { flex: 1; }
1008
- /* System cards */
1009
- .sys-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.75rem; padding: 1rem; }
1010
- .sys-card { background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 0.75rem; }
1011
- .sys-card .label { font-size: 0.7rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; }
1012
- .sys-card .value { font-size: 1.25rem; font-weight: 600; margin-top: 0.25rem; }
1013
- /* Request table */
1014
- .status-ok { color: var(--success); }
1015
- .status-err { color: var(--danger); }
1016
- .status-warn { color: var(--warn); }
1017
- /* Filter buttons */
1018
- .filter-btn { cursor: pointer; }
1019
- .filter-btn.active { border-color: var(--primary); color: var(--primary); }
1020
- code, .mono { font-family: var(--mono); font-size: 0.82rem; }
1021
- </style>
1022
- </head>
1023
- <body>
1024
-
1025
- <div class="dev-header">
1026
- <img src="/images/logo.svg" style="width:1.5rem;height:1.5rem;cursor:pointer;opacity:0.7;transition:opacity 0.15s" title="Back to app" onclick="exitDevAdmin()" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.7'" alt="Tina4">
1027
- <h1>Tina4 Dev Admin</h1>
1028
- <span class="badge">DEV</span>
1029
- <span style="margin-left:auto; font-size:0.75rem; color:var(--muted)" id="timestamp"></span>
1030
- </div>
1031
-
1032
- <div class="dev-tabs">
1033
- <button class="dev-tab active" onclick="showTab('routes', event)">Routes <span class="count" id="routes-count">0</span></button>
1034
- <button class="dev-tab" onclick="showTab('queue', event)">Queue <span class="count" id="queue-count">0</span></button>
1035
- <button class="dev-tab" onclick="showTab('mailbox', event)">Mailbox <span class="count" id="mailbox-count">0</span></button>
1036
- <button class="dev-tab" onclick="showTab('messages', event)">Messages <span class="count" id="messages-count">0</span></button>
1037
- <button class="dev-tab" onclick="showTab('database', event)">Database <span class="count" id="db-count">0</span></button>
1038
- <button class="dev-tab" onclick="showTab('requests', event)">Requests <span class="count" id="req-count">0</span></button>
1039
- <button class="dev-tab" onclick="showTab('errors', event)">Errors <span class="count" id="err-count">0</span></button>
1040
- <button class="dev-tab" onclick="showTab('websockets', event)">WS <span class="count" id="ws-count">0</span></button>
1041
- <button class="dev-tab" onclick="showTab('system', event)">System</button>
1042
- <button class="dev-tab" onclick="showTab('tools', event)">Tools</button>
1043
- <button class="dev-tab" onclick="showTab('connections', event)">Connections</button>
1044
- <button class="dev-tab" onclick="showTab('metrics', event)">Metrics</button>
1045
- <button class="dev-tab" onclick="showTab('chat', event)">Tina4</button>
1046
- </div>
1047
-
1048
- <div class="dev-content">
1049
-
1050
- <!-- Routes Panel -->
1051
- <div id="panel-routes" class="dev-panel">
1052
- <div class="dev-panel-header">
1053
- <h2>Registered Routes</h2>
1054
- <button class="btn btn-sm" onclick="loadRoutes()">Refresh</button>
1055
- </div>
1056
- <table>
1057
- <thead><tr><th>Method</th><th>Path</th><th>Auth</th><th>Handler</th></tr></thead>
1058
- <tbody id="routes-body"></tbody>
1059
- </table>
1060
- </div>
1061
-
1062
- <!-- Queue Panel -->
1063
- <div id="panel-queue" class="dev-panel hidden">
1064
- <div class="dev-panel-header">
1065
- <h2>Queue Jobs</h2>
1066
- <div class="flex gap-sm">
1067
- <button class="btn btn-sm" onclick="loadQueue()">Refresh</button>
1068
- <button class="btn btn-sm" onclick="retryQueue()">Retry Failed</button>
1069
- <button class="btn btn-sm btn-danger" onclick="purgeQueue()">Purge Done</button>
1070
- </div>
1071
- </div>
1072
- <div class="toolbar">
1073
- <button class="btn btn-sm filter-btn active" onclick="filterQueue('', event)">All</button>
1074
- <button class="btn btn-sm filter-btn" onclick="filterQueue('pending', event)">Pending <span id="q-pending">0</span></button>
1075
- <button class="btn btn-sm filter-btn" onclick="filterQueue('completed', event)">Done <span id="q-completed">0</span></button>
1076
- <button class="btn btn-sm filter-btn" onclick="filterQueue('failed', event)">Failed <span id="q-failed">0</span></button>
1077
- <button class="btn btn-sm filter-btn" onclick="filterQueue('reserved', event)">Active <span id="q-reserved">0</span></button>
1078
- </div>
1079
- <table>
1080
- <thead><tr><th>ID</th><th>Topic</th><th>Status</th><th>Attempts</th><th>Created</th><th>Data</th><th></th></tr></thead>
1081
- <tbody id="queue-body"></tbody>
1082
- </table>
1083
- <div id="queue-empty" class="empty hidden">No queue jobs</div>
1084
- </div>
1085
-
1086
- <!-- Mailbox Panel -->
1087
- <div id="panel-mailbox" class="dev-panel hidden">
1088
- <div class="dev-panel-header">
1089
- <h2>Dev Mailbox</h2>
1090
- <div class="flex gap-sm">
1091
- <button class="btn btn-sm" onclick="loadMailbox()">Refresh</button>
1092
- <button class="btn btn-sm btn-primary" onclick="seedMailbox()">Seed 5</button>
1093
- <button class="btn btn-sm btn-danger" onclick="clearMailbox()">Clear</button>
1094
- </div>
1095
- </div>
1096
- <div class="toolbar">
1097
- <button class="btn btn-sm filter-btn active" onclick="filterMailbox('', event)">All</button>
1098
- <button class="btn btn-sm filter-btn" onclick="filterMailbox('inbox', event)">Inbox</button>
1099
- <button class="btn btn-sm filter-btn" onclick="filterMailbox('outbox', event)">Outbox</button>
1100
- </div>
1101
- <div id="mailbox-list"></div>
1102
- <div id="mail-detail" class="hidden p-md"></div>
1103
- </div>
1104
-
1105
- <!-- Messages Panel -->
1106
- <div id="panel-messages" class="dev-panel hidden">
1107
- <div class="dev-panel-header">
1108
- <h2>Message Log</h2>
1109
- <div class="flex gap-sm items-center">
1110
- <input type="text" id="msg-search" class="input" placeholder="Search messages..." onkeydown="if(event.key==='Enter')searchMessages()">
1111
- <button class="btn btn-sm" onclick="searchMessages()">Search</button>
1112
- <button class="btn btn-sm" onclick="loadMessages()">All</button>
1113
- <button class="btn btn-sm btn-danger" onclick="clearMessages()">Clear</button>
1114
- </div>
1115
- </div>
1116
- <div id="messages-list"></div>
1117
- <div id="messages-empty" class="empty">No messages logged</div>
1118
- </div>
1119
-
1120
- <!-- Database Panel -->
1121
- <div id="panel-database" class="dev-panel hidden">
1122
- <div class="dev-panel-header">
1123
- <h2>Database</h2>
1124
- <button class="btn btn-sm" onclick="loadTables()">Refresh</button>
1125
- </div>
1126
- <div style="display:flex;height:calc(100vh - 140px);overflow:hidden">
1127
- <!-- Left: Tables navigation -->
1128
- <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">
1129
- <div class="text-sm text-muted" style="font-weight:600">Tables</div>
1130
- <div id="table-list" class="text-sm"></div>
1131
- <div style="border-top:1px solid var(--border);padding-top:0.5rem;margin-top:auto">
1132
- <div class="text-sm text-muted" style="font-weight:600;margin-bottom:0.25rem">Seed Data</div>
1133
- <select id="seed-table" class="input" style="width:100%;margin-bottom:0.25rem;font-size:0.75rem"><option value="">Pick table...</option></select>
1134
- <div class="flex gap-sm items-center">
1135
- <input type="number" id="seed-count" class="input" value="10" min="1" max="1000" style="width:60px;font-size:0.75rem">
1136
- <button class="btn btn-sm btn-success" onclick="seedTable()">Seed</button>
1137
- </div>
1138
- </div>
1139
- </div>
1140
- <!-- Right: Query + Results -->
1141
- <div style="flex:1;display:flex;flex-direction:column;overflow:hidden;padding:0.5rem">
1142
- <div style="display:flex;gap:0.5rem;align-items:center;margin-bottom:0.25rem">
1143
- <select id="query-type" class="input" style="width:auto;font-size:0.75rem">
1144
- <option value="sql">SQL</option>
1145
- </select>
1146
- <span class="text-sm text-muted">Limit</span>
1147
- <select id="query-limit" class="input" style="width:70px;font-size:0.75rem">
1148
- <option value="20">20</option>
1149
- <option value="50">50</option>
1150
- <option value="100">100</option>
1151
- <option value="500">500</option>
1152
- <option value="0">All</option>
1153
- </select>
1154
- <button class="btn btn-sm btn-primary" onclick="runQuery()">Run</button>
1155
- <button class="btn btn-sm" id="btn-csv" onclick="copyResults('csv',this)" title="Copy results as CSV">Copy CSV</button>
1156
- <button class="btn btn-sm" id="btn-json" onclick="copyResults('json',this)" title="Copy results as JSON">Copy JSON</button>
1157
- <button class="btn btn-sm" onclick="pasteData()" title="Paste tab-separated data as INSERTs">Paste</button>
1158
- <span class="text-sm text-muted">Ctrl+Enter</span>
1159
- </div>
1160
- <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>
1161
- <div id="query-error" class="hidden" style="color:var(--danger);font-size:0.75rem;margin-top:0.25rem"></div>
1162
- <div id="query-results" style="flex:1;overflow:auto;margin-top:0.25rem;font-size:0.75rem"></div>
1163
- </div>
1164
- </div>
1165
- </div>
1166
-
1167
- <!-- Requests Panel -->
1168
- <div id="panel-requests" class="dev-panel hidden">
1169
- <div class="dev-panel-header">
1170
- <h2>Request Inspector</h2>
1171
- <div class="flex gap-sm">
1172
- <button class="btn btn-sm" onclick="loadRequests()">Refresh</button>
1173
- <button class="btn btn-sm btn-danger" onclick="clearRequests()">Clear</button>
1174
- </div>
1175
- </div>
1176
- <div id="req-stats" class="toolbar text-sm text-muted"></div>
1177
- <table>
1178
- <thead><tr><th>Time</th><th>Method</th><th>Path</th><th>Status</th><th>Duration</th><th>Size</th></tr></thead>
1179
- <tbody id="req-body"></tbody>
1180
- </table>
1181
- <div id="req-empty" class="empty hidden">No requests captured</div>
1182
- </div>
1183
-
1184
- <!-- Errors Panel -->
1185
- <div id="panel-errors" class="dev-panel hidden">
1186
- <div class="dev-panel-header">
1187
- <h2>Error Tracker</h2>
1188
- <div class="flex gap-sm">
1189
- <button class="btn btn-sm" onclick="loadErrors()">Refresh</button>
1190
- <button class="btn btn-sm btn-danger" onclick="clearResolvedErrors()">Clear Resolved</button>
1191
- </div>
1192
- </div>
1193
- <div id="errors-list"></div>
1194
- <div id="errors-empty" class="empty">No errors tracked</div>
1195
- </div>
1196
-
1197
- <!-- WebSocket Panel -->
1198
- <div id="panel-websockets" class="dev-panel hidden">
1199
- <div class="dev-panel-header">
1200
- <h2>WebSocket Connections</h2>
1201
- <button class="btn btn-sm" onclick="loadWebSockets()">Refresh</button>
1202
- </div>
1203
- <table>
1204
- <thead><tr><th>ID</th><th>Path</th><th>IP</th><th>Connected</th><th>Status</th><th></th></tr></thead>
1205
- <tbody id="ws-body"></tbody>
1206
- </table>
1207
- <div id="ws-empty" class="empty">No active connections</div>
1208
- </div>
1209
-
1210
- <!-- System Panel -->
1211
- <div id="panel-system" class="dev-panel hidden">
1212
- <div class="dev-panel-header">
1213
- <h2>System Overview</h2>
1214
- <button class="btn btn-sm" onclick="loadSystem()">Refresh</button>
1215
- </div>
1216
- <div id="sys-cards" class="sys-grid"></div>
1217
- <div id="sys-extensions" class="hidden"></div>
1218
- </div>
1219
-
1220
- <!-- Tools Panel -->
1221
- <div id="panel-tools" class="dev-panel hidden">
1222
- <div class="dev-panel-header">
1223
- <h2>Developer Tools</h2>
1224
- </div>
1225
- <div class="sys-grid">
1226
- <div class="sys-card" style="cursor:pointer" onclick="runTool('test')">
1227
- <div class="label">Run Tests</div>
1228
- <div style="font-size:0.8rem;margin-top:0.25rem">Execute the RSpec test suite</div>
1229
- </div>
1230
- <div class="sys-card" style="cursor:pointer" onclick="runTool('routes')">
1231
- <div class="label">List Routes</div>
1232
- <div style="font-size:0.8rem;margin-top:0.25rem">Show all registered routes with auth status</div>
1233
- </div>
1234
- <div class="sys-card" style="cursor:pointer" onclick="runTool('migrate')">
1235
- <div class="label">Run Migrations</div>
1236
- <div style="font-size:0.8rem;margin-top:0.25rem">Apply pending database migrations</div>
1237
- </div>
1238
- <div class="sys-card" style="cursor:pointer" onclick="runTool('seed')">
1239
- <div class="label">Run Seeders</div>
1240
- <div style="font-size:0.8rem;margin-top:0.25rem">Execute seed scripts</div>
1241
- </div>
1242
- </div>
1243
- <div id="tool-output" class="hidden" style="margin:1rem">
1244
- <div class="dev-panel-header">
1245
- <h2 id="tool-title">Output</h2>
1246
- <button class="btn btn-sm" onclick="document.getElementById('tool-output').classList.add('hidden')">Close</button>
1247
- </div>
1248
- <pre id="tool-result" style="padding:1rem;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);font-size:0.75rem;font-family:var(--mono);max-height:400px;overflow:auto;white-space:pre-wrap"></pre>
1249
- </div>
1250
- </div>
1251
-
1252
- <!-- Connections Panel -->
1253
- <div id="panel-connections" class="dev-panel hidden">
1254
- <div class="dev-panel-header">
1255
- <h2>Connection Builder</h2>
1256
- </div>
1257
- <div class="p-md">
1258
- <div class="flex gap-md" style="flex-wrap:wrap">
1259
- <div style="flex:1;min-width:300px">
1260
- <div class="mb-sm">
1261
- <label class="text-sm text-muted" style="display:block;margin-bottom:0.25rem">Driver</label>
1262
- <select id="conn-driver" class="input" style="width:100%" onchange="connDriverChanged()">
1263
- <option value="sqlite">SQLite</option>
1264
- <option value="postgresql">PostgreSQL</option>
1265
- <option value="mysql">MySQL</option>
1266
- <option value="mssql">MSSQL</option>
1267
- <option value="firebird">Firebird</option>
1268
- </select>
1269
- </div>
1270
- <div class="mb-sm conn-server-field">
1271
- <label class="text-sm text-muted" style="display:block;margin-bottom:0.25rem">Host</label>
1272
- <input type="text" id="conn-host" class="input" style="width:100%" value="localhost" placeholder="localhost" oninput="updateConnectionUrl()">
1273
- </div>
1274
- <div class="mb-sm conn-server-field">
1275
- <label class="text-sm text-muted" style="display:block;margin-bottom:0.25rem">Port</label>
1276
- <input type="number" id="conn-port" class="input" style="width:100%" placeholder="5432" oninput="updateConnectionUrl()">
1277
- </div>
1278
- <div class="mb-sm">
1279
- <label class="text-sm text-muted" style="display:block;margin-bottom:0.25rem">Database</label>
1280
- <input type="text" id="conn-database" class="input" style="width:100%" placeholder="mydb" oninput="updateConnectionUrl()">
1281
- </div>
1282
- <div class="mb-sm conn-server-field">
1283
- <label class="text-sm text-muted" style="display:block;margin-bottom:0.25rem">Username</label>
1284
- <input type="text" id="conn-username" class="input" style="width:100%" placeholder="username">
1285
- </div>
1286
- <div class="mb-sm conn-server-field">
1287
- <label class="text-sm text-muted" style="display:block;margin-bottom:0.25rem">Password</label>
1288
- <input type="password" id="conn-password" class="input" style="width:100%" placeholder="password">
1289
- </div>
1290
- <div class="mb-sm">
1291
- <label class="text-sm text-muted" style="display:block;margin-bottom:0.25rem">Connection URL</label>
1292
- <input type="text" id="conn-url" class="input input-mono" style="width:100%" readonly>
1293
- </div>
1294
- <div class="flex gap-sm">
1295
- <button class="btn btn-primary" onclick="testConnection()">Test Connection</button>
1296
- <button class="btn btn-success" onclick="saveConnection()">Save to .env</button>
1297
- </div>
1298
- </div>
1299
- <div style="width:300px">
1300
- <div class="dev-panel" style="margin-bottom:1rem">
1301
- <div class="dev-panel-header"><h2>Test Result</h2></div>
1302
- <div id="conn-test-result" class="p-md text-sm text-muted">No test run yet</div>
1303
- </div>
1304
- <div class="dev-panel">
1305
- <div class="dev-panel-header"><h2>Current .env Values</h2></div>
1306
- <div id="conn-env-values" class="p-md text-sm text-muted">Loading...</div>
1307
- </div>
1308
- </div>
1309
- </div>
1310
- </div>
1311
- </div>
1312
-
1313
- <script>
1314
- function connDriverChanged() {
1315
- var driver = document.getElementById('conn-driver').value;
1316
- var ports = {postgresql: 5432, mysql: 3306, mssql: 1433, firebird: 3050};
1317
- var isSqlite = (driver === 'sqlite');
1318
- document.getElementById('conn-port').value = ports[driver] || '';
1319
- var fields = document.querySelectorAll('.conn-server-field');
1320
- for (var i = 0; i < fields.length; i++) {
1321
- fields[i].style.display = isSqlite ? 'none' : '';
1322
- }
1323
- updateConnectionUrl();
1324
- }
1325
- function updateConnectionUrl() {
1326
- var driver = document.getElementById('conn-driver').value;
1327
- var host = document.getElementById('conn-host').value || 'localhost';
1328
- var port = document.getElementById('conn-port').value;
1329
- var database = document.getElementById('conn-database').value;
1330
- if (driver === 'sqlite') {
1331
- document.getElementById('conn-url').value = 'sqlite:///' + database;
1332
- } else {
1333
- document.getElementById('conn-url').value = driver + '://' + host + ':' + port + '/' + database;
1334
- }
1335
- }
1336
- function testConnection() {
1337
- var url = document.getElementById('conn-url').value;
1338
- var username = document.getElementById('conn-username').value;
1339
- var password = document.getElementById('conn-password').value;
1340
- var el = document.getElementById('conn-test-result');
1341
- el.innerHTML = '<span class="text-muted">Testing...</span>';
1342
- fetch('/__dev/api/connections/test', {
1343
- method: 'POST',
1344
- headers: {'Content-Type': 'application/json'},
1345
- body: JSON.stringify({url: url, username: username, password: password})
1346
- }).then(function(r){return r.json()}).then(function(data) {
1347
- if (data.success) {
1348
- el.innerHTML = '<div style="color:var(--success);font-weight:600;margin-bottom:0.5rem">&#10004; Connected</div>' +
1349
- '<div class="text-sm">Version: ' + (data.version || 'N/A') + '</div>' +
1350
- '<div class="text-sm">Tables: ' + (data.tables !== undefined ? data.tables : 'N/A') + '</div>';
1351
- } else {
1352
- el.innerHTML = '<div style="color:var(--danger);font-weight:600;margin-bottom:0.5rem">&#10008; Failed</div>' +
1353
- '<div class="text-sm" style="color:var(--danger)">' + (data.error || 'Unknown error') + '</div>';
1354
- }
1355
- }).catch(function(e) {
1356
- el.innerHTML = '<div style="color:var(--danger)">Error: ' + e.message + '</div>';
1357
- });
1358
- }
1359
- function saveConnection() {
1360
- var url = document.getElementById('conn-url').value;
1361
- var username = document.getElementById('conn-username').value;
1362
- var password = document.getElementById('conn-password').value;
1363
- if (!url) { alert('Please build a connection URL first'); return; }
1364
- fetch('/__dev/api/connections/save', {
1365
- method: 'POST',
1366
- headers: {'Content-Type': 'application/json'},
1367
- body: JSON.stringify({url: url, username: username, password: password})
1368
- }).then(function(r){return r.json()}).then(function(data) {
1369
- if (data.success) {
1370
- alert('Connection saved to .env');
1371
- loadConnectionEnv();
1372
- } else {
1373
- alert('Save failed: ' + (data.error || 'Unknown error'));
1374
- }
1375
- }).catch(function(e) { alert('Error: ' + e.message); });
1376
- }
1377
- function loadConnectionEnv() {
1378
- fetch('/__dev/api/connections').then(function(r){return r.json()}).then(function(data) {
1379
- var el = document.getElementById('conn-env-values');
1380
- el.innerHTML = '<div class="mb-sm"><span class="text-muted">DATABASE_URL:</span> <code>' + (data.url || '<em>not set</em>') + '</code></div>' +
1381
- '<div class="mb-sm"><span class="text-muted">DATABASE_USERNAME:</span> <code>' + (data.username || '<em>not set</em>') + '</code></div>' +
1382
- '<div><span class="text-muted">DATABASE_PASSWORD:</span> <code>' + (data.password || '<em>not set</em>') + '</code></div>';
1383
- }).catch(function() {
1384
- document.getElementById('conn-env-values').innerHTML = '<span class="text-muted">Could not load .env values</span>';
1385
- });
1386
- }
1387
- document.addEventListener('DOMContentLoaded', function() {
1388
- var connTab = document.querySelector('[onclick*="connections"]');
1389
- if (connTab) {
1390
- connTab.addEventListener('click', function() { loadConnectionEnv(); }, {once: true});
1391
- }
1392
- });
1393
- </script>
1394
-
1395
- <!-- Metrics Panel -->
1396
- <div id="panel-metrics" class="dev-panel hidden">
1397
- <div class="dev-panel-header">
1398
- <h2>Code Metrics</h2>
1399
- <div>
1400
- <button class="btn btn-sm" onclick="loadAllMetrics()">Refresh</button>
1401
- </div>
1402
- </div>
1403
- <div id="metrics-bubble" style="margin:1rem;"></div>
1404
- <div id="metrics-drilldown" style="margin:0 1rem;display:none;"></div>
1405
- <div id="metrics-quick" class="sys-grid"></div>
1406
- <div id="metrics-largest" style="margin-top:1rem;"></div>
1407
- <div id="metrics-tables" style="margin-top:1rem;padding:0 1rem 1rem;overflow-x:auto;">
1408
- <h3 style="margin:1rem 0 0.5rem;color:var(--primary);">File Analysis</h3>
1409
- <div id="metrics-heatmap"></div>
1410
- <h3 style="margin:1rem 0 0.5rem;color:var(--primary);">Most Complex Functions</h3>
1411
- <div id="metrics-complex"></div>
1412
- <h3 style="margin:1rem 0 0.5rem;color:var(--primary);">Coupling Analysis</h3>
1413
- <div id="metrics-coupling"></div>
1414
- <h3 style="margin:1rem 0 0.5rem;color:var(--primary);">Violations</h3>
1415
- <div id="metrics-violations"></div>
1416
- </div>
1417
- </div>
1418
-
1419
- <!-- Chat Panel (Tina4) -->
1420
- <div id="panel-chat" class="dev-panel hidden">
1421
- <div class="dev-panel-header">
1422
- <h2>Tina4</h2>
1423
- <div class="flex gap-sm items-center">
1424
- <select id="ai-provider" class="input" style="width:120px">
1425
- <option value="anthropic">Claude</option>
1426
- <option value="openai">OpenAI</option>
1427
- </select>
1428
- <input type="password" id="ai-key" class="input" placeholder="Paste API key..." style="width:250px">
1429
- <button class="btn btn-sm btn-primary" onclick="setAiKey()">Set Key</button>
1430
- <span class="text-sm text-muted" id="ai-status">No key set</span>
1431
- </div>
1432
- </div>
1433
- <div class="chat-container">
1434
- <div class="chat-messages" id="chat-messages">
1435
- <div class="chat-msg chat-bot">Hi! I'm Tina4. Ask me about routes, ORM, database, queues, templates, auth, or any Tina4 feature.</div>
1436
- </div>
1437
- <div class="chat-input-row">
1438
- <input type="text" id="chat-input" class="input" placeholder="Ask Tina4..." onkeydown="if(event.key==='Enter')sendChat()">
1439
- <button class="btn btn-primary" onclick="sendChat()">Send</button>
1440
- </div>
1441
- </div>
1442
- </div>
1443
-
1444
- </div>
1445
-
1446
- <script src="/__dev/js/tina4-dev-admin.min.js"></script>
1447
- <script>
1448
- // ── Metrics Panel JS ──
1449
- var _metricsFullData=null;
1450
- function miColor(mi){
1451
- if(mi>=60) return 'rgb('+(Math.round(34+(1-((mi-60)/40))*186))+','+(Math.round(197-(1-((mi-60)/40))*50))+',0)';
1452
- if(mi>=30) return 'rgb('+(Math.round(220+((60-mi)/30)*19))+','+(Math.round(180-((60-mi)/30)*112))+',0)';
1453
- return 'rgb(239,'+(Math.round(68-mi*2))+',0)';
1454
- }
1455
- function renderBubbleChart(files,depGraph,scanMode){
1456
- var container=document.getElementById('metrics-bubble');
1457
- if(!files||!files.length){container.innerHTML='<p style="color:var(--muted);padding:1rem">No files to analyze</p>';return;}
1458
- depGraph=depGraph||{};
1459
- scanMode=scanMode||'project';
1460
- var W=container.offsetWidth||900,H=Math.max(450,Math.min(650,W*0.45));
1461
- var maxLoc=Math.max.apply(null,files.map(function(f){return f.loc}))||1;
1462
- var maxDeps=Math.max.apply(null,files.map(function(f){return f.dep_count||0}))||1;
1463
- var maxCC=Math.max.apply(null,files.map(function(f){return f.complexity||0}))||1;
1464
- var minR=14,maxR=Math.min(70,W/10);
1465
- function healthColor(f){
1466
- var cc=Math.min((f.avg_complexity||0)/10,1);
1467
- var untested=f.has_tests?0:1;
1468
- var deps=Math.min((f.dep_count||0)/5,1);
1469
- var score=cc*0.4+untested*0.4+deps*0.2;
1470
- score=Math.max(0,Math.min(1,score));
1471
- var hue=Math.round(120*(1-score));
1472
- var sat=Math.round(70+score*30);
1473
- var lit=Math.round(42+18*(1-score));
1474
- return 'hsl('+hue+','+sat+'%,'+lit+'%)';
1475
- }
1476
- var pathIdx={};
1477
- files.forEach(function(f,i){pathIdx[f.path]=i;});
1478
- function sizeScore(f){return (f.loc/maxLoc)*0.4+((f.avg_complexity||0)/10)*0.4+((f.dep_count||0)/maxDeps)*0.2;}
1479
- var sorted=files.slice().sort(function(a,b){return sizeScore(a)-sizeScore(b)});
1480
- var cx=W/2,cy=H/2;
1481
- var bubbles=[];
1482
- var angle=0,spiralR=0;
1483
- for(var i=0;i<sorted.length;i++){
1484
- var f=sorted[i];
1485
- var r=minR+Math.sqrt(sizeScore(f))*(maxR-minR);
1486
- var color=healthColor(f);
1487
- var placed=false;
1488
- for(var attempt=0;attempt<800;attempt++){
1489
- var px=cx+spiralR*Math.cos(angle);
1490
- var py=cy+spiralR*Math.sin(angle);
1491
- var collides=false;
1492
- for(var j=0;j<bubbles.length;j++){
1493
- var dx=px-bubbles[j].x,dy=py-bubbles[j].y;
1494
- if(Math.sqrt(dx*dx+dy*dy)<r+bubbles[j].r+2){collides=true;break;}
1495
- }
1496
- if(!collides&&px>r+2&&px<W-r-2&&py>r+25&&py<H-r-2){
1497
- bubbles.push({x:px,y:py,vx:0,vy:0,r:r,color:color,f:f});
1498
- placed=true;break;
1499
- }
1500
- angle+=0.2;spiralR+=0.04;
1501
- }
1502
- 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});}
1503
- }
1504
- var edges=[];
1505
- function basename(p){var n=p.split('/').pop();var d=n.lastIndexOf('.');return(d>0?n.substring(0,d):n).toLowerCase();}
1506
- var nameIdx={};
1507
- bubbles.forEach(function(b,i){nameIdx[basename(b.f.path)]=i;});
1508
- Object.keys(depGraph).forEach(function(src){
1509
- var srcIdx=null;
1510
- bubbles.forEach(function(b,i){if(b.f.path===src)srcIdx=i;});
1511
- if(srcIdx===null)return;
1512
- (depGraph[src]||[]).forEach(function(tgt){
1513
- var tgtName=tgt.split('.').pop().toLowerCase();
1514
- var tgtIdx=nameIdx[tgtName];
1515
- if(tgtIdx!==undefined&&srcIdx!==tgtIdx)edges.push([srcIdx,tgtIdx]);
1516
- });
1517
- });
1518
- var canvas=document.createElement('canvas');
1519
- canvas.width=W;canvas.height=H;
1520
- canvas.style.cssText='display:block;border:1px solid var(--border);border-radius:8px;cursor:pointer;background:#0f172a';
1521
- var modeLabel=scanMode==='framework'?'<span style="color:#cba6f7;font-weight:600"> (Framework)</span> Add code to src/ to see your project':'';
1522
- 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>';
1523
- container.appendChild(canvas);
1524
- var ctx=canvas.getContext('2d');
1525
- var hoveredIdx=-1,dragIdx=-1,dragOX=0,dragOY=0;
1526
- function simulate(){
1527
- var damping=0.65,springK=0.002,repulse=40,gravity=0.008;
1528
- var cx=W/2,cy=H/2;
1529
- bubbles.forEach(function(b,idx){
1530
- if(idx===dragIdx)return;
1531
- var dx=cx-b.x,dy=cy-b.y;
1532
- var sizeFactor=0.3+(b.r/maxR)*0.7;
1533
- var pull=gravity*sizeFactor*sizeFactor;
1534
- b.vx+=dx*pull;b.vy+=dy*pull;
1535
- });
1536
- edges.forEach(function(e){
1537
- var a=bubbles[e[0]],b=bubbles[e[1]];
1538
- var dx=b.x-a.x,dy=b.y-a.y;
1539
- var dist=Math.sqrt(dx*dx+dy*dy)||1;
1540
- var rest=a.r+b.r+20;
1541
- var force=(dist-rest)*springK;
1542
- var fx=dx/dist*force,fy=dy/dist*force;
1543
- if(e[0]!==dragIdx){a.vx+=fx;a.vy+=fy;}
1544
- if(e[1]!==dragIdx){b.vx-=fx;b.vy-=fy;}
1545
- });
1546
- for(var i=0;i<bubbles.length;i++){
1547
- for(var j=i+1;j<bubbles.length;j++){
1548
- var a=bubbles[i],b=bubbles[j];
1549
- var dx=b.x-a.x,dy=b.y-a.y;
1550
- var dist=Math.sqrt(dx*dx+dy*dy)||1;
1551
- var minDist=a.r+b.r+20;
1552
- if(dist<minDist){
1553
- var force=repulse*(minDist-dist)/minDist;
1554
- var fx=dx/dist*force,fy=dy/dist*force;
1555
- if(i!==dragIdx){a.vx-=fx;a.vy-=fy;}
1556
- if(j!==dragIdx){b.vx+=fx;b.vy+=fy;}
1557
- }
1558
- }
1559
- }
1560
- bubbles.forEach(function(b,idx){
1561
- if(idx===dragIdx)return;
1562
- b.vx*=damping;b.vy*=damping;
1563
- var maxV=2;
1564
- if(b.vx>maxV)b.vx=maxV;if(b.vx<-maxV)b.vx=-maxV;
1565
- if(b.vy>maxV)b.vy=maxV;if(b.vy<-maxV)b.vy=-maxV;
1566
- b.x+=b.vx;b.y+=b.vy;
1567
- b.x=Math.max(b.r+2,Math.min(W-b.r-2,b.x));
1568
- b.y=Math.max(b.r+25,Math.min(H-b.r-2,b.y));
1569
- });
1570
- }
1571
- function draw(){
1572
- simulate();
1573
- ctx.clearRect(0,0,W,H);
1574
- ctx.save();ctx.translate(panX,panY);ctx.scale(zoom,zoom);
1575
- ctx.strokeStyle='rgba(255,255,255,0.03)';ctx.lineWidth=1/zoom;
1576
- for(var gx=0;gx<W/zoom;gx+=50){ctx.beginPath();ctx.moveTo(gx,0);ctx.lineTo(gx,H/zoom);ctx.stroke();}
1577
- for(var gy=0;gy<H/zoom;gy+=50){ctx.beginPath();ctx.moveTo(0,gy);ctx.lineTo(W/zoom,gy);ctx.stroke();}
1578
- edges.forEach(function(e){
1579
- var a=bubbles[e[0]],b=bubbles[e[1]];
1580
- var dx=b.x-a.x,dy=b.y-a.y;
1581
- var dist=Math.sqrt(dx*dx+dy*dy)||1;
1582
- var highlighted=(hoveredIdx===e[0]||hoveredIdx===e[1]);
1583
- ctx.beginPath();
1584
- ctx.moveTo(a.x+dx/dist*a.r,a.y+dy/dist*a.r);
1585
- var ex=b.x-dx/dist*b.r,ey=b.y-dy/dist*b.r;
1586
- ctx.lineTo(ex,ey);
1587
- ctx.strokeStyle=highlighted?'rgba(139,180,250,0.9)':'rgba(255,255,255,0.3)';
1588
- ctx.lineWidth=highlighted?3:1.5;ctx.stroke();
1589
- var aLen=highlighted?14:8;
1590
- var aAngle=Math.atan2(dy,dx);
1591
- ctx.beginPath();
1592
- ctx.moveTo(ex,ey);
1593
- ctx.lineTo(ex-aLen*Math.cos(aAngle-0.4),ey-aLen*Math.sin(aAngle-0.4));
1594
- ctx.lineTo(ex-aLen*Math.cos(aAngle+0.4),ey-aLen*Math.sin(aAngle+0.4));
1595
- ctx.closePath();ctx.fillStyle=ctx.strokeStyle;ctx.fill();
1596
- });
1597
- bubbles.forEach(function(b,idx){
1598
- var isHovered=(idx===hoveredIdx);
1599
- var drawR=isHovered?b.r+4:b.r;
1600
- 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();}
1601
- ctx.beginPath();ctx.arc(b.x,b.y,drawR,0,Math.PI*2);
1602
- ctx.fillStyle=b.color;ctx.globalAlpha=isHovered?1.0:0.85;ctx.fill();
1603
- 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();
1604
- var name=b.f.path.split('/').pop().replace('.rb','');
1605
- if(drawR>16){
1606
- var fs=Math.max(8,Math.min(13,drawR*0.38));
1607
- ctx.fillStyle='#fff';ctx.font='600 '+fs+'px monospace';ctx.textAlign='center';
1608
- ctx.fillText(name,b.x,b.y-2);
1609
- ctx.fillStyle='rgba(255,255,255,0.65)';ctx.font=(fs-1)+'px monospace';
1610
- ctx.fillText(b.f.loc+' LOC',b.x,b.y+fs);
1611
- if(isHovered&&drawR>25){
1612
- ctx.fillStyle='rgba(255,255,255,0.5)';ctx.font=(fs-2)+'px monospace';
1613
- ctx.fillText('CC:'+b.f.complexity+' MI:'+b.f.maintainability,b.x,b.y+fs*2);
1614
- }
1615
- }
1616
- var mfs=Math.max(9,drawR*0.3);
1617
- var mrad=mfs*0.7;
1618
- var mpad=mrad*2.4;
1619
- var my=b.y-drawR+mrad+3;
1620
- if(drawR>14&&b.f.has_tests){
1621
- var mx=b.x-(b.f.dep_count>0?mpad*0.5:0);
1622
- ctx.beginPath();ctx.arc(mx,my,mrad,0,Math.PI*2);
1623
- ctx.fillStyle='#16a34a';ctx.fill();
1624
- ctx.fillStyle='#fff';ctx.font='bold '+mfs+'px sans-serif';ctx.textAlign='center';
1625
- ctx.fillText('T',mx,my+mfs*0.35);
1626
- }
1627
- if(drawR>14&&b.f.dep_count>0){
1628
- var mx2=b.x+(b.f.has_tests?mpad*0.5:0);
1629
- ctx.beginPath();ctx.arc(mx2,my,mrad,0,Math.PI*2);
1630
- ctx.fillStyle='#ea580c';ctx.fill();
1631
- ctx.fillStyle='#fff';ctx.font='bold '+mfs+'px sans-serif';ctx.textAlign='center';
1632
- ctx.fillText('D',mx2,my+mfs*0.35);
1633
- }
1634
- b._drawX=b.x;b._drawY=b.y;b._drawR=drawR;
1635
- });
1636
- var totalLoc=0,totalFiles=bubbles.length,testedCount=0;
1637
- bubbles.forEach(function(b){totalLoc+=b.f.loc;if(b.f.has_tests)testedCount++;});
1638
- var avgMI=bubbles.reduce(function(s,b){return s+b.f.maintainability},0)/totalFiles;
1639
- ctx.fillStyle='rgba(255,255,255,0.35)';ctx.font='11px monospace';ctx.textAlign='right';
1640
- ctx.restore();
1641
- ctx.fillStyle='rgba(255,255,255,0.35)';ctx.font='11px monospace';ctx.textAlign='right';
1642
- ctx.fillText(totalFiles+' files | '+totalLoc.toLocaleString()+' LOC | MI:'+avgMI.toFixed(1)+' | Tested:'+testedCount+'/'+totalFiles,W-12,H-10);
1643
- window._metricsAnimFrame=requestAnimationFrame(draw);
1644
- }
1645
- draw();
1646
- var panning=false,panStartX=0,panStartY=0;
1647
- canvas.addEventListener('contextmenu',function(e){e.preventDefault();});
1648
- canvas.addEventListener('mousemove',function(e){
1649
- var rect=canvas.getBoundingClientRect();
1650
- var mx=e.clientX-rect.left,my=e.clientY-rect.top;
1651
- if(panning){
1652
- panX+=(mx-panStartX);panY+=(my-panStartY);
1653
- panStartX=mx;panStartY=my;return;
1654
- }
1655
- if(dragIdx>=0){
1656
- var wmx=(mx-panX)/zoom,wmy=(my-panY)/zoom;
1657
- bubbles[dragIdx].x=wmx-dragOX;bubbles[dragIdx].y=wmy-dragOY;
1658
- bubbles[dragIdx].vx=0;bubbles[dragIdx].vy=0;return;
1659
- }
1660
- var wmx2=(mx-panX)/zoom,wmy2=(my-panY)/zoom;
1661
- hoveredIdx=-1;
1662
- for(var i=bubbles.length-1;i>=0;i--){
1663
- var b=bubbles[i];
1664
- var dx=wmx2-b.x,dy=wmy2-b.y;
1665
- if(Math.sqrt(dx*dx+dy*dy)<=b.r){hoveredIdx=i;break;}
1666
- }
1667
- canvas.style.cursor=panning?'move':hoveredIdx>=0?'grab':'default';
1668
- });
1669
- canvas.addEventListener('mousedown',function(e){
1670
- var rect=canvas.getBoundingClientRect();
1671
- var mx=e.clientX-rect.left,my=e.clientY-rect.top;
1672
- if(e.button===2){
1673
- panning=true;panStartX=mx;panStartY=my;
1674
- canvas.style.cursor='move';return;
1675
- }
1676
- if(hoveredIdx>=0){
1677
- dragIdx=hoveredIdx;
1678
- var wmx=(mx-panX)/zoom;
1679
- var wmy=(my-panY)/zoom;
1680
- dragOX=wmx-bubbles[dragIdx].x;
1681
- dragOY=wmy-bubbles[dragIdx].y;
1682
- canvas.style.cursor='grabbing';
1683
- }
1684
- });
1685
- canvas.addEventListener('mouseup',function(){
1686
- if(panning){panning=false;canvas.style.cursor='default';}
1687
- if(dragIdx>=0){canvas.style.cursor='grab';dragIdx=-1;}
1688
- });
1689
- canvas.addEventListener('mouseleave',function(){hoveredIdx=-1;dragIdx=-1;panning=false;});
1690
- canvas.addEventListener('dblclick',function(e){
1691
- if(hoveredIdx<0)return;
1692
- drillDownFile(bubbles[hoveredIdx].f.path);
1693
- });
1694
- var zoom=1.0,panX=0,panY=0;
1695
- canvas.addEventListener('wheel',function(e){
1696
- e.preventDefault();
1697
- var rect=canvas.getBoundingClientRect();
1698
- var mx=(e.clientX-rect.left-panX)/zoom;
1699
- var my=(e.clientY-rect.top-panY)/zoom;
1700
- var oldZoom=zoom;
1701
- zoom*=e.deltaY<0?1.08:0.93;
1702
- zoom=Math.max(0.5,Math.min(2.5,zoom));
1703
- panX+=(mx*oldZoom-mx*zoom);
1704
- panY+=(my*oldZoom-my*zoom);
1705
- bubbles.forEach(function(b){});
1706
- },{passive:false});
1707
- bubbles.forEach(function(b){b._baseR=b.r;});
1708
- }
1709
- function drillDownFile(path){
1710
- var dd=document.getElementById('metrics-drilldown');
1711
- dd.style.display='block';
1712
- 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>';
1713
- fetch('/__dev/api/metrics/file?path='+encodeURIComponent(path)).then(function(r){return r.json()}).then(function(d){
1714
- if(d.error){dd.querySelector('.p-md').innerHTML='<p style="color:var(--danger)">'+d.error+'</p>';return;}
1715
- var html='<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:0.5rem;margin-bottom:1rem">';
1716
- html+='<div class="sys-card"><div class="label">LOC</div><div class="value">'+d.loc+'</div></div>';
1717
- html+='<div class="sys-card"><div class="label">Total Lines</div><div class="value">'+d.total_lines+'</div></div>';
1718
- html+='<div class="sys-card"><div class="label">Classes</div><div class="value">'+d.classes+'</div></div>';
1719
- html+='<div class="sys-card"><div class="label">Functions</div><div class="value">'+(d.functions?d.functions.length:0)+'</div></div>';
1720
- html+='<div class="sys-card"><div class="label">Imports</div><div class="value">'+(d.imports?d.imports.length:0)+'</div></div>';
1721
- html+='</div>';
1722
- if(d.functions&&d.functions.length){
1723
- html+='<h3 style="margin:0.5rem 0;color:var(--primary);font-size:0.85rem">Cyclomatic Complexity by Function</h3>';
1724
- var maxCC=Math.max.apply(null,d.functions.map(function(f){return f.complexity}))||1;
1725
- html+='<div style="display:flex;flex-direction:column;gap:4px">';
1726
- d.functions.forEach(function(f){
1727
- var pct=Math.max(3,f.complexity/maxCC*100);
1728
- var color=f.complexity>20?'#ef4444':f.complexity>10?'#eab308':f.complexity>5?'#3b82f6':'#22c55e';
1729
- html+='<div style="display:flex;align-items:center;gap:8px;font-size:0.75rem;font-family:var(--mono)">';
1730
- html+='<span style="width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)" title="'+f.name+'">'+f.name+'</span>';
1731
- html+='<div style="flex:1;height:16px;background:var(--bg);border-radius:3px;overflow:hidden;position:relative">';
1732
- html+='<div style="width:'+pct+'%;height:100%;background:'+color+';border-radius:3px;transition:width 0.3s"></div>';
1733
- html+='</div>';
1734
- html+='<span style="width:70px;text-align:right;color:'+color+';font-weight:600">CC:'+f.complexity+'</span>';
1735
- html+='<span style="width:60px;text-align:right;color:var(--muted)">'+f.loc+' LOC</span>';
1736
- html+='<span style="width:30px;text-align:right;color:var(--muted)">L'+f.line+'</span>';
1737
- html+='</div>';
1738
- });
1739
- html+='</div>';
1740
- }
1741
- if(d.imports&&d.imports.length){
1742
- html+='<h3 style="margin:0.75rem 0 0.25rem;color:var(--primary);font-size:0.85rem">Dependencies</h3>';
1743
- html+='<div style="display:flex;flex-wrap:wrap;gap:4px">';
1744
- d.imports.forEach(function(imp){
1745
- 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>';
1746
- });
1747
- html+='</div>';
1748
- }
1749
- if(d.warnings&&d.warnings.length){
1750
- html+='<h3 style="margin:0.75rem 0 0.25rem;color:#eab308;font-size:0.85rem">&#9888; Warnings</h3>';
1751
- html+='<div style="display:flex;flex-direction:column;gap:4px">';
1752
- d.warnings.forEach(function(w){
1753
- html+='<div style="padding:4px 8px;background:rgba(234,179,8,0.08);border-left:3px solid #eab308;border-radius:0 4px 4px 0;font-size:0.75rem;font-family:var(--mono);color:var(--text)">';
1754
- html+='<span style="color:#eab308;margin-right:6px">L'+w.line+'</span>'+w.message+'</div>';
1755
- });
1756
- html+='</div>';
1757
- }
1758
- dd.querySelector('.p-md').innerHTML=html;
1759
- }).catch(function(e){
1760
- dd.querySelector('.p-md').innerHTML='<p style="color:var(--danger)">Error: '+e.message+'</p>';
1761
- });
1762
- dd.scrollIntoView({behavior:'smooth',block:'start'});
1763
- }
1764
- function loadAllMetrics(){
1765
- if(window._metricsAnimFrame)cancelAnimationFrame(window._metricsAnimFrame);
1766
- var el=document.getElementById('metrics-quick');
1767
- el.innerHTML='<div class="sys-card"><div class="value">Loading...</div></div>';
1768
- fetch('/__dev/api/metrics').then(function(r){return r.json()}).then(function(d){
1769
- if(d.error){el.innerHTML='<div class="sys-card"><div class="value" style="color:var(--danger)">'+d.error+'</div></div>';return;}
1770
- el.innerHTML=
1771
- '<div class="sys-card"><div class="label">Ruby Files</div><div class="value">'+d.file_count+'</div></div>'+
1772
- '<div class="sys-card"><div class="label">Lines of Code</div><div class="value">'+d.total_loc.toLocaleString()+'</div></div>'+
1773
- '<div class="sys-card"><div class="label">Comment Lines</div><div class="value">'+d.total_comment.toLocaleString()+'</div></div>'+
1774
- '<div class="sys-card"><div class="label">Blank Lines</div><div class="value">'+d.total_blank.toLocaleString()+'</div></div>'+
1775
- '<div class="sys-card"><div class="label">Classes</div><div class="value">'+d.classes+'</div></div>'+
1776
- '<div class="sys-card"><div class="label">Functions</div><div class="value">'+d.functions+'</div></div>'+
1777
- '<div class="sys-card"><div class="label">Routes</div><div class="value">'+d.route_count+'</div></div>'+
1778
- '<div class="sys-card"><div class="label">ORM Models</div><div class="value">'+d.orm_count+'</div></div>'+
1779
- '<div class="sys-card"><div class="label">Templates</div><div class="value">'+d.template_count+'</div></div>'+
1780
- '<div class="sys-card"><div class="label">Migrations</div><div class="value">'+d.migration_count+'</div></div>';
1781
- }).catch(function(e){el.innerHTML='<div class="sys-card"><div class="value" style="color:var(--danger)">Error: '+e.message+'</div></div>';});
1782
- document.getElementById('metrics-bubble').innerHTML='<p style="color:var(--muted);padding:1rem">Analyzing codebase...</p>';
1783
- fetch('/__dev/api/metrics/full').then(function(r){return r.json()}).then(function(d){
1784
- _metricsFullData=d;
1785
- if(d.error){document.getElementById('metrics-bubble').innerHTML='<p style="color:var(--danger);padding:1rem">'+d.error+'</p>';return;}
1786
- renderBubbleChart(d.file_metrics,d.dependency_graph,d.scan_mode);
1787
- var hm=document.getElementById('metrics-heatmap');
1788
- var rows=d.file_metrics.map(function(f){
1789
- var color=miColor(f.maintainability);
1790
- var barW=Math.max(2,Math.min(100,f.maintainability));
1791
- 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>';
1792
- }).join('');
1793
- 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>';
1794
- var cf=document.getElementById('metrics-complex');
1795
- var frows=d.most_complex_functions.map(function(f){
1796
- var color=f.complexity>20?'#ef4444':f.complexity>10?'#eab308':'#22c55e';
1797
- 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>';
1798
- }).join('');
1799
- 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>';
1800
- var cp=document.getElementById('metrics-coupling');
1801
- var crows=d.file_metrics.filter(function(f){return f.coupling_afferent>0||f.coupling_efferent>0}).map(function(f){
1802
- 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>';
1803
- }).join('');
1804
- 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>';
1805
- var vl=document.getElementById('metrics-violations');
1806
- if(d.violations&&d.violations.length){
1807
- var vrows=d.violations.map(function(v){
1808
- var icon=v.type==='error'?'&#9888;':'&#9432;';
1809
- var color=v.type==='error'?'#ef4444':'#eab308';
1810
- 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>';
1811
- }).join('');
1812
- vl.innerHTML='<table style="width:100%"><thead><tr><th></th><th>Issue</th><th>Location</th></tr></thead><tbody>'+vrows+'</tbody></table>';
1813
- }else{
1814
- vl.innerHTML='<p style="color:#22c55e">&#10003; No violations found</p>';
1815
- }
1816
- }).catch(function(e){
1817
- document.getElementById('metrics-bubble').innerHTML='<p style="color:var(--danger);padding:1rem">Error: '+e.message+'</p>';
1818
- });
1819
- }
1820
- var _metricsLoaded=false;
1821
- var _origShowTab=typeof showTab==='function'?showTab:null;
1822
- if(_origShowTab){
1823
- showTab=function(name){
1824
- _origShowTab(name);
1825
- if(name==='metrics'&&!_metricsLoaded){_metricsLoaded=true;loadAllMetrics();}
1826
- };
1827
- }
1828
- var metricsTab=document.querySelector('[onclick*="metrics"]');
1829
- if(metricsTab)metricsTab.addEventListener('click',function(){if(!_metricsLoaded){_metricsLoaded=true;loadAllMetrics();}});
1830
- </script>
1831
- <script>
1832
- // Self-diagnostic — detect if the external JS failed to load
1833
- (function() {
1834
- if (typeof showTab !== 'function') {
1835
- var banner = document.createElement('div');
1836
- banner.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:99999;background:#ef4444;color:#fff;padding:0.75rem 1rem;font-family:system-ui;font-size:0.85rem;text-align:center';
1837
- banner.innerHTML = '<strong>Dev Admin Error:</strong> tina4-dev-admin.min.js failed to load. Check that /__dev/js/tina4-dev-admin.min.js is accessible.';
1838
- document.body.insertBefore(banner, document.body.firstChild);
1839
- }
1840
- })();
1841
- </script>
1842
- </body>
1843
- </html>
1844
- HTML
1845
- end
1846
900
  end
1847
901
  end
1848
902
  end