tina4ruby 0.5.2 → 3.0.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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -1
  3. data/README.md +360 -559
  4. data/exe/{tina4 → tina4ruby} +1 -0
  5. data/lib/tina4/ai.rb +312 -0
  6. data/lib/tina4/auth.rb +44 -3
  7. data/lib/tina4/auto_crud.rb +163 -0
  8. data/lib/tina4/cli.rb +242 -77
  9. data/lib/tina4/constants.rb +46 -0
  10. data/lib/tina4/cors.rb +74 -0
  11. data/lib/tina4/database/sqlite3_adapter.rb +139 -0
  12. data/lib/tina4/database.rb +43 -7
  13. data/lib/tina4/debug.rb +4 -79
  14. data/lib/tina4/dev_admin.rb +1162 -0
  15. data/lib/tina4/dev_mailbox.rb +191 -0
  16. data/lib/tina4/dev_reload.rb +9 -9
  17. data/lib/tina4/drivers/firebird_driver.rb +19 -3
  18. data/lib/tina4/drivers/mssql_driver.rb +3 -3
  19. data/lib/tina4/drivers/mysql_driver.rb +4 -4
  20. data/lib/tina4/drivers/postgres_driver.rb +9 -2
  21. data/lib/tina4/drivers/sqlite_driver.rb +1 -1
  22. data/lib/tina4/env.rb +42 -2
  23. data/lib/tina4/error_overlay.rb +252 -0
  24. data/lib/tina4/events.rb +90 -0
  25. data/lib/tina4/field_types.rb +4 -0
  26. data/lib/tina4/frond.rb +1336 -0
  27. data/lib/tina4/gallery/auth/meta.json +1 -0
  28. data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -0
  29. data/lib/tina4/gallery/database/meta.json +1 -0
  30. data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -0
  31. data/lib/tina4/gallery/error-overlay/meta.json +1 -0
  32. data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -0
  33. data/lib/tina4/gallery/orm/meta.json +1 -0
  34. data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -0
  35. data/lib/tina4/gallery/queue/meta.json +1 -0
  36. data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +27 -0
  37. data/lib/tina4/gallery/rest-api/meta.json +1 -0
  38. data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -0
  39. data/lib/tina4/gallery/templates/meta.json +1 -0
  40. data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -0
  41. data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -0
  42. data/lib/tina4/health.rb +39 -0
  43. data/lib/tina4/html_element.rb +148 -0
  44. data/lib/tina4/localization.rb +2 -2
  45. data/lib/tina4/log.rb +203 -0
  46. data/lib/tina4/messenger.rb +484 -0
  47. data/lib/tina4/migration.rb +132 -29
  48. data/lib/tina4/orm.rb +337 -31
  49. data/lib/tina4/public/css/tina4.css +178 -1
  50. data/lib/tina4/public/css/tina4.min.css +1 -2
  51. data/lib/tina4/public/favicon.ico +0 -0
  52. data/lib/tina4/public/images/logo.svg +5 -0
  53. data/lib/tina4/public/images/tina4-logo-icon.webp +0 -0
  54. data/lib/tina4/public/js/frond.min.js +420 -0
  55. data/lib/tina4/public/js/tina4-dev-admin.min.js +367 -0
  56. data/lib/tina4/public/js/tina4.min.js +93 -0
  57. data/lib/tina4/public/swagger/index.html +90 -0
  58. data/lib/tina4/public/swagger/oauth2-redirect.html +63 -0
  59. data/lib/tina4/queue.rb +40 -4
  60. data/lib/tina4/queue_backends/lite_backend.rb +88 -0
  61. data/lib/tina4/rack_app.rb +314 -23
  62. data/lib/tina4/rate_limiter.rb +123 -0
  63. data/lib/tina4/request.rb +61 -15
  64. data/lib/tina4/response.rb +54 -24
  65. data/lib/tina4/response_cache.rb +134 -0
  66. data/lib/tina4/router.rb +90 -15
  67. data/lib/tina4/scss_compiler.rb +2 -2
  68. data/lib/tina4/seeder.rb +56 -61
  69. data/lib/tina4/service_runner.rb +303 -0
  70. data/lib/tina4/session.rb +85 -0
  71. data/lib/tina4/session_handlers/mongo_handler.rb +1 -1
  72. data/lib/tina4/session_handlers/valkey_handler.rb +43 -0
  73. data/lib/tina4/shutdown.rb +84 -0
  74. data/lib/tina4/sql_translation.rb +295 -0
  75. data/lib/tina4/template.rb +36 -6
  76. data/lib/tina4/templates/base.twig +2 -2
  77. data/lib/tina4/templates/errors/302.twig +14 -0
  78. data/lib/tina4/templates/errors/401.twig +9 -0
  79. data/lib/tina4/templates/errors/403.twig +22 -15
  80. data/lib/tina4/templates/errors/404.twig +22 -15
  81. data/lib/tina4/templates/errors/500.twig +31 -15
  82. data/lib/tina4/templates/errors/502.twig +9 -0
  83. data/lib/tina4/templates/errors/503.twig +12 -0
  84. data/lib/tina4/templates/errors/base.twig +37 -0
  85. data/lib/tina4/version.rb +1 -1
  86. data/lib/tina4/webserver.rb +28 -18
  87. data/lib/tina4.rb +57 -21
  88. metadata +51 -19
  89. data/lib/tina4/public/js/tina4.js +0 -134
  90. data/lib/tina4/public/js/tina4helper.js +0 -387
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tina4
4
+ # Programmatic HTML builder — avoids string concatenation.
5
+ #
6
+ # Usage:
7
+ # el = Tina4::HtmlElement.new("div", { class: "card" }, ["Hello"])
8
+ # el.to_s # => '<div class="card">Hello</div>'
9
+ #
10
+ # # Builder pattern (via call)
11
+ # el = Tina4::HtmlElement.new("div").call(Tina4::HtmlElement.new("p").call("Text"))
12
+ #
13
+ # # Helper functions
14
+ # include Tina4::HtmlHelpers
15
+ # html = _div({ class: "card" }, _p("Hello"))
16
+ #
17
+ class HtmlElement
18
+ VOID_TAGS = %w[
19
+ area base br col embed hr img input
20
+ link meta param source track wbr
21
+ ].freeze
22
+
23
+ HTML_TAGS = %w[
24
+ a abbr address area article aside audio
25
+ b base bdi bdo blockquote body br button
26
+ canvas caption cite code col colgroup
27
+ data datalist dd del details dfn dialog div dl dt
28
+ em embed
29
+ fieldset figcaption figure footer form
30
+ h1 h2 h3 h4 h5 h6 head header hgroup hr html
31
+ i iframe img input ins
32
+ kbd
33
+ label legend li link
34
+ main map mark menu meta meter
35
+ nav noscript
36
+ object ol optgroup option output
37
+ p param picture pre progress
38
+ q
39
+ rp rt ruby
40
+ s samp script section select slot small source span
41
+ strong style sub summary sup
42
+ table tbody td template textarea tfoot th thead time
43
+ title tr track
44
+ u ul
45
+ var video
46
+ wbr
47
+ ].freeze
48
+
49
+ attr_reader :tag, :attrs, :children
50
+
51
+ # @param tag [String] HTML tag name
52
+ # @param attrs [Hash] attribute => value
53
+ # @param children [Array] child elements (strings or HtmlElement instances)
54
+ def initialize(tag, attrs = {}, children = [])
55
+ @tag = tag.to_s.downcase
56
+ @attrs = attrs
57
+ @children = children
58
+ end
59
+
60
+ # Builder pattern — appends children and/or merges attributes.
61
+ #
62
+ # @param args [Array] Strings, HtmlElements, Hashes (treated as attrs), or Arrays
63
+ # @return [HtmlElement] a new HtmlElement with the appended children
64
+ def call(*args)
65
+ new_attrs = @attrs.dup
66
+ new_children = @children.dup
67
+
68
+ args.each do |arg|
69
+ case arg
70
+ when Hash
71
+ new_attrs = new_attrs.merge(arg)
72
+ when Array
73
+ new_children.concat(arg)
74
+ else
75
+ new_children << arg
76
+ end
77
+ end
78
+
79
+ self.class.new(@tag, new_attrs, new_children)
80
+ end
81
+
82
+ # Render to HTML string.
83
+ def to_s
84
+ html = "<#{@tag}"
85
+
86
+ @attrs.each do |key, value|
87
+ case value
88
+ when true
89
+ html << " #{key}"
90
+ when false, nil
91
+ next
92
+ else
93
+ escaped = value.to_s
94
+ .gsub("&", "&amp;")
95
+ .gsub('"', "&quot;")
96
+ .gsub("<", "&lt;")
97
+ .gsub(">", "&gt;")
98
+ html << " #{key}=\"#{escaped}\""
99
+ end
100
+ end
101
+
102
+ if VOID_TAGS.include?(@tag)
103
+ html << ">"
104
+ return html
105
+ end
106
+
107
+ html << ">"
108
+
109
+ @children.each do |child|
110
+ html << child.to_s
111
+ end
112
+
113
+ html << "</#{@tag}>"
114
+ html
115
+ end
116
+ end
117
+
118
+ # Module providing _div, _p, _span, etc. helper methods.
119
+ # Include in any class or use extend on a module.
120
+ module HtmlHelpers
121
+ HtmlElement::HTML_TAGS.each do |tag|
122
+ define_method("_#{tag}") do |*args|
123
+ attrs = {}
124
+ children = []
125
+
126
+ args.each do |arg|
127
+ case arg
128
+ when Hash
129
+ attrs = attrs.merge(arg)
130
+ when Array
131
+ children.concat(arg)
132
+ when HtmlElement
133
+ children << arg
134
+ else
135
+ children << arg
136
+ end
137
+ end
138
+
139
+ HtmlElement.new(tag, attrs, children)
140
+ end
141
+ end
142
+ end
143
+
144
+ # Module-level convenience: Tina4.html_helpers returns a module you can include.
145
+ def self.html_helpers
146
+ HtmlHelpers
147
+ end
148
+ end
@@ -28,7 +28,7 @@ module Tina4
28
28
  data = JSON.parse(File.read(file))
