tina4ruby 3.13.111 → 3.13.112

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: fc71b0c82d6e81e389c825ceaea7a266203979977c2215af5efe677c2aabc57d
4
- data.tar.gz: 9022b81347b8d00a6c95c6f24edeef3e28d528018a6b8a0afd446b3da877d77e
3
+ metadata.gz: 0c0ba91f4e37ef96e557eb291a221cf419821ad59a00bad3e4fe1dbbfaaa5a97
4
+ data.tar.gz: c54f721f615114967ed5f4e6e86b1c3573da6fb85dcf4fcc9003f0840608b848
5
5
  SHA512:
6
- metadata.gz: 1690d686f268e05963e12078a6097bb02e3c920cef1a0a9e063da928bca30b37a58f507a7ab151be65a64bed2e482b9c61c902b2eb84e14e835c04a5e5b966ec
7
- data.tar.gz: 32929f5560047f02f29e0f75a4585704fc6ccdf9b74bcce3abb664c8a5fb5b55ba2c78c930e0449f7e8cba1c700b200c2294e863c70186d76b9cc80520e220a1
6
+ metadata.gz: 1ada54d7fac14bbfec973040294322d70155c99024c071a4d5a9b558f0ab16d69bf2c57020e92db43ca70a1eeb00099944ddf6d6cf40268ba51f755621426e5c
7
+ data.tar.gz: b95811c10bf0db5264f304e19ade1f6849f790c4eec98c4106ed2c7a4201380492082fc4a67ef32fd517e0ad58997a7c3888d49e37e79c845e2d2c92c6fa40d4
@@ -407,6 +407,16 @@ module Tina4
407
407
  serve_dashboard
408
408
  when ["GET", "/__dev/js/tina4-dev-admin.min.js"]
409
409
  serve_dev_js
410
+ when ["GET", "/__dev/toolbar.css"]
411
+ # tina4stack #115: the injected toolbar's stylesheet, served as an
412
+ # external asset so the toolbar carries no inline style= and renders
413
+ # under the default default-src 'self' CSP. Parity with PHP/Python/Node.
414
+ [200, { "content-type" => "text/css; charset=utf-8" }, [Tina4::RackApp.toolbar_css]]
415
+ when ["GET", "/__dev/toolbar.js"]
416
+ # tina4stack #115: the injected toolbar's script (version modal, dash
417
+ # overlay, WebSocket-primary reloader), served as an external asset so
418
+ # the toolbar carries no inline <script>/onclick and stays CSP-clean.
419
+ [200, { "content-type" => "application/javascript; charset=utf-8" }, [Tina4::RackApp.toolbar_js]]
410
420
  when ["GET", "/__dev/api/mtime"]
411
421
  json_response({ mtime: @reload_mtime || 0, file: @reload_file || "" })
412
422
  when ["POST", "/__dev/api/reload"]
@@ -915,64 +915,174 @@ module Tina4
915
915
  [-1, {}, []]
916
916
  end
917
917
 
918
+ # Inject the CSP-clean dev toolbar into HTML responses in dev mode.
919
+ #
920
+ # tina4stack #115: the toolbar is CSP-clean - it carries NO inline `style=`,
921
+ # NO `onclick=` (or any inline handler) and NO inline `<script>` block. All
922
+ # styling lives in the external `/__dev/toolbar.css` stylesheet and every
923
+ # interaction is wired via `addEventListener` in the external
924
+ # `/__dev/toolbar.js`, so the toolbar renders under the framework's default
925
+ # `default-src 'self'` CSP with no violations. Parity with PHP/Python/Node,
926
+ # which serve the same external `/__dev/toolbar.{css,js}` assets.
918
927
  def inject_dev_overlay(body, request_info, ai_port: false)
919
928
  version = Tina4::VERSION
