tina4ruby 3.11.35 → 3.12.0

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.
@@ -200,7 +200,7 @@ module Tina4
200
200
  end
201
201
 
202
202
  # API_KEY bypass — matches tina4_python behavior
203
- api_key = ENV["TINA4_API_KEY"] || ENV["API_KEY"]
203
+ api_key = ENV["TINA4_API_KEY"]
204
204
  if api_key && !api_key.empty? && token == api_key
205
205
  env["tina4.auth_payload"] = { "api_key" => true }
206
206
  elsif token
@@ -330,18 +330,28 @@ module Tina4
330
330
  end
331
331
 
332
332
  def handle_404(path)
333
- # Try serving a template file (e.g. /hello -> src/templates/hello.twig or hello.html)
333
+ # Try serving a template file (e.g. /hello -> src/templates/pages/hello.twig)
334
334
  template_response = try_serve_template(path)
335
335
  return template_response if template_response
336
336
 
337
- # Show landing page for GET "/"
338
- return render_landing_page if path == "/"
337
+ # Show landing page for GET "/" — but ONLY in dev mode.
338
+ # Production never shows it, so the framework version, dev-admin link,
339
+ # and gallery never leak to real users. Symmetric with /__dev/* gating.
340
+ return render_landing_page if path == "/" && dev_mode?
339
341
 
340
342
  Tina4::Log.warning("404 Not Found: #{path}")
341
343
  body = Tina4::Template.render_error(404, { "path" => path }) rescue "404 Not Found"
342
344
  [404, { "content-type" => "text/html" }, [body]]
343
345
  end
344
346
 
347
+ # Honour TINA4_TEMPLATE_ROUTING=off|false|0|no|disabled as an explicit
348
+ # kill switch. Default: enabled. Drop a file in src/templates/pages/ and
349
+ # it serves at the matching URL — the zero-config Tina4 convention.
350
+ def template_auto_routing_enabled?
351
+ val = ENV.fetch("TINA4_TEMPLATE_ROUTING", "on").to_s.strip.downcase
352
+ !%w[off false 0 no disabled].include?(val)
353
+ end
354
+
345
355
  def should_show_landing_page?
346
356
  # Check if any index template exists in src/templates/
347
357
  templates_dir = File.join(@root_dir, "src", "templates")
@@ -357,19 +367,39 @@ module Tina4
357
367
  [200, { "content-type" => "text/html" }, [body]]
358
368
  end
359
369
 
360
- # Resolve a URL path to a template file.
370
+ # Resolve a URL path to a template file in src/templates/pages/.
371
+ #
372
+ # Only files inside ``src/templates/pages/`` auto-route from a URL.
373
+ # Anything in ``src/templates/`` outside ``pages/`` (partials, layouts,
374
+ # base.twig, errors, components) is never served standalone — those
375
+ # remain renderable via {% include %} / {% extends %} / explicit render
376
+ # calls but never auto-serve from a URL.
377
+ #
378
+ # Files starting with ``_`` (e.g. pages/_helper.twig) are always skipped
379
+ # even within pages/ — Hugo/Jekyll convention for private partials.
380
+ #
361
381
  # Dev mode: checks filesystem every time for live changes.
362
382
  # Production: uses a cached lookup built once at startup.
383
+ #
384
+ # The whole feature can be turned off with ``TINA4_TEMPLATE_ROUTING=off``.
363
385
  def resolve_template(path)
386
+ return nil unless template_auto_routing_enabled?
387
+
364
388
  clean_path = path.sub(%r{^/}, "")
365
389
  clean_path = "index" if clean_path.empty?
390
+
391
+ # Skip underscore-prefixed segments — private files even within pages/.
392
+ return nil if clean_path.split("/").any? { |seg| seg.start_with?("_") }
393
+
366
394
  is_dev = %w[true 1 yes].include?(ENV.fetch("TINA4_DEBUG", "false").downcase)
367
395
 
368
396
  if is_dev
369
- templates_dir = File.join(@root_dir, "src", "templates")
397
+ pages_dir = File.join(@root_dir, "src", "templates", "pages")
370
398
  %w[.twig .html].each do |ext|
371
- candidate = clean_path + ext
372
- return candidate if File.file?(File.join(templates_dir, candidate))
399
+ candidate_inside_pages = clean_path + ext
400
+ if File.file?(File.join(pages_dir, candidate_inside_pages))
401
+ return File.join("pages", candidate_inside_pages)
402
+ end
373
403
  end
