tina4ruby 3.13.149 → 3.13.150

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '08653f37ab3b3baf01b6103f97ec3893f910fbaf5a50b4b34a5521c872e9a225'
4
- data.tar.gz: 2df60c055ca63a7957f79da033add45a12e3418f1e5890a3a986c254a8f4f6ec
3
+ metadata.gz: e63d80b48d2821411c3174e3307176cdf996bfc722c2c228be2b829df8a898c3
4
+ data.tar.gz: 4323b78fdba05283005597a4c8489d29a8b74cb02754bc9ec7b4c88f361d0cbc
5
5
  SHA512:
6
- metadata.gz: dafb902ac5f0b385862acf78fbc1ded074233082a98d7dac462f095fb82fc4043a0ff5369eb9a7dbcd0c4662daab0c00b34a1c1647bcb6f29332a7d4882bbc30
7
- data.tar.gz: 2408c4a0f724372a555efb9a39d3dd30b4334476109e257e4eb301d38db666f27f932bcd6b278c0087e3080f109042924236b550c6fafb6d8763b0db9e9c505d
6
+ metadata.gz: 68483df7f4ae92df44d121678397b0c6a4884277e88c7321fc0ed57ba36e34ae26bb8bca3f148eed1904127907d2a0c20369ca729341131ebea409a3e47b799b
7
+ data.tar.gz: aa95f3c686a89537e480bdc2d49de82a1d8cec58dfe0e1c24398f8f19ea3552ba9ec2eac8a351ef859971400ed0756afe2e8421bcd4461ee4c02ffce21e6f715
data/CHANGELOG.md CHANGED
@@ -6,6 +6,16 @@ number means the same thing everywhere.
6
6
  **The authoritative release notes for every shipped version live in the documentation:**
7
7
  https://tina4.com/ruby/36-releases
8
8
 
9
+ ## 3.13.150 — 2026-10-11
10
+
11
+ ### Dev admin
12
+ - The dashboard AI talks to the `tina4 ai serve` gateway on :11435, not a model
13
+ directly; `/__dev/split` serves the shared split-preview asset; the scaffold
14
+ panel is data-driven from the CLI generators; and the Code Landscape metrics
15
+ bubbles are free to spread (zoom-out + Fit frame the layout, was crammed into
16
+ the viewport). The dashboard metrics scan passes `--no-history` so it stops
17
+ self-reloading. Shipped in the shared SPA bundle.
18
+
9
19
  ## 3.13.149 — 2026-10-10
10
20
 
11
21
  ### CRUD
@@ -439,6 +439,8 @@ module Tina4
439
439
  serve_dashboard
440
440
  when ["GET", "/__dev/js/tina4-dev-admin.min.js"]
441
441
  serve_dev_js
442
+ when ["GET", "/__dev/split"], ["GET", "/__dev/split/"]
443
+ serve_split
442
444
  when ["GET", "/__dev/toolbar.css"]
443
445
  # tina4stack #115: the injected toolbar's stylesheet, served as an
444
446
  # external asset so the toolbar carries no inline style= and renders
@@ -934,6 +936,18 @@ module Tina4
934
936
  end
935
937
  end
936
938
 
939
+ # Detached split view — dashboard/editor LEFT, live app preview RIGHT.
940
+ # Served from the SHARED asset (the same tina4-dev-admin-split.html the
941
+ # python/php/node frameworks serve); a plain shell, no toolbar injected.
942
+ def serve_split
943
+ path = File.join(File.dirname(__FILE__), "public", "js", "tina4-dev-admin-split.html")
944
+ if File.file?(path)
945
+ [200, { "content-type" => "text/html; charset=utf-8" }, [File.read(path)]]
946
+ else
947
+ [404, { "content-type" => "text/plain" }, ["tina4-dev-admin-split.html not found"]]
948
+ end
949
+ end
950
+
937
951
  def status_payload
938
952
  db_table_count = 0
939
953
  begin
data/lib/tina4/metrics.rb CHANGED
@@ -50,7 +50,11 @@ module Tina4
50
50
  binary = engine_path
51
51
  raise MetricsEngineError, "tina4 not found on PATH - #{INSTALL_HINT}" unless binary
52
52
 
53
- stdout, stderr, status = Open3.capture3(binary, 'metrics', '--path', path.to_s, '--json')
53
+ # --no-history: a dashboard READ must never write .tina4-metrics.json.
54
+ # Without it every scan records history, the file watcher sees the write
55
+ # and reloads the page, which re-scans - a self-inflicted ~2-second reload
56
+ # loop that makes the dashboard unusable.
57
+ stdout, stderr, status = Open3.capture3(binary, 'metrics', '--path', path.to_s, '--json', '--no-history')
54
58
  unless status.success?
55
59
  detail = (stderr.empty? ? stdout : stderr).strip.lines.first || "exit code #{status.exitstatus}"