920
- # DEVADMIN-DEC-04 (feature 127): the toolbar is injected into EVERY
921
- # text/html response (including 404s), so the reflected request
922
- # method/path/matched-pattern MUST be HTML-escaped or a crafted path
923
- # reflects <script> that runs in the dev-server origin and can then drive
924
- # every ungated /__dev mutation route. (Parity with PHP htmlspecialchars
925
- # and the Python master html.escape; Ruby was reflecting them raw.)
929
+ # DEVADMIN-DEC-04 (feature 127): the toolbar rides EVERY text/html response
930
+ # (including 404s), so the reflected request method/path/matched-pattern
931
+ # MUST be HTML-escaped or a crafted path reflects <script> that runs in the
932
+ # dev-server origin and can then drive every ungated /__dev mutation route.
933
+ # (Parity with PHP htmlspecialchars and the Python master html.escape.)
926
934
  method = CGI.escapeHTML(request_info[:method].to_s)
927
935
  path = CGI.escapeHTML(request_info[:path].to_s)
928
936
  matched_pattern = CGI.escapeHTML(request_info[:matched_pattern].to_s)
929
- request_id = Tina4::Log.get_request_id || "-"
937
+ request_id = CGI.escapeHTML((Tina4::Log.get_request_id || "-").to_s)
930
938
  route_count = Tina4::Router.routes.length
939
+ ruby_version = RUBY_VERSION
931
940
 
932
- ai_badge = ai_port ? '<span style="background:#7c3aed;color:#fff;font-size:10px;padding:1px 6px;border-radius:3px;font-weight:bold;">AI PORT</span>' : ""
941
+ # data-reload gates the live reloader in toolbar.js: "1" on the human dev
942
+ # port, "0" on the AI/stable port (the JS early-returns before starting the
943
+ # reloader when it is not "1"). Matches Python's render_dev_toolbar.
944
+ reload = ai_port ? "0" : "1"
945
+ ai_badge = ai_port ? '<span class="t4-ai-badge">AI PORT</span>' : ""
933
946
 
934
947
  toolbar = <<~HTML.strip
935
- <div id="tina4-dev-toolbar" style="position:fixed;bottom:0;left:0;right:0;background:#333;color:#fff;font-family:monospace;font-size:12px;padding:6px 16px;z-index:99999;display:flex;align-items:center;gap:16px;">
936
- #{ai_badge}<span id="tina4-ver-btn" style="color:#d32f2f;font-weight:bold;cursor:pointer;text-decoration:underline dotted;" onclick="tina4VersionModal()" title="Click to check for updates">Tina4 v#{version}</span>
937
- <div id="tina4-ver-modal" style="display:none;position:fixed;bottom:3rem;left:1rem;background:#1e1e2e;border:1px solid #d32f2f;border-radius:8px;padding:16px 20px;z-index:100000;min-width:320px;box-shadow:0 8px 32px rgba(0,0,0,0.5);font-family:monospace;font-size:13px;color:#cdd6f4;">
938
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
939
- <strong style="color:#89b4fa;">Version Info</strong>
940
- <span onclick="document.getElementById('tina4-ver-modal').style.display='none'" style="cursor:pointer;color:#888;">&times;</span>
948
+ <link rel="stylesheet" href="/__dev/toolbar.css">
949
+ <div id="tina4-dev-toolbar" data-reload="#{reload}">
950
+ #{ai_badge}<span id="tina4-ver-btn" title="Click to check for updates">Tina4 v#{version}</span>
951
+ <div id="tina4-ver-modal">
952
+ <div class="t4-modal-head">
953
+ <strong class="t4-modal-title">Version Info</strong>
954
+ <span id="tina4-ver-close" class="t4-x">&times;</span>
941
955
  </div>
942
- <div id="tina4-ver-body" style="line-height:1.8;">
943
- <div>Current: <strong style="color:#a6e3a1;">v#{version}</strong></div>
944
- <div id="tina4-ver-latest" style="color:#888;">Checking for updates...</div>
956
+ <div id="tina4-ver-body">
957
+ <div>Current: <strong class="t4-ok">v#{version}</strong></div>
958
+ <div id="tina4-ver-latest" class="t4-dim">Checking for updates...</div>
945
959
  </div>
946
960
  </div>
947
- <span style="color:#4caf50;">#{method}</span>
961
+ <span class="t4-green">#{method}</span>
948
962
  <span>#{path}</span>