374
404
  return nil
375
405
  end
@@ -379,15 +409,22 @@ module Tina4
379
409
  @template_cache[clean_path]
380
410
  end
381
411
 
412
+ # Scan src/templates/pages/ once and build url_path -> template_file lookup.
413
+ # Only files under pages/ are eligible — partials, layouts, base.twig,
414
+ # errors etc. remain renderable via explicit render calls but never
415
+ # auto-serve from a URL.
382
416
  def build_template_cache
383
417
  cache = {}
384
- templates_dir = File.join(@root_dir, "src", "templates")
385
- return cache unless File.directory?(templates_dir)
386
-
387
- Dir.glob(File.join(templates_dir, "**", "*.{twig,html}")).each do |f|
388
- rel = f.sub(templates_dir + File::SEPARATOR, "").tr("\\", "/")
389
- url_path = rel.sub(/\.(twig|html)$/, "")
390
- cache[url_path] ||= rel
418
+ pages_dir = File.join(@root_dir, "src", "templates", "pages")
419
+ return cache unless File.directory?(pages_dir)
420
+
421
+ Dir.glob(File.join(pages_dir, "**", "*.{twig,html}")).each do |f|
422
+ rel_inside_pages = f.sub(pages_dir + File::SEPARATOR, "").tr("\\", "/")
423
+ # Skip private files even within pages/ (e.g. pages/_helper.twig)
424
+ next if rel_inside_pages.split("/").any? { |seg| seg.start_with?("_") }
425
+ url_path = rel_inside_pages.sub(/\.(twig|html)$/, "")
426
+ rel_from_templates = "pages/#{rel_inside_pages}"
427
+ cache[url_path] ||= rel_from_templates
391
428
  end
392
429
  cache
393
430
  end
@@ -714,10 +751,55 @@ module Tina4
714
751
  <span style="color:#ffeb3b;">req:#{request_id}</span>
715
752
  <span style="color:#90caf9;">#{route_count} routes</span>
716
753
  <span style="color:#888;">Ruby #{RUBY_VERSION}</span>
717
- <a href="#" onclick="(function(e){e.preventDefault();var p=document.getElementById('tina4-dev-panel');if(p){p.style.display=p.style.display==='none'?'block':'none';return;}var c=document.createElement('div');c.id='tina4-dev-panel';c.style.cssText='position:fixed;top:3rem;left:0;right:0;bottom:2rem;z-index:99998;transition:all 0.2s';var f=document.createElement('iframe');f.src='/__dev';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';c.appendChild(f);document.body.appendChild(c);})(event)" style="color:#ef9a9a;margin-left:auto;text-decoration:none;cursor:pointer;">Dashboard &#8599;</a>
754
+ <a href="#" onclick="window.__tina4ToggleOverlay(event)" style="color:#ef9a9a;margin-left:auto;text-decoration:none;cursor:pointer;">Dashboard &#8599;</a>
718
755
  <span onclick="this.parentElement.style.display='none'" style="cursor:pointer;color:#888;margin-left:8px;">&#10005;</span>
719
756
  </div>
720
757
  <script>