56
60
  raise MetricsEngineError, "tina4 metrics failed on #{path}: #{detail.strip}"
data/lib/tina4/plan.rb CHANGED
@@ -407,7 +407,7 @@ module Tina4
407
407
  user_parts << "Extra context from caller: #{prompt}" unless prompt.to_s.empty?
408
408
  user_parts << "Reply with ONLY the JSON array — no explanation, no markdown fences."
409
409
 
410
- ai_url = ENV.fetch("TINA4_AI_URL", "http://localhost:11437/api/chat")
410
+ ai_url = ENV.fetch("TINA4_AI_URL", "http://localhost:11435/api/chat")
411
411
  ai_model = ENV.fetch("TINA4_AI_MODEL", "qwen2.5-coder:14b")
412
412
 
413
413
  reply = begin
@@ -41,8 +41,8 @@
41
41
  },
42
42
  {
43
43
  "path": "js/tina4-dev-admin.min.js",
44
- "component": "tina4-dev-admin-21700e0",
45
- "sha256": "0ca84246c26afb6a3243aafd1c65fa9486c076039e5d721b6f44661b3a59d7c2"
44
+ "component": "tina4-dev-admin-8c4617d",
45
+ "sha256": "6b05be06b5d76ee4126a11f019d104d3920c1e3234250cfe1966a33f34c1a234"
46
46
  }
47
47
  ]
48
48
  }
