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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1 -1
- data/README.md +360 -559
- data/exe/{tina4 → tina4ruby} +1 -0
- data/lib/tina4/ai.rb +312 -0
- data/lib/tina4/auth.rb +44 -3
- data/lib/tina4/auto_crud.rb +163 -0
- data/lib/tina4/cli.rb +242 -77
- data/lib/tina4/constants.rb +46 -0
- data/lib/tina4/cors.rb +74 -0
- data/lib/tina4/database/sqlite3_adapter.rb +139 -0
- data/lib/tina4/database.rb +43 -7
- data/lib/tina4/debug.rb +4 -79
- data/lib/tina4/dev_admin.rb +1162 -0
- data/lib/tina4/dev_mailbox.rb +191 -0
- data/lib/tina4/dev_reload.rb +9 -9
- data/lib/tina4/drivers/firebird_driver.rb +19 -3
- data/lib/tina4/drivers/mssql_driver.rb +3 -3
- data/lib/tina4/drivers/mysql_driver.rb +4 -4
- data/lib/tina4/drivers/postgres_driver.rb +9 -2
- data/lib/tina4/drivers/sqlite_driver.rb +1 -1
- data/lib/tina4/env.rb +42 -2
- data/lib/tina4/error_overlay.rb +252 -0
- data/lib/tina4/events.rb +90 -0
- data/lib/tina4/field_types.rb +4 -0
- data/lib/tina4/frond.rb +1336 -0
- data/lib/tina4/gallery/auth/meta.json +1 -0
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -0
- data/lib/tina4/gallery/database/meta.json +1 -0
- data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -0
- data/lib/tina4/gallery/error-overlay/meta.json +1 -0
- data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -0
- data/lib/tina4/gallery/orm/meta.json +1 -0
- data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -0
- data/lib/tina4/gallery/queue/meta.json +1 -0
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +27 -0
- data/lib/tina4/gallery/rest-api/meta.json +1 -0
- data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -0
- data/lib/tina4/gallery/templates/meta.json +1 -0
- data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -0
- data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -0
- data/lib/tina4/health.rb +39 -0
- data/lib/tina4/html_element.rb +148 -0
- data/lib/tina4/localization.rb +2 -2
- data/lib/tina4/log.rb +203 -0
- data/lib/tina4/messenger.rb +484 -0
- data/lib/tina4/migration.rb +132 -29
- data/lib/tina4/orm.rb +337 -31
- data/lib/tina4/public/css/tina4.css +178 -1
- data/lib/tina4/public/css/tina4.min.css +1 -2
- data/lib/tina4/public/favicon.ico +0 -0
- data/lib/tina4/public/images/logo.svg +5 -0
- data/lib/tina4/public/images/tina4-logo-icon.webp +0 -0
- data/lib/tina4/public/js/frond.min.js +420 -0
- data/lib/tina4/public/js/tina4-dev-admin.min.js +367 -0
- data/lib/tina4/public/js/tina4.min.js +93 -0
- data/lib/tina4/public/swagger/index.html +90 -0
- data/lib/tina4/public/swagger/oauth2-redirect.html +63 -0
- data/lib/tina4/queue.rb +40 -4
- data/lib/tina4/queue_backends/lite_backend.rb +88 -0
- data/lib/tina4/rack_app.rb +314 -23
- data/lib/tina4/rate_limiter.rb +123 -0
- data/lib/tina4/request.rb +61 -15
- data/lib/tina4/response.rb +54 -24
- data/lib/tina4/response_cache.rb +134 -0
- data/lib/tina4/router.rb +90 -15
- data/lib/tina4/scss_compiler.rb +2 -2
- data/lib/tina4/seeder.rb +56 -61
- data/lib/tina4/service_runner.rb +303 -0
- data/lib/tina4/session.rb +85 -0
- data/lib/tina4/session_handlers/mongo_handler.rb +1 -1
- data/lib/tina4/session_handlers/valkey_handler.rb +43 -0
- data/lib/tina4/shutdown.rb +84 -0
- data/lib/tina4/sql_translation.rb +295 -0
- data/lib/tina4/template.rb +36 -6
- data/lib/tina4/templates/base.twig +2 -2
- data/lib/tina4/templates/errors/302.twig +14 -0
- data/lib/tina4/templates/errors/401.twig +9 -0
- data/lib/tina4/templates/errors/403.twig +22 -15
- data/lib/tina4/templates/errors/404.twig +22 -15
- data/lib/tina4/templates/errors/500.twig +31 -15
- data/lib/tina4/templates/errors/502.twig +9 -0
- data/lib/tina4/templates/errors/503.twig +12 -0
- data/lib/tina4/templates/errors/base.twig +37 -0
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +28 -18
- data/lib/tina4.rb +57 -21
- metadata +51 -19
- data/lib/tina4/public/js/tina4.js +0 -134
- 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("&", "&")
|
|
95
|
+
.gsub('"', """)
|
|
96
|
+
.gsub("<", "<")
|
|
97
|
+
.gsub(">", ">")
|
|
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
|
data/lib/tina4/localization.rb
CHANGED
|
@@ -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::
|
|
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::
|
|
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
|