949
- <span style="color:#666;">&rarr; #{matched_pattern}</span>
950
- <span style="color:#ffeb3b;">req:#{request_id}</span>
951
- <span style="color:#90caf9;">#{route_count} routes</span>
952
- <span style="color:#888;">Ruby #{RUBY_VERSION}</span>
953
- <a href="#" onclick="window.__tina4ToggleOverlay(event)" style="color:#ef9a9a;margin-left:auto;text-decoration:none;cursor:pointer;">Dashboard &#8599;</a>
954
- <span onclick="this.parentElement.style.display='none'" style="cursor:pointer;color:#888;margin-left:8px;">&#10005;</span>
963
+ <span class="t4-arrow">&rarr; #{matched_pattern}</span>
964
+ <span class="t4-yellow">req:#{request_id}</span>
965
+ <span class="t4-blue">#{route_count} routes</span>
966
+ <span class="t4-dim">Ruby #{ruby_version}</span>
967
+ <a href="#" id="tina4-dash-link" class="t4-dash">Dashboard &#8599;</a>
968
+ <span id="tina4-bar-close" class="t4-x t4-bar-close">&#10005;</span>
955
969
  </div>
956
- <script>
957
- // Overlay open/toggle helper + auto-restore. Persist the dev-admin
958
- // iframe's open/closed state across parent reloads so saving a
959
- // file doesn't lose the user's dev-admin context. Cross-framework
960
- // parity with PHP / Python / Node — same localStorage key.
961
- (function(){
970
+ <script src="/__dev/toolbar.js"></script>
971
+ HTML
972
+
973
+ if body.include?("</body>")
974
+ body.sub("</body>", "#{toolbar}\n</body>")
975
+ else
976
+ body + "\n" + toolbar
977
+ end
978
+ end
979
+
980
+ # CSS for the injected dev toolbar. Served as an external stylesheet (see
981
+ # DevAdmin.handle_request) so the toolbar carries no inline `style=` and
982
+ # stays CSP-clean under a strict `default-src 'self'`.
983
+ def self.toolbar_css
984
+ <<~CSS
985
+ #tina4-dev-toolbar{position:fixed;bottom:0;left:0;right:0;background:#333;color:#fff;font-family:monospace;font-size:12px;padding:6px 16px;z-index:99999;display:flex;align-items:center;gap:16px}
986
+ #tina4-dev-toolbar a{text-decoration:none}
987
+ .t4-ai-badge{background:#7c3aed;color:#fff;font-size:10px;padding:1px 6px;border-radius:3px;font-weight:bold}
988
+ #tina4-ver-btn{color:#d32f2f;font-weight:bold;cursor:pointer;text-decoration:underline dotted}
989
+ #tina4-ver-modal{display:none;position:fixed;bottom:3rem;left:1rem;background:#1e1e2e;border:1px solid #d32f2f;border-radius:8px;padding:16px 20px;z-index:100000;min-width:320px;box-shadow:0 8px 32px rgba(0,0,0,.5);font-family:monospace;font-size:13px;color:#cdd6f4}
990
+ .t4-modal-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}
991
+ .t4-modal-title{color:#89b4fa}
992
+ #tina4-ver-body{line-height:1.8}
993
+ .t4-x{cursor:pointer;color:#888}
994
+ .t4-bar-close{margin-left:8px}
995
+ .t4-green{color:#4caf50}
996
+ .t4-dim{color:#888}
997
+ .t4-arrow{color:#666}
998
+ .t4-yellow{color:#ffeb3b}
999
+ .t4-blue{color:#90caf9}
1000
+ .t4-ok{color:#a6e3a1}
1001
+ .t4-warn{color:#f9e2af}
1002
+ .t4-err{color:#f38ba8}
1003
+ .t4-purple{color:#cba6f7}
1004
+ .t4-link{color:#89b4fa}
1005
+ .t4-code{background:#313244;padding:2px 6px;border-radius:3px}
1006
+ .t4-note{margin-top:6px}
1007
+ .t4-dash{color:#ef9a9a;margin-left:auto;cursor:pointer}
1008
+ #tina4-dev-panel{position:fixed;top:3rem;left:0;right:0;bottom:2rem;z-index:99998;transition:all .2s}
1009
+ #tina4-dev-panel iframe{width:100%;height:100%;border:1px solid #CC342D;border-radius:.5rem;box-shadow:0 8px 32px rgba(0,0,0,.5);background:#0f172a}
1010
+ CSS
1011
+ end
1012
+
1013
+ # JS for the injected dev toolbar - the version-check modal, the dashboard
1014
+ # overlay, and the WebSocket-primary live reloader. Served as an external
1015
+ # script (see DevAdmin.handle_request) so the toolbar carries no inline
1016
+ # handlers or `<script>` and stays CSP-clean. Every handler is wired via
1017
+ # addEventListener; the reloader only starts when the toolbar's `data-reload`
1018
+ # attribute is "1" (reload not suppressed for this request). Mirrors the
1019
+ # Python master's injected client and the PHP external toolbar.js exactly.
1020
+ def self.toolbar_js
1021
+ poll_interval_ms = (ENV["TINA4_DEV_POLL_INTERVAL"] || "3000").to_i
1022
+ poll_interval_ms = 3000 if poll_interval_ms <= 0
1023
+ <<~JS
1024
+ (function () {
1025
+ var bar = document.getElementById('tina4-dev-toolbar');
1026
+ if (!bar) { return; }
1027
+
1028
+ var modal = document.getElementById('tina4-ver-modal');
1029
+ function upToDate(el, latest) {
1030
+ el.className = 't4-ok';
1031
+ el.innerHTML = 'Latest: <strong class="t4-ok">v' + latest + '</strong> &mdash; You are up to date!';
1032
+ }
1033
+ function checkVersion() {
1034
+ if (modal.style.display === 'block') { modal.style.display = 'none'; return; }
1035
+ modal.style.display = 'block';
1036
+ var el = document.getElementById('tina4-ver-latest');
1037
+ el.className = 't4-dim';
1038
+ el.textContent = 'Checking for updates...';
1039
+ fetch('/__dev/api/version-check').then(function (r) { return r.json(); }).then(function (d) {
1040
+ var latest = d.latest, current = d.current;
1041
+ if (latest === current) { upToDate(el, latest); return; }
1042
+ var cP = current.split('.').map(Number), lP = latest.split('.').map(Number);
1043
+ var isNewer = false, i, c, l;
1044
+ for (i = 0; i < Math.max(cP.length, lP.length); i++) { c = cP[i] || 0; l = lP[i] || 0; if (l > c) { isNewer = true; break; } if (l < c) { break; } }
1045
+ var isAhead = false;
1046
+ if (!isNewer) { for (i = 0; i < Math.max(cP.length, lP.length); i++) { var c2 = cP[i] || 0, l2 = lP[i] || 0; if (c2 > l2) { isAhead = true; break; } if (c2 < l2) { break; } } }
1047
+ if (isNewer) {
1048
+ var breaking = (lP[0] !== cP[0] || lP[1] !== cP[1]);
1049
+ el.className = '';
1050
+ el.innerHTML = 'Latest: <strong class="t4-warn">v' + latest + '</strong>';
1051
+ if (breaking) {
1052
+ el.innerHTML += '<div class="t4-err t4-note">&#9888; Major/minor version change &mdash; check the <a href="https://github.com/tina4stack/tina4-ruby/releases" target="_blank" class="t4-link">changelog</a> for breaking changes before upgrading.</div>';
1053
+ } else {
1054
+ el.innerHTML += '<div class="t4-warn t4-note">Patch update available. Run: <code class="t4-code">gem install tina4ruby</code></div>';
1055
+ }
1056
+ } else if (isAhead) {
1057
+ el.className = 't4-purple';
1058
+ el.innerHTML = 'You are running <strong class="t4-purple">v' + current + '</strong> (ahead of RubyGems <strong>v' + latest + '</strong> &mdash; not yet published).';
1059
+ } else {
1060
+ upToDate(el, latest);
1061
+ }
1062
+ }).catch(function () {
1063
+ el.className = 't4-err';
1064
+ el.textContent = 'Could not check for updates (offline?)';
1065
+ });
1066
+ }
1067
+ var verBtn = document.getElementById('tina4-ver-btn');
1068
+ if (verBtn) { verBtn.addEventListener('click', checkVersion); }
1069
+ var verClose = document.getElementById('tina4-ver-close');
1070
+ if (verClose) { verClose.addEventListener('click', function () { modal.style.display = 'none'; }); }
1071
+ var barClose = document.getElementById('tina4-bar-close');
1072
+ if (barClose) { barClose.addEventListener('click', function () { bar.style.display = 'none'; }); }
1073
+
962
1074
  var STATE_KEY = 'tina4_dev_overlay_open';
963
1075
  function buildOverlay() {
964
1076
  var c = document.createElement('div');
965
1077
  c.id = 'tina4-dev-panel';
966
- c.style.cssText = 'position:fixed;top:3rem;left:0;right:0;bottom:2rem;z-index:99998;transition:all 0.2s';
967
1078
  var f = document.createElement('iframe');
968
1079
  f.src = '/__dev';
969
- f.style.cssText = 'width:100%;height:100%;border:1px solid #CC342D;border-radius:0.5rem;box-shadow:0 8px 32px rgba(0,0,0,0.5);background:#0f172a';
970
1080
  c.appendChild(f);
971
1081
  document.body.appendChild(c);
972
1082
  return c;
973
1083
  }
974
- window.__tina4ToggleOverlay = function(e) {
975
- if (e) e.preventDefault();
1084
+ function toggleOverlay(e) {
1085
+ if (e) { e.preventDefault(); }
976
1086
  var p = document.getElementById('tina4-dev-panel');
977
1087
  if (p) {
978
1088
  var hide = p.style.display !== 'none';
@@ -982,151 +1092,58 @@ module Tina4
982
1092
  }
983
1093
  buildOverlay();
984
1094
  try { localStorage.setItem(STATE_KEY, '1'); } catch (_) {}
985
- };
986
- function restoreIfOpen() {
987
- try {
988
- if (location.pathname.indexOf('/__dev') === 0) return;
989
- if (localStorage.getItem(STATE_KEY) === '1' && !document.getElementById('tina4-dev-panel')) {
990
- buildOverlay();
991
- }
992
- } catch (_) {}
993
- }
994
- if (document.readyState === 'loading') {
995
- document.addEventListener('DOMContentLoaded', restoreIfOpen);
996
- } else {
997
- restoreIfOpen();
998
1095
  }
999
- })();
1000
- </script>
1001
- <script>
1002
- function tina4VersionModal(){
1003
- var m=document.getElementById('tina4-ver-modal');
1004
- if(m.style.display==='block'){m.style.display='none';return;}
1005
- m.style.display='block';
1006
- var el=document.getElementById('tina4-ver-latest');
1007
- el.innerHTML='Checking for updates...';
1008
- el.style.color='#888';
1009
- fetch('/__dev/api/version-check')
1010
- .then(function(r){return r.json()})
1011
- .then(function(d){
1012
- var latest=d.latest;
1013
- var current=d.current;
1014
- if(latest===current){
1015
- el.innerHTML='Latest: <strong style="color:#a6e3a1;">v'+latest+'</strong> &mdash; You are up to date!';
1016
- el.style.color='#a6e3a1';
1017
- }else{
1018
- var cParts=current.split('.').map(Number);
1019
- var lParts=latest.split('.').map(Number);
1020
- var isNewer=false;
1021
- for(var i=0;i<Math.max(cParts.length,lParts.length);i++){
1022
- var c=cParts[i]||0,l=lParts[i]||0;
1023
- if(l>c){isNewer=true;break;}
1024
- if(l<c)break;
1025
- }
1026
- var isAhead=false;
1027
- if(!isNewer){for(var i=0;i<Math.max(cParts.length,lParts.length);i++){var c2=cParts[i]||0,l2=lParts[i]||0;if(c2>l2){isAhead=true;break;}if(c2<l2)break;}}
1028
- if(isNewer){
1029
- var breaking=(lParts[0]!==cParts[0]||lParts[1]!==cParts[1]);
1030
- el.innerHTML='Latest: <strong style="color:#f9e2af;">v'+latest+'</strong>';
1031
- if(breaking){
1032
- el.innerHTML+='<div style="color:#f38ba8;margin-top:6px;">&#9888; Major/minor version change &mdash; check the <a href="https://github.com/tina4stack/tina4-ruby/releases" target="_blank" style="color:#89b4fa;">changelog</a> for breaking changes before upgrading.</div>';
1033
- }else{
1034
- el.innerHTML+='<div style="color:#f9e2af;margin-top:6px;">Patch update available. Run: <code style="background:#313244;padding:2px 6px;border-radius:3px;">gem install tina4ruby</code></div>';
1035
- }
1036
- }else if(isAhead){
1037
- el.innerHTML='You are running <strong style="color:#cba6f7;">v'+current+'</strong> (ahead of RubyGems <strong>v'+latest+'</strong> &mdash; not yet published).';
1038
- el.style.color='#cba6f7';
1039
- }else{
1040
- el.innerHTML='Latest: <strong style="color:#a6e3a1;">v'+latest+'</strong> &mdash; You are up to date!';
1041
- el.style.color='#a6e3a1';
1042
- }
1096
+ var dash = document.getElementById('tina4-dash-link');
1097
+ if (dash) { dash.addEventListener('click', toggleOverlay); }
1098
+ try {
1099
+ if (location.pathname.indexOf('/__dev') !== 0
1100
+ && localStorage.getItem(STATE_KEY) === '1'
1101
+ && !document.getElementById('tina4-dev-panel')) {
1102
+ buildOverlay();
1043
1103
  }
1044
- })
1045
- .catch(function(){
1046
- el.innerHTML='Could not check for updates (offline?)';
1047
- el.style.color='#f38ba8';
1048
- });
1049
- }
1050
- #{ai_port ? "" : dev_reload_client_js}
1051
- </script>
1052
- HTML
1053
-
1054
- if body.include?("</body>")
1055
- body.sub("</body>", "#{toolbar}\n</body>")
1056
- else
1057
- body + "\n" + toolbar
1058
- end
1059
- end
1060
-
1061
- # WebSocket-primary dev reloader injected into HTML pages in debug mode.
1062
- #
1063
- # The running server re-imports changed src/ route files in-process and
1064
- # pushes a {type,file,mtime} message over /__dev_reload — no respawn,
1065
- # instant refresh. The mtime poll is a FALLBACK only: it is started when
1066
- # the socket is down and stopped the moment it connects. On a CSS change
1067
- # the client swaps <link rel=stylesheet> hrefs with a cache-bust query;
1068
- # any other change does a full location.reload(). The poll seeds its
1069
- # last-seen mtime to a null sentinel (NOT 0) and reloads whenever the
1070
- # polled mtime DIFFERS (not just when greater) so the first change after
1071
- # load isn't swallowed and a counter reset on restart still triggers.
1072
- # Mirrors the Python master's injected client exactly.
1073
- def dev_reload_client_js
1074
- poll_interval_ms = (ENV["TINA4_DEV_POLL_INTERVAL"] || "3000").to_i
1075
- poll_interval_ms = 3000 if poll_interval_ms <= 0
1076
- <<~JS
1077
- (function(){
1078
- var _t4_css_exts=['.css','.scss'],_t4_debounce=null;
1079
- var _t4_interval=parseInt('#{poll_interval_ms}')||3000;
1080
- var _t4_ws=null,_t4_poll_timer=null,_t4_mtime=null;
1081
- function _t4_apply(d){
1082
- d=d||{};
1083
- var f=d.file||'',t=d.type||'';
1084
- var isCss=t==='css'||_t4_css_exts.some(function(e){return f.endsWith(e)});
1085
- if(isCss){
1086
- var links=document.querySelectorAll('link[rel="stylesheet"]');
1087
- links.forEach(function(l){
1088
- var href=l.getAttribute('href');
1089
- if(href){l.setAttribute('href',href.split('?')[0]+'?_t4='+(d.mtime||Date.now()))}
1104
+ } catch (_) {}
1105
+
1106
+ if (bar.getAttribute('data-reload') !== '1') { return; }
1107
+ var cssExts = ['.css', '.scss'], debounce = null, interval = #{poll_interval_ms};
1108
+ var ws = null, pollTimer = null, mtime = null;
1109
+ function apply(d) {
1110
+ d = d || {};
1111
+ var f = d.file || '', t = d.type || '';
1112
+ var isCss = t === 'css' || cssExts.some(function (e) { return f.endsWith(e); });
1113
+ if (isCss) {
1114
+ document.querySelectorAll('link[rel="stylesheet"]').forEach(function (l) {
1115
+ var href = l.getAttribute('href');
1116
+ if (href) { l.setAttribute('href', href.split('?')[0] + '?_t4=' + (d.mtime || Date.now())); }
1090
1117
  });
1091
- }else{
1118
+ } else {
1092
1119
  location.reload();
1093
1120
  }
1094
1121
  }
1095
- function _t4_poll(){
1096
- fetch('/__dev/api/mtime').then(function(r){return r.json()}).then(function(d){
1097
- if(_t4_mtime===null){_t4_mtime=d.mtime;return;}
1098
- if(d.mtime!==_t4_mtime){
1099
- _t4_mtime=d.mtime;
1100
- if(_t4_debounce)clearTimeout(_t4_debounce);
1101
- _t4_debounce=setTimeout(function(){_t4_apply(d);},500);
1102
- }
1103
- }).catch(function(){});
1104
- }
1105
- function _t4_startPoll(){
1106
- if(_t4_poll_timer)return;
1107
- _t4_mtime=null;
1108
- _t4_poll_timer=setInterval(_t4_poll,_t4_interval);
1109
- }
1110
- function _t4_stopPoll(){
1111
- if(_t4_poll_timer){clearInterval(_t4_poll_timer);_t4_poll_timer=null;}
1122
+ function poll() {
1123
+ fetch('/__dev/api/mtime').then(function (r) { return r.json(); }).then(function (d) {
1124
+ if (mtime === null) { mtime = d.mtime; return; }
1125
+ if (d.mtime !== mtime) { mtime = d.mtime; if (debounce) { clearTimeout(debounce); } debounce = setTimeout(function () { apply(d); }, 500); }
1126
+ }).catch(function () {});
1112
1127
  }
1113
- function _t4_connect(){
1114
- var url=(location.protocol==='https:'?'wss':'ws')+'://'+location.host+'/__dev_reload';
1115
- try{_t4_ws=new WebSocket(url);}catch(_){_t4_startPoll();return;}
1116
- _t4_ws.addEventListener('open',function(){_t4_stopPoll();});
1117
- _t4_ws.addEventListener('message',function(ev){
1118
- var d=null;
1119
- try{d=typeof ev.data==='string'?JSON.parse(ev.data):null;}catch(_){}
1120
- if(!d)return;
1121
- if(d.type==='reload'||d.type==='change'||d.type==='css'){
1122
- if(_t4_debounce)clearTimeout(_t4_debounce);
1123
- _t4_debounce=setTimeout(function(){_t4_apply(d);},150);
1128
+ function startPoll() { if (pollTimer) { return; } mtime = null; pollTimer = setInterval(poll, interval); }
1129
+ function stopPoll() { if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } }
1130
+ function connect() {
1131
+ var url = (location.protocol === 'https:' ? 'wss' : 'ws') + '://' + location.host + '/__dev_reload';
1132
+ try { ws = new WebSocket(url); } catch (_) { startPoll(); return; }
1133
+ ws.addEventListener('open', function () { stopPoll(); });
1134
+ ws.addEventListener('message', function (ev) {
1135
+ var d = null;
1136
+ try { d = typeof ev.data === 'string' ? JSON.parse(ev.data) : null; } catch (_) {}
1137
+ if (!d) { return; }
1138
+ if (d.type === 'reload' || d.type === 'change' || d.type === 'css') {
1139
+ if (debounce) { clearTimeout(debounce); }
1140
+ debounce = setTimeout(function () { apply(d); }, 150);
1124
1141
  }
1125
1142
  });
1126
- _t4_ws.addEventListener('close',function(){_t4_ws=null;_t4_startPoll();setTimeout(_t4_connect,2000);});
1127
- _t4_ws.addEventListener('error',function(){try{_t4_ws&&_t4_ws.close();}catch(_){}});
1143
+ ws.addEventListener('close', function () { ws = null; startPoll(); setTimeout(connect, 2000); });
1144
+ ws.addEventListener('error', function () { try { ws && ws.close(); } catch (_) {} });
1128
1145
  }
1129
- _t4_connect();
1146
+ connect();
1130
1147
  })();
1131
1148
  JS
1132
1149
  end
data/lib/tina4/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tina4
4
- VERSION = "3.13.111"
4
+ VERSION = "3.13.112"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tina4ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.13.111
4
+ version: 3.13.112
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tina4 Team