@@ -0,0 +1,202 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Tina4 Dev - Split</title>
3
+ <style>
4
+ html,body{margin:0;height:100%;background:#0f172a;font-family:system-ui,-apple-system,sans-serif;overflow:hidden}
5
+ /* One top toolbar controls both panels and the preview device. */
6
+ .t4-topbar{height:40px;box-sizing:border-box;display:flex;align-items:center;gap:6px;padding:0 10px;background:#0b1220;border-bottom:1px solid #1f2937}
7
+ .t4-brand{color:#e2e8f0;font:600 12px system-ui;margin-right:6px}
8
+ .t4-spacer{flex:1 1 auto}
9
+ .t4-devbtn{background:#1e293b;color:#cbd5e1;border:1px solid #334155;border-radius:6px;padding:3px 9px;font:12px system-ui;cursor:pointer}
10
+ .t4-devbtn.active{background:#2563eb;border-color:#2563eb;color:#fff}
11
+ /* Icon toggle buttons: full opacity + highlight when the panel is shown,
12
+ dimmed when hidden. */
13
+ .t4-tg{display:inline-flex;align-items:center;justify-content:center;width:28px;height:26px;background:#1e293b;color:#94a3b8;border:1px solid #334155;border-radius:6px;cursor:pointer;opacity:.5}
14
+ .t4-tg svg{width:15px;height:15px}
15
+ .t4-tg.active{background:#334155;color:#fff;opacity:1}
16
+ .t4-dim{color:#64748b;font:11px/1 ui-monospace,monospace;margin:0 6px}
17
+ .t4-popout{background:#0f766e;color:#fff;border:1px solid #0f766e;border-radius:6px;padding:3px 10px;font:12px system-ui;cursor:pointer;margin-left:6px}
18
+ .t4-split{display:flex;height:calc(100vh - 41px);width:100vw}
19
+ .t4-left{flex:0 0 auto;width:58%;min-width:0;height:100%;overflow:hidden}
20
+ .t4-left iframe{width:100%;height:100%;border:0;display:block}
21
+ .t4-gutter{flex:0 0 6px;cursor:col-resize;background:#1f2937}
22
+ .t4-gutter:hover,.t4-gutter.drag{background:#2563eb}
23
+ .t4-right{flex:1 1 auto;min-width:0;height:100%;overflow:hidden;display:flex;flex-direction:column}
24
+ /* A LIGHT backdrop so the dark device frame stands out. */
25
+ .t4-stage{flex:1 1 auto;min-width:0;min-height:0;position:relative;overflow:hidden;display:flex;align-items:center;justify-content:center;background:radial-gradient(circle at 50% 32%,#f8fafc,#c3ccd9)}
26
+ /* The device frame - a phone/tablet bezel around the live app. overflow:hidden
27
+ + border-radius CLIP the app to the rounded frame, so content is contained.
28
+ No width/height transition: fit() measures right after a toggle, and an
29
+ animated size would be read mid-transition (the "clicking does nothing" bug). */
30
+ .t4-device{position:relative;background:#111;box-shadow:0 16px 50px rgba(15,23,42,.45);transform-origin:center center;overflow:hidden}
31
+ .t4-device iframe{width:100%;height:100%;border:0;display:block;background:#fff}
32
+ .t4-device.mobile{border:14px solid #111;border-radius:46px}
33
+ .t4-device.tablet{border:16px solid #111;border-radius:26px}
34
+ /* Desktop fills the whole preview block - no bezel, no scaling. */
35
+ .t4-device.desktop{border:0;border-radius:0;box-shadow:none;width:100%;height:100%}
36
+ .t4-hidden{display:none !important}
37
+ /* While dragging the gutter, let mouse events pass over the iframes. */
38
+ body.t4-dragging iframe{pointer-events:none}
39
+ </style></head>
40
+ <body>
41
+ <div class="t4-topbar">
42
+ <span class="t4-brand">Tina4 Dev</span>
43
+ <button class="t4-tg active" id="t4-tg-files" title="Show/hide the file navigator"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4"><rect x="1.5" y="2.5" width="13" height="11" rx="1.5"/><line x1="6" y1="2.5" x2="6" y2="13.5"/></svg></button>
44
+ <button class="t4-tg active" id="t4-tg-editor" title="Show/hide the code editor"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="5.5,4.5 2,8 5.5,11.5"/><polyline points="10.5,4.5 14,8 10.5,11.5"/></svg></button>
45
+ <button class="t4-tg active" id="t4-tg-preview" title="Show/hide the preview"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"><rect x="1.5" y="2.5" width="13" height="9" rx="1.5"/><line x1="5.5" y1="14" x2="10.5" y2="14"/><line x1="8" y1="11.5" x2="8" y2="14"/></svg></button>
46
+ <span class="t4-spacer"></span>
47
+ <span class="t4-dim" id="t4-dim">1440 x 900</span>
48
+ <button class="t4-devbtn" data-mode="mobile">Mobile</button>
49
+ <button class="t4-devbtn" data-mode="tablet">Tablet</button>
50
+ <button class="t4-devbtn active" data-mode="desktop">Desktop</button>
51
+ <button class="t4-popout" id="t4-popout" title="Open this view in its own window - drag it to another screen">&#8689; Pop out</button>
52
+ </div>
53
+ <div class="t4-split" id="t4-split">
54
+ <div class="t4-left" id="t4-left"><iframe src="/__dev" title="Tina4 Dev Admin"></iframe></div>
55
+ <div class="t4-gutter" id="t4-gutter"></div>
56
+ <div class="t4-right" id="t4-right">
57
+ <div class="t4-stage" id="t4-stage">
58
+ <div class="t4-device desktop" id="t4-device">
59
+ <iframe id="t4-preview" src="/" title="Live preview"></iframe>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ <script>
65
+ (function () {
66
+ var SIZES = { mobile: {w:390,h:844}, tablet: {w:820,h:1180}, desktop: {w:1440,h:900} };
67
+ var KEY = 'tina4_split_device';
68
+ var device = document.getElementById('t4-device');
69
+ var stage = document.getElementById('t4-stage');
70
+ var dim = document.getElementById('t4-dim');
71
+ var left = document.getElementById('t4-left');
72
+ var right = document.getElementById('t4-right');
73
+ var gutter = document.getElementById('t4-gutter');
74
+ var split = document.getElementById('t4-split');
75
+ var mode = 'desktop';
76
+ try { var saved = localStorage.getItem(KEY); if (saved && SIZES[saved]) mode = saved; } catch (_) {}
77
+ var leftWidth = '58%';
78
+
79
+ // ── device preview ──
80
+ function fit() {
81
+ if (right.classList.contains('t4-hidden')) return;
82
+ if (mode === 'desktop') {
83
+ // Desktop just fills the preview block - responsive, no frame, no scale.
84
+ device.style.transform = 'none';
85
+ device.style.width = '100%';
86
+ device.style.height = '100%';
87
+ var r0 = stage.getBoundingClientRect();
88
+ if (dim) dim.textContent = Math.round(r0.width) + ' x ' + Math.round(r0.height);
89
+ return;
90
+ }
91
+ var s = SIZES[mode];
92
+ device.style.transform = 'none';
93
+ device.style.width = s.w + 'px';
94
+ device.style.height = s.h + 'px';
95
+ var rect = stage.getBoundingClientRect();
96
+ var pad = 28;
97
+ var ow = device.offsetWidth, oh = device.offsetHeight;
98
+ var scale = Math.min((rect.width - pad) / ow, (rect.height - pad) / oh, 1);
99
+ if (!isFinite(scale) || scale <= 0) scale = 1;
100
+ device.style.transform = 'scale(' + scale + ')';
101
+ }
102
+ function setMode(m) {
103
+ if (!SIZES[m]) return;
104
+ mode = m;
105
+ device.className = 't4-device ' + m;
106
+ if (dim) dim.textContent = SIZES[m].w + ' x ' + SIZES[m].h;
107
+ document.querySelectorAll('.t4-devbtn').forEach(function (b) {
108
+ b.classList.toggle('active', b.getAttribute('data-mode') === m);
109
+ });
110
+ try { localStorage.setItem(KEY, m); } catch (_) {}
111
+ fit();
112
+ }
113
+ document.querySelectorAll('.t4-devbtn').forEach(function (b) {
114
+ b.addEventListener('click', function () { setMode(b.getAttribute('data-mode')); });
115
+ });
116
+ var pop = document.getElementById('t4-popout');
117
+ if (pop) pop.addEventListener('click', function () {
118
+ var s = SIZES[mode];
119
+ var w = Math.min(s.w, (screen.availWidth || 1920) - 40);
120
+ var h = Math.min(s.h, (screen.availHeight || 1080) - 120);
121
+ window.open('/', 'tina4preview_' + mode, 'width=' + w + ',height=' + h + ',menubar=no,toolbar=no,location=no');
122
+ });
123
+
124
+ // ── show / hide each panel (Files, Editor, Preview) ──
125
+ // Files (the nav) and Editor (the code area) are SEPARATE sections living
126
+ // inside the left /__dev SPA iframe, so hiding the editor must not take the
127
+ // file nav with it. They are same-origin, so we reach into the iframe and
128
+ // toggle its own `#editor-sidebar` (nav) and `.editor-main` (editor). The
129
+ // Preview is the right pane of this page.
130
+ var vis = { files: true, editor: true, preview: true };
131
+ function frameDoc() {
132
+ try { return document.querySelector('#t4-left iframe').contentDocument; } catch (_) { return null; }
133
+ }
134
+ function setFrameEl(sel, show) {
135
+ var d = frameDoc(); if (!d) return;
136
+ var el = d.querySelector(sel); if (el) el.style.display = show ? '' : 'none';
137
+ }
138
+ function applyPanels() {
139
+ setFrameEl('#editor-sidebar', vis.files);
140
+ setFrameEl('#editor-splitter-left', vis.files && vis.editor);
141
+ setFrameEl('.editor-main', vis.editor);
142
+ var leftEmpty = !vis.files && !vis.editor;
143
+ left.classList.toggle('t4-hidden', leftEmpty);
144
+ right.classList.toggle('t4-hidden', !vis.preview);
145
+ gutter.classList.toggle('t4-hidden', leftEmpty || !vis.preview);
146
+ if (!vis.preview) {
147
+ // Preview hidden: the editor/nav fills the whole width.
148
+ left.style.flex = '1 1 auto'; left.style.width = 'auto';
149
+ } else if (vis.files && !vis.editor) {
150
+ // Only the file nav is shown: shrink the left pane to the nav width so
151
+ // there is no empty gap where the editor used to be.
152
+ left.style.flex = '0 0 auto'; left.style.width = '250px';
153
+ } else {
154
+ left.style.flex = '0 0 auto'; left.style.width = leftWidth;
155
+ }
156
+ document.getElementById('t4-tg-files').classList.toggle('active', vis.files);
157
+ document.getElementById('t4-tg-editor').classList.toggle('active', vis.editor);
158
+ document.getElementById('t4-tg-preview').classList.toggle('active', vis.preview);
159
+ fit();
160
+ }
161
+ function toggle(name) {
162
+ // Never hide all three - at least one stays visible.
163
+ var n = { files: vis.files, editor: vis.editor, preview: vis.preview };
164
+ n[name] = !n[name];
165
+ if (!n.files && !n.editor && !n.preview) return;
166
+ vis[name] = !vis[name];
167
+ applyPanels();
168
+ }
169
+ document.getElementById('t4-tg-files').addEventListener('click', function () { toggle('files'); });
170
+ document.getElementById('t4-tg-editor').addEventListener('click', function () { toggle('editor'); });
171
+ document.getElementById('t4-tg-preview').addEventListener('click', function () { toggle('preview'); });
172
+
173
+ // ── drag the gutter to resize ──
174
+ var dragging = false;
175
+ gutter.addEventListener('mousedown', function (e) {
176
+ dragging = true;
177
+ document.body.classList.add('t4-dragging');
178
+ gutter.classList.add('drag');
179
+ e.preventDefault();
180
+ });
181
+ window.addEventListener('mousemove', function (e) {
182
+ if (!dragging) return;
183
+ var r = split.getBoundingClientRect();
184
+ var w = e.clientX - r.left;
185
+ w = Math.max(220, Math.min(w, r.width - 260));
186
+ leftWidth = w + 'px';
187
+ left.style.width = leftWidth;
188
+ fit();
189
+ });
190
+ window.addEventListener('mouseup', function () {
191
+ if (!dragging) return;
192
+ dragging = false;
193
+ document.body.classList.remove('t4-dragging');
194
+ gutter.classList.remove('drag');
195
+ });
196
+
197
+ window.addEventListener('resize', fit);
198
+ setMode(mode);
199
+ applyPanels();
200
+ })();
201
+ </script>
202
+ </body></html>