29
29
  translations[locale] ||= {}
30
30
  translations[locale].merge!(data)
31
- Tina4::Debug.debug("Loaded locale: #{locale} from #{file}")
31
+ Tina4::Log.debug("Loaded locale: #{locale} from #{file}")
32
32
  end
33
33
 
34
34
  # Also support YAML
@@ -40,7 +40,7 @@ module Tina4
40
40
  translations[locale] ||= {}
41
41
  translations[locale].merge!(data) if data.is_a?(Hash)
42
42
  rescue LoadError
43
- Tina4::Debug.warning("YAML support requires the 'yaml' gem")
43
+ Tina4::Log.warning("YAML support requires the 'yaml' gem")
44
44
  end
45
45
  end
46
46
  end
data/lib/tina4/log.rb ADDED
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+
6
+ module Tina4
7
+ module Log
8
+ LEVELS = {
9
+ "[TINA4_LOG_ALL]" => 0,
10
+ "[TINA4_LOG_DEBUG]" => 0,
11
+ "[TINA4_LOG_INFO]" => 1,
12
+ "[TINA4_LOG_WARNING]" => 2,
13
+ "[TINA4_LOG_ERROR]" => 3,
14
+ "[TINA4_LOG_NONE]" => 4
15
+ }.freeze
16
+
17
+ SEVERITY_MAP = {
18
+ debug: 0, info: 1, warn: 2, error: 3
19
+ }.freeze
20
+
21
+ COLORS = {
22
+ reset: "\e[0m", red: "\e[31m", green: "\e[32m",
23
+ yellow: "\e[33m", blue: "\e[34m", magenta: "\e[35m",
24
+ cyan: "\e[36m", gray: "\e[90m"
25
+ }.freeze
26
+
27
+ # ANSI escape code regex for stripping from file output
28
+ ANSI_RE = /\033\[[0-9;]*m/
29
+
30
+ class << self
31
+ attr_reader :log_dir
32
+
33
+ def setup(root_dir = Dir.pwd)
34
+ @log_dir = File.join(root_dir, "logs")
35
+ FileUtils.mkdir_p(@log_dir)
36
+
37
+ @max_size_mb = (ENV["TINA4_LOG_MAX_SIZE"] || "10").to_i
38
+ @max_size = @max_size_mb * 1024 * 1024
39
+ @keep = (ENV["TINA4_LOG_KEEP"] || "5").to_i
40
+ @json_mode = production?
41
+ @log_file = File.join(@log_dir, "tina4.log")
42
+
43
+ @console_level = resolve_level
44
+ @request_id = nil
45
+ @current_context = {}
46
+ @mutex = Mutex.new
47
+ @initialized = true
48
+ end
49
+
50
+ def set_request_id(id)
51
+ @mutex.synchronize { @request_id = id }
52
+ end
53
+
54
+ def clear_request_id
55
+ @mutex.synchronize { @request_id = nil }
56
+ end
57
+
58
+ def request_id
59
+ @mutex.synchronize { @request_id }
60
+ end
61
+
62
+ def json_mode?
63
+ @json_mode
64
+ end
65
+
66
+ def info(message, context = {})
67
+ log(:info, message, context)
68
+ end
69
+
70
+ def debug(message, context = {})
71
+ log(:debug, message, context)
72
+ end
73
+
74
+ def warning(message, context = {})
75
+ log(:warn, message, context)
76
+ end
77
+
78
+ def error(message, context = {})
79
+ log(:error, message, context)
80
+ end
81
+
82
+ private
83
+
84
+ def production?
85
+ env = ENV["TINA4_ENV"] || ENV["RACK_ENV"] || ENV["RUBY_ENV"] || "development"
86
+ env.downcase == "production"
87
+ end
88
+
89
+ def log(level, message, context = {})
90
+ setup unless @initialized
91
+ @current_context = context.is_a?(Hash) ? context : {}
92
+
93
+ formatted = format_line(level, message)
94
+
95
+ # Console output respects TINA4_LOG_LEVEL
96
+ severity = SEVERITY_MAP[level] || 0
97
+ if severity >= @console_level
98
+ if @json_mode
99
+ $stdout.puts json_line(level, message)
100
+ else
101
+ $stdout.puts colorize(level, formatted)
102
+ end
103
+ end
104
+
105
+ # File always gets ALL levels, plain text (no ANSI)
106
+ write_to_file(strip_ansi(formatted))
107
+
108
+ @current_context = {}
109
+ end
110
+
111
+ def resolve_level
112
+ env_level = ENV["TINA4_LOG_LEVEL"] || "[TINA4_LOG_ALL]"
113
+ LEVELS[env_level] || 0
114
+ end
115
+
116
+ def severity_to_level(level)
117
+ case level
118
+ when :debug then "DEBUG"
119
+ when :info then "INFO"
120
+ when :warn then "WARNING"
121
+ when :error then "ERROR"
122
+ else level.to_s.upcase
123
+ end
124
+ end
125
+
126
+ def utc_timestamp
127
+ now = Time.now.utc
128
+ now.strftime("%Y-%m-%dT%H:%M:%S.") + format("%03d", now.usec / 1000) + "Z"
129
+ end
130
+
131
+ def strip_ansi(text)
132
+ text.gsub(ANSI_RE, "")
133
+ end
134
+
135
+ def format_line(level, message)
136
+ level_str = severity_to_level(level)
137
+ ts = utc_timestamp
138
+ rid = request_id
139
+ rid_str = rid ? " [#{rid}]" : ""
140
+ ctx = @current_context && !@current_context.empty? ? " #{JSON.generate(@current_context)}" : ""
141
+ "#{ts} [#{level_str.ljust(7)}]#{rid_str} #{message}#{ctx}"
142
+ end
143
+
144
+ def json_line(level, message)
145
+ level_str = severity_to_level(level)
146
+ entry = {
147
+ timestamp: utc_timestamp,
148
+ level: level_str,
149
+ message: message
150
+ }
151
+ rid = request_id
152
+ entry[:request_id] = rid if rid
153
+ entry[:context] = @current_context if @current_context && !@current_context.empty?
154
+ JSON.generate(entry)
155
+ end
156
+
157
+ def colorize(level, line)
158
+ color = case level
159
+ when :debug then COLORS[:cyan]
160
+ when :info then COLORS[:green]
161
+ when :warn then COLORS[:yellow]
162
+ when :error then COLORS[:red]
163
+ else COLORS[:reset]
164
+ end
165
+ "#{color}#{line}#{COLORS[:reset]}"
166
+ end
167
+
168
+ def write_to_file(line)
169
+ rotate_if_needed
170
+ begin
171
+ File.open(@log_file, "a") { |f| f.puts(line) }
172
+ rescue IOError, SystemCallError
173
+ # Don't crash on log write failure
174
+ end
175
+ end
176
+
177
+ # Numbered rotation: tina4.log → tina4.log.1 → tina4.log.2 → ... → tina4.log.{keep}
178
+ def rotate_if_needed
179
+ return unless File.exist?(@log_file)
180
+
181
+ begin
182
+ return if File.size(@log_file) < @max_size
183
+ rescue SystemCallError
184
+ return
185
+ end
186
+
187
+ # Delete the oldest rotated file if it exists
188
+ oldest = "#{@log_file}.#{@keep}"
189
+ File.delete(oldest) if File.exist?(oldest)
190
+
191
+ # Shift existing rotated files: .{n} → .{n+1}
192
+ (@keep - 1).downto(1) do |n|
193
+ src = "#{@log_file}.#{n}"
194
+ dst = "#{@log_file}.#{n + 1}"
195
+ File.rename(src, dst) if File.exist?(src)
196
+ end
197
+
198
+ # Rename current log to .1
199
+ File.rename(@log_file, "#{@log_file}.1") rescue nil
200
+ end
201
+ end
202
+ end
203
+ end