758
+ // Overlay open/toggle helper + auto-restore. Persist the dev-admin
759
+ // iframe's open/closed state across parent reloads so saving a
760
+ // file doesn't lose the user's dev-admin context. Cross-framework
761
+ // parity with PHP / Python / Node — same localStorage key.
762
+ (function(){
763
+ var STATE_KEY = 'tina4_dev_overlay_open';
764
+ function buildOverlay() {
765
+ var c = document.createElement('div');
766
+ c.id = 'tina4-dev-panel';
767
+ c.style.cssText = 'position:fixed;top:3rem;left:0;right:0;bottom:2rem;z-index:99998;transition:all 0.2s';
768
+ var f = document.createElement('iframe');
769
+ f.src = '/__dev';
770
+ 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';
771
+ c.appendChild(f);
772
+ document.body.appendChild(c);
773
+ return c;
774
+ }
775
+ window.__tina4ToggleOverlay = function(e) {
776
+ if (e) e.preventDefault();
777
+ var p = document.getElementById('tina4-dev-panel');
778
+ if (p) {
779
+ var hide = p.style.display !== 'none';
780
+ p.style.display = hide ? 'none' : 'block';
781
+ try { localStorage.setItem(STATE_KEY, hide ? '0' : '1'); } catch (_) {}
782
+ return;
783
+ }
784
+ buildOverlay();
785
+ try { localStorage.setItem(STATE_KEY, '1'); } catch (_) {}
786
+ };
787
+ function restoreIfOpen() {
788
+ try {
789
+ if (location.pathname.indexOf('/__dev') === 0) return;
790
+ if (localStorage.getItem(STATE_KEY) === '1' && !document.getElementById('tina4-dev-panel')) {
791
+ buildOverlay();
792
+ }
793
+ } catch (_) {}
794
+ }
795
+ if (document.readyState === 'loading') {
796
+ document.addEventListener('DOMContentLoaded', restoreIfOpen);
797
+ } else {
798
+ restoreIfOpen();
799
+ }
800
+ })();
801
+ </script>
802
+ <script>
721
803
  function tina4VersionModal(){
722
804
  var m=document.getElementById('tina4-ver-modal');
723
805
  if(m.style.display==='block'){m.style.display='none';return;}
@@ -151,6 +151,9 @@ module Tina4
151
151
  self
152
152
  end
153
153
 
154
+ # Render a Frond/Twig template file with data and return self. Tries
155
+ # the user template directory first, falling back to the framework's
156
+ # built-in templates. Sets the response body to the rendered HTML.
154
157
  def render(template_path, data = {}, status: 200, template_dir: nil)
155
158
  @status_code = status
156
159
  @headers["content-type"] = HTML_CONTENT_TYPE
data/lib/tina4/session.rb CHANGED
@@ -16,7 +16,7 @@ module Tina4
16
16
 
17
17
  def initialize(env, options = {})
18
18
  @options = DEFAULT_OPTIONS.merge(options)
19
- @options[:secret] ||= ENV["SECRET"] || "tina4-default-secret"
19
+ @options[:secret] ||= ENV["TINA4_SECRET"] || "tina4-default-secret"
20
20
  @handler = create_handler
21
21
  @id = extract_session_id(env) || SecureRandom.hex(32)
22
22
  @data = load_session
@@ -17,7 +17,7 @@ module Tina4
17
17
 
18
18
  def initialize(options = {})
19
19
  @ttl = options[:ttl] || 86400
20
- @db = options[:db] || Tina4::Database.new(ENV["DATABASE_URL"])
20
+ @db = options[:db] || Tina4::Database.new(ENV["TINA4_DATABASE_URL"])
21
21
  ensure_table
22
22
  end
23
23
 
@@ -53,6 +53,16 @@ module Tina4
53
53
  end
54
54
  end
55
55
 
56
+ # Stop background tasks
57
+ if defined?(Tina4::Background)
58
+ begin
59
+ Tina4::Background.stop_all
60
+ Tina4::Log.info("Background tasks stopped")
61
+ rescue => e
62
+ Tina4::Log.error("Error stopping background tasks: #{e.message}")
63
+ end
64
+ end
65
+
56
66
  # Close database connections
57
67
  if Tina4.database
58
68
  begin
data/lib/tina4/swagger.rb CHANGED
@@ -19,9 +19,9 @@ module Tina4
19
19
  {
20
20
  "openapi" => "3.0.3",
21
21
  "info" => {
22
- "title" => ENV["SWAGGER_TITLE"] || ENV["PROJECT_NAME"] || "Tina4 API",
23
- "version" => ENV["VERSION"] || Tina4::VERSION,
24
- "description" => "Auto-generated API documentation"
22
+ "title" => ENV["TINA4_SWAGGER_TITLE"] || ENV["PROJECT_NAME"] || "Tina4 API",
23
+ "version" => ENV["TINA4_SWAGGER_VERSION"] || Tina4::VERSION,
24
+ "description" => ENV["TINA4_SWAGGER_DESCRIPTION"] || "Auto-generated API documentation"
25
25
  },
26
26
  "servers" => [
27
27
  { "url" => "/" }
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.11.35"
4
+ VERSION = "3.12.0"
5
5
  end
@@ -54,6 +54,9 @@ module Tina4
54
54
  end
55
55
 
56
56
  def start
57
+ # Refuse to boot with v3.11 / v2 era un-prefixed env vars set.
58
+ Tina4.check_legacy_env_vars!
59
+
57
60
  is_managed = ARGV.include?('--managed')
58
61
  unless is_managed || ENV['TINA4_OVERRIDE_CLIENT'] == 'true'
59
62
  puts
data/lib/tina4.rb CHANGED
@@ -35,11 +35,14 @@ require_relative "tina4/cors"
35
35
  require_relative "tina4/rate_limiter"
36
36
  require_relative "tina4/health"
37
37
  require_relative "tina4/shutdown"
38
+ require_relative "tina4/background"
38
39
  require_relative "tina4/localization"
39
40
  require_relative "tina4/container"
40
41
  require_relative "tina4/queue"
41
42
  require_relative "tina4/service_runner"
42
43
  require_relative "tina4/events"
44
+ require_relative "tina4/plan"
45
+ require_relative "tina4/project_index"
43
46
  require_relative "tina4/dev_admin"
44
47
  require_relative "tina4/messenger"
45
48
  require_relative "tina4/dev_mailbox"
@@ -50,6 +53,7 @@ require_relative "tina4/response_cache"
50
53
  require_relative "tina4/html_element"
51
54
  require_relative "tina4/error_overlay"
52
55
  require_relative "tina4/test_client"
56
+ require_relative "tina4/docs"
53
57
  require_relative "tina4/mcp"
54
58
 
55
59
  module Tina4
@@ -383,6 +387,16 @@ module Tina4
383
387
  Tina4::ServiceRunner.register(name, nil, options, &block)
384
388
  end
385
389
 
390
+ # Register a periodic background task.
391
+ # Mirrors Python's tina4_python.core.server.background(fn, interval) and
392
+ # PHP's $app->background($callback, $interval).
393
+ #
394
+ # Tina4.background(interval: 5.0) { drain_queue }
395
+ # Tina4.background(method(:health_check), interval: 30.0)
396
+ def background(callback = nil, interval: 1.0, &block)
397
+ Tina4::Background.register(callback, interval: interval, &block)
398
+ end
399
+
386
400
  # DI container shortcuts
387
401
  def register(name, instance = nil, &block)
388
402
  Tina4::Container.register(name, instance, &block)
@@ -426,7 +440,7 @@ module Tina4
426
440
  end
427
441
 
428
442
  def setup_database
429
- db_url = ENV["DATABASE_URL"] || ENV["DB_URL"]
443
+ db_url = ENV["TINA4_DATABASE_URL"]
430
444
  if db_url && !db_url.empty?
431
445
  begin
432
446
  @database = Tina4::Database.new(db_url)
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.11.35
4
+ version: 3.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tina4 Team
@@ -281,6 +281,7 @@ files:
281
281
  - lib/tina4/api.rb
282
282
  - lib/tina4/auth.rb
283
283
  - lib/tina4/auto_crud.rb
284
+ - lib/tina4/background.rb
284
285
  - lib/tina4/cache.rb
285
286
  - lib/tina4/cli.rb
286
287
  - lib/tina4/constants.rb
@@ -294,6 +295,7 @@ files:
294
295
  - lib/tina4/dev.rb
295
296
  - lib/tina4/dev_admin.rb
296
297
  - lib/tina4/dev_mailbox.rb
298
+ - lib/tina4/docs.rb
297
299
  - lib/tina4/drivers/firebird_driver.rb
298
300
  - lib/tina4/drivers/mongodb_driver.rb
299
301
  - lib/tina4/drivers/mssql_driver.rb
@@ -333,11 +335,14 @@ files:
333
335
  - lib/tina4/middleware.rb
334
336
  - lib/tina4/migration.rb
335
337
  - lib/tina4/orm.rb
338
+ - lib/tina4/plan.rb
339
+ - lib/tina4/project_index.rb
336
340
  - lib/tina4/public/css/tina4.css
337
341
  - lib/tina4/public/css/tina4.min.css
338
342
  - lib/tina4/public/favicon.ico
339
343
  - lib/tina4/public/images/logo.svg
340
344
  - lib/tina4/public/images/tina4-logo-icon.webp
345
+ - lib/tina4/public/js/frond.js
341
346
  - lib/tina4/public/js/frond.min.js
342
347
  - lib/tina4/public/js/tina4-dev-admin.js
343
348
  - lib/tina4/public/js/tina4-dev-admin.min.js