tina4ruby 3.11.13 → 3.11.15
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 +80 -80
- data/LICENSE.txt +21 -21
- data/README.md +137 -137
- data/exe/tina4ruby +5 -5
- data/lib/tina4/ai.rb +696 -696
- data/lib/tina4/api.rb +189 -189
- data/lib/tina4/auth.rb +305 -305
- data/lib/tina4/auto_crud.rb +244 -244
- data/lib/tina4/cache.rb +154 -154
- data/lib/tina4/cli.rb +1449 -1449
- data/lib/tina4/constants.rb +46 -46
- data/lib/tina4/container.rb +74 -74
- data/lib/tina4/cors.rb +74 -74
- data/lib/tina4/crud.rb +692 -692
- data/lib/tina4/database/sqlite3_adapter.rb +165 -165
- data/lib/tina4/database.rb +625 -625
- data/lib/tina4/database_result.rb +208 -208
- data/lib/tina4/debug.rb +8 -8
- data/lib/tina4/dev.rb +14 -14
- data/lib/tina4/dev_admin.rb +935 -935
- data/lib/tina4/dev_mailbox.rb +191 -191
- data/lib/tina4/drivers/firebird_driver.rb +124 -110
- data/lib/tina4/drivers/mongodb_driver.rb +561 -561
- data/lib/tina4/drivers/mssql_driver.rb +112 -112
- data/lib/tina4/drivers/mysql_driver.rb +90 -90
- data/lib/tina4/drivers/odbc_driver.rb +191 -191
- data/lib/tina4/drivers/postgres_driver.rb +116 -106
- data/lib/tina4/drivers/sqlite_driver.rb +122 -122
- data/lib/tina4/env.rb +95 -95
- data/lib/tina4/error_overlay.rb +252 -252
- data/lib/tina4/events.rb +109 -109
- data/lib/tina4/field_types.rb +154 -154
- data/lib/tina4/frond.rb +2025 -2025
- data/lib/tina4/gallery/auth/meta.json +1 -1
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -114
- data/lib/tina4/gallery/database/meta.json +1 -1
- data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -43
- data/lib/tina4/gallery/error-overlay/meta.json +1 -1
- data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -17
- data/lib/tina4/gallery/orm/meta.json +1 -1
- data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -16
- data/lib/tina4/gallery/queue/meta.json +1 -1
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +325 -325
- data/lib/tina4/gallery/rest-api/meta.json +1 -1
- data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -14
- data/lib/tina4/gallery/templates/meta.json +1 -1
- data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -12
- data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -257
- data/lib/tina4/graphql.rb +966 -966
- data/lib/tina4/health.rb +39 -39
- data/lib/tina4/html_element.rb +170 -170
- data/lib/tina4/job.rb +80 -80
- data/lib/tina4/localization.rb +168 -168
- data/lib/tina4/log.rb +203 -203
- data/lib/tina4/mcp.rb +696 -696
- data/lib/tina4/messenger.rb +587 -587
- data/lib/tina4/metrics.rb +793 -793
- data/lib/tina4/middleware.rb +445 -445
- data/lib/tina4/migration.rb +451 -451
- data/lib/tina4/orm.rb +790 -790
- data/lib/tina4/public/css/tina4.css +2463 -2463
- data/lib/tina4/public/css/tina4.min.css +1 -1
- data/lib/tina4/public/images/logo.svg +5 -5
- data/lib/tina4/public/js/frond.min.js +2 -2
- data/lib/tina4/public/js/tina4-dev-admin.js +565 -565
- data/lib/tina4/public/js/tina4-dev-admin.min.js +480 -480
- data/lib/tina4/public/js/tina4.min.js +92 -92
- data/lib/tina4/public/js/tina4js.min.js +48 -48
- data/lib/tina4/public/swagger/index.html +90 -90
- data/lib/tina4/public/swagger/oauth2-redirect.html +63 -63
- data/lib/tina4/query_builder.rb +380 -380
- data/lib/tina4/queue.rb +366 -366
- data/lib/tina4/queue_backends/kafka_backend.rb +80 -80
- data/lib/tina4/queue_backends/lite_backend.rb +298 -298
- data/lib/tina4/queue_backends/mongo_backend.rb +126 -126
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -73
- data/lib/tina4/rack_app.rb +817 -817
- data/lib/tina4/rate_limiter.rb +130 -130
- data/lib/tina4/request.rb +268 -255
- data/lib/tina4/response.rb +346 -346
- data/lib/tina4/response_cache.rb +551 -551
- data/lib/tina4/router.rb +406 -406
- data/lib/tina4/scss/tina4css/_alerts.scss +34 -34
- data/lib/tina4/scss/tina4css/_badges.scss +22 -22
- data/lib/tina4/scss/tina4css/_buttons.scss +69 -69
- data/lib/tina4/scss/tina4css/_cards.scss +49 -49
- data/lib/tina4/scss/tina4css/_forms.scss +156 -156
- data/lib/tina4/scss/tina4css/_grid.scss +81 -81
- data/lib/tina4/scss/tina4css/_modals.scss +84 -84
- data/lib/tina4/scss/tina4css/_nav.scss +149 -149
- data/lib/tina4/scss/tina4css/_reset.scss +94 -94
- data/lib/tina4/scss/tina4css/_tables.scss +54 -54
- data/lib/tina4/scss/tina4css/_typography.scss +55 -55
- data/lib/tina4/scss/tina4css/_utilities.scss +197 -197
- data/lib/tina4/scss/tina4css/_variables.scss +117 -117
- data/lib/tina4/scss/tina4css/base.scss +1 -1
- data/lib/tina4/scss/tina4css/colors.scss +48 -48
- data/lib/tina4/scss/tina4css/tina4.scss +17 -17
- data/lib/tina4/scss_compiler.rb +178 -178
- data/lib/tina4/seeder.rb +567 -567
- data/lib/tina4/service_runner.rb +303 -303
- data/lib/tina4/session.rb +297 -297
- data/lib/tina4/session_handlers/database_handler.rb +72 -72
- data/lib/tina4/session_handlers/file_handler.rb +67 -67
- data/lib/tina4/session_handlers/mongo_handler.rb +49 -49
- data/lib/tina4/session_handlers/redis_handler.rb +43 -43
- data/lib/tina4/session_handlers/valkey_handler.rb +43 -43
- data/lib/tina4/shutdown.rb +84 -84
- data/lib/tina4/sql_translation.rb +158 -158
- data/lib/tina4/swagger.rb +124 -124
- data/lib/tina4/template.rb +894 -894
- data/lib/tina4/templates/base.twig +26 -26
- data/lib/tina4/templates/errors/302.twig +14 -14
- data/lib/tina4/templates/errors/401.twig +9 -9
- data/lib/tina4/templates/errors/403.twig +29 -29
- data/lib/tina4/templates/errors/404.twig +29 -29
- data/lib/tina4/templates/errors/500.twig +38 -38
- data/lib/tina4/templates/errors/502.twig +9 -9
- data/lib/tina4/templates/errors/503.twig +12 -12
- data/lib/tina4/templates/errors/base.twig +37 -37
- data/lib/tina4/test_client.rb +159 -159
- data/lib/tina4/testing.rb +340 -340
- data/lib/tina4/validator.rb +174 -174
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +312 -312
- data/lib/tina4/websocket.rb +343 -343
- data/lib/tina4/websocket_backplane.rb +190 -190
- data/lib/tina4/wsdl.rb +564 -564
- data/lib/tina4.rb +458 -458
- data/lib/tina4ruby.rb +4 -4
- metadata +3 -3
data/lib/tina4/error_overlay.rb
CHANGED
|
@@ -1,252 +1,252 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Tina4 Debug — Rich error overlay for development mode.
|
|
4
|
-
#
|
|
5
|
-
# Renders a professional, syntax-highlighted HTML error page when an unhandled
|
|
6
|
-
# exception occurs in a route handler.
|
|
7
|
-
#
|
|
8
|
-
# begin
|
|
9
|
-
# handler.call(request, response)
|
|
10
|
-
# rescue => e
|
|
11
|
-
# Tina4::ErrorOverlay.render_error_overlay(e, request: env)
|
|
12
|
-
# end
|
|
13
|
-
#
|
|
14
|
-
# Only activate when TINA4_DEBUG is true.
|
|
15
|
-
# In production, call Tina4::ErrorOverlay.render_production_error instead.
|
|
16
|
-
|
|
17
|
-
module Tina4
|
|
18
|
-
module ErrorOverlay
|
|
19
|
-
# ── Colour palette (Catppuccin Mocha) ──────────────────────────────
|
|
20
|
-
BG = "#1e1e2e"
|
|
21
|
-
SURFACE = "#313244"
|
|
22
|
-
OVERLAY_COLOR = "#45475a"
|
|
23
|
-
TEXT_COLOR = "#cdd6f4"
|
|
24
|
-
SUBTEXT = "#a6adc8"
|
|
25
|
-
RED = "#f38ba8"
|
|
26
|
-
YELLOW = "#f9e2af"
|
|
27
|
-
BLUE = "#89b4fa"
|
|
28
|
-
GREEN = "#a6e3a1"
|
|
29
|
-
LAVENDER = "#b4befe"
|
|
30
|
-
PEACH = "#fab387"
|
|
31
|
-
ERROR_LINE_BG = "rgba(243,139,168,0.15)"
|
|
32
|
-
|
|
33
|
-
CONTEXT_LINES = 7
|
|
34
|
-
|
|
35
|
-
class << self
|
|
36
|
-
# Render a rich HTML error overlay.
|
|
37
|
-
#
|
|
38
|
-
# @param exception [Exception] the caught exception
|
|
39
|
-
# @param request [Hash, nil] optional request details (Rack env or custom hash)
|
|
40
|
-
# @return [String] complete HTML page
|
|
41
|
-
def render_error_overlay(exception, request: nil)
|
|
42
|
-
exc_type = exception.class.name
|
|
43
|
-
exc_msg = exception.message
|
|
44
|
-
|
|
45
|
-
# ── Stack trace ──
|
|
46
|
-
frames_html = +""
|
|
47
|
-
backtrace = exception.backtrace || []
|
|
48
|
-
backtrace.each do |line|
|
|
49
|
-
file, lineno, method = parse_backtrace_line(line)
|
|
50
|
-
frames_html << format_frame(file, lineno, method)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# ── Request info ──
|
|
54
|
-
request_pairs = []
|
|
55
|
-
if request.is_a?(Hash)
|
|
56
|
-
request.each do |k, v|
|
|
57
|
-
key = k.to_s
|
|
58
|
-
if v.is_a?(Hash)
|
|
59
|
-
v.each { |hk, hv| request_pairs << ["#{key}.#{hk}", hv.to_s] }
|
|
60
|
-
elsif key.start_with?("HTTP_") || %w[REQUEST_METHOD REQUEST_URI SERVER_PROTOCOL
|
|
61
|
-
REMOTE_ADDR SERVER_PORT QUERY_STRING CONTENT_TYPE CONTENT_LENGTH
|
|
62
|
-
method url path].include?(key)
|
|
63
|
-
request_pairs << [key, v.to_s]
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
request_section = request_pairs.empty? ? "" : collapsible("Request Details", table(request_pairs))
|
|
68
|
-
|
|
69
|
-
# ── Environment ──
|
|
70
|
-
env_pairs = [
|
|
71
|
-
["Framework", "Tina4 Ruby"],
|
|
72
|
-
["Version", defined?(Tina4::VERSION) ? Tina4::VERSION : "unknown"],
|
|
73
|
-
["Ruby", RUBY_VERSION],
|
|
74
|
-
["Platform", RUBY_PLATFORM],
|
|
75
|
-
["Debug", ENV.fetch("TINA4_DEBUG", "false")],
|
|
76
|
-
["Log Level", ENV.fetch("TINA4_LOG_LEVEL", "ERROR")]
|
|
77
|
-
]
|
|
78
|
-
env_section = collapsible("Environment", table(env_pairs))
|
|
79
|
-
stack_section = collapsible("Stack Trace", frames_html, open_by_default: true)
|
|
80
|
-
|
|
81
|
-
<<~HTML
|
|
82
|
-
<!DOCTYPE html>
|
|
83
|
-
<html lang="en">
|
|
84
|
-
<head>
|
|
85
|
-
<meta charset="utf-8">
|
|
86
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
87
|
-
<title>Tina4 Error — #{esc(exc_type)}</title>
|
|
88
|
-
<style>
|
|
89
|
-
*{margin:0;padding:0;box-sizing:border-box;}
|
|
90
|
-
body{background:#{BG};color:#{TEXT_COLOR};font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:24px;line-height:1.5;}
|
|
91
|
-
</style>
|
|
92
|
-
</head>
|
|
93
|
-
<body>
|
|
94
|
-
<div style="max-width:960px;margin:0 auto;">
|
|
95
|
-
<div style="margin-bottom:24px;">
|
|
96
|
-
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
|
|
97
|
-
<span style="background:#{RED};color:#{BG};padding:4px 12px;border-radius:4px;font-weight:700;font-size:13px;text-transform:uppercase;">Error</span>
|
|
98
|
-
<span style="color:#{SUBTEXT};font-size:14px;">Tina4 Debug Overlay</span>
|
|
99
|
-
</div>
|
|
100
|
-
<h1 style="color:#{RED};font-size:28px;font-weight:700;margin-bottom:8px;">#{esc(exc_type)}</h1>
|
|
101
|
-
<p style="color:#{TEXT_COLOR};font-size:18px;font-family:'SF Mono','Fira Code','Consolas',monospace;background:#{SURFACE};padding:12px 16px;border-radius:6px;border-left:4px solid #{RED};">#{esc(exc_msg)}</p>
|
|
102
|
-
</div>
|
|
103
|
-
#{stack_section}
|
|
104
|
-
#{request_section}
|
|
105
|
-
#{env_section}
|
|
106
|
-
<div style="margin-top:32px;padding-top:16px;border-top:1px solid #{OVERLAY_COLOR};color:#{SUBTEXT};font-size:12px;">
|
|
107
|
-
Tina4 Debug Overlay — This page is only shown in debug mode. Set TINA4_DEBUG=false in production.
|
|
108
|
-
</div>
|
|
109
|
-
</div>
|
|
110
|
-
</body>
|
|
111
|
-
</html>
|
|
112
|
-
HTML
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# Render a safe, generic error page for production.
|
|
116
|
-
def render_production_error(status_code: 500, message: "Internal Server Error", path: "")
|
|
117
|
-
# Determine color based on status code
|
|
118
|
-
code_color = case status_code
|
|
119
|
-
when 403 then "#f59e0b"
|
|
120
|
-
when 404 then "#3b82f6"
|
|
121
|
-
else "#ef4444"
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
<<~HTML
|
|
125
|
-
<!DOCTYPE html>
|
|
126
|
-
<html lang="en">
|
|
127
|
-
<head>
|
|
128
|
-
<meta charset="utf-8">
|
|
129
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
130
|
-
<title>#{status_code} — #{esc(message)}</title>
|
|
131
|
-
<style>
|
|
132
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
133
|
-
body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
|
|
134
|
-
.error-card { background: #1e293b; border: 1px solid #334155; border-radius: 1rem; padding: 3rem; text-align: center; max-width: 520px; width: 90%; }
|
|
135
|
-
.error-code { font-size: 8rem; font-weight: 900; color: #{code_color}; opacity: 0.6; line-height: 1; margin-bottom: 0.5rem; }
|
|
136
|
-
.error-title { font-size: 1.5rem; font-weight: 700; margin-bottom: 0.75rem; }
|
|
137
|
-
.error-msg { color: #94a3b8; font-size: 1rem; margin-bottom: 1.5rem; line-height: 1.5; }
|
|
138
|
-
.error-path { font-family: 'SF Mono', monospace; background: #0f172a; color: #{code_color}; padding: 0.5rem 1rem; border-radius: 0.5rem; font-size: 0.85rem; word-break: break-all; margin-bottom: 1.5rem; display: inline-block; }
|
|
139
|
-
.error-home { display: inline-block; padding: 0.6rem 2rem; background: #3b82f6; color: #fff; text-decoration: none; border-radius: 0.5rem; font-size: 0.9rem; font-weight: 600; }
|
|
140
|
-
.error-home:hover { opacity: 0.9; }
|
|
141
|
-
.logo { font-size: 1.5rem; margin-bottom: 1rem; opacity: 0.5; }
|
|
142
|
-
</style>
|
|
143
|
-
</head>
|
|
144
|
-
<body>
|
|
145
|
-
<div class="error-card">
|
|
146
|
-
<div class="error-code">#{status_code}</div>
|
|
147
|
-
<div class="error-title">#{esc(message)}</div>
|
|
148
|
-
<div class="error-msg">Something went wrong while processing your request.</div>
|
|
149
|
-
#{path.to_s.empty? ? '' : "<div class=\"error-path\">#{esc(path)}</div><br>"}
|
|
150
|
-
<a href="/" class="error-home">Go Home</a>
|
|
151
|
-
</div>
|
|
152
|
-
</body>
|
|
153
|
-
</html>
|
|
154
|
-
HTML
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
# Return true if TINA4_DEBUG is enabled.
|
|
158
|
-
def is_debug_mode
|
|
159
|
-
Tina4::Env.is_truthy(ENV.fetch("TINA4_DEBUG", ""))
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
private
|
|
163
|
-
|
|
164
|
-
def esc(text)
|
|
165
|
-
text.to_s
|
|
166
|
-
.gsub("&", "&")
|
|
167
|
-
.gsub("<", "<")
|
|
168
|
-
.gsub(">", ">")
|
|
169
|
-
.gsub('"', """)
|
|
170
|
-
.gsub("'", "'")
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
def parse_backtrace_line(line)
|
|
174
|
-
if line =~ /\A(.+):(\d+):in [`'](.+)'\z/
|
|
175
|
-
[$1, $2.to_i, $3]
|
|
176
|
-
elsif line =~ /\A(.+):(\d+)\z/
|
|
177
|
-
[$1, $2.to_i, "{main}"]
|
|
178
|
-
else
|
|
179
|
-
[line, 0, "{unknown}"]
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
def read_source_lines(filename, lineno)
|
|
184
|
-
return [] unless filename && lineno.positive? && File.file?(filename) && File.readable?(filename)
|
|
185
|
-
|
|
186
|
-
all_lines = File.readlines(filename, chomp: true)
|
|
187
|
-
start_idx = [0, lineno - CONTEXT_LINES - 1].max
|
|
188
|
-
end_idx = [all_lines.length, lineno + CONTEXT_LINES].min
|
|
189
|
-
(start_idx...end_idx).map do |i|
|
|
190
|
-
num = i + 1
|
|
191
|
-
[num, all_lines[i] || "", num == lineno]
|
|
192
|
-
end
|
|
193
|
-
rescue StandardError
|
|
194
|
-
[]
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
def format_source_block(filename, lineno)
|
|
198
|
-
lines = read_source_lines(filename, lineno)
|
|
199
|
-
return "" if lines.empty?
|
|
200
|
-
|
|
201
|
-
rows = lines.map do |num, text, is_error|
|
|
202
|
-
bg = is_error ? "background:#{ERROR_LINE_BG};" : ""
|
|
203
|
-
marker = is_error ? "▶" : " "
|
|
204
|
-
"<div style=\"#{bg}display:flex;padding:1px 0;\">" \
|
|
205
|
-
"<span style=\"color:#{YELLOW};min-width:3.5em;text-align:right;padding-right:1em;user-select:none;\">#{num}</span>" \
|
|
206
|
-
"<span style=\"color:#{RED};width:1.2em;user-select:none;\">#{marker}</span>" \
|
|
207
|
-
"<span style=\"color:#{TEXT_COLOR};white-space:pre-wrap;tab-size:4;\">#{esc(text)}</span>" \
|
|
208
|
-
"</div>"
|
|
209
|
-
end.join("\n")
|
|
210
|
-
|
|
211
|
-
"<div style=\"background:#{SURFACE};border-radius:6px;padding:12px;overflow-x:auto;" \
|
|
212
|
-
"font-family:'SF Mono','Fira Code','Consolas',monospace;font-size:13px;line-height:1.6;\">" \
|
|
213
|
-
"#{rows}</div>"
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def format_frame(filename, lineno, func_name)
|
|
217
|
-
source = (filename && lineno.positive?) ? format_source_block(filename, lineno) : ""
|
|
218
|
-
"<div style=\"margin-bottom:16px;\">" \
|
|
219
|
-
"<div style=\"margin-bottom:4px;\">" \
|
|
220
|
-
"<span style=\"color:#{BLUE};\">#{esc(filename.to_s)}</span>" \
|
|
221
|
-
"<span style=\"color:#{SUBTEXT};\"> : </span>" \
|
|
222
|
-
"<span style=\"color:#{YELLOW};\">#{lineno}</span>" \
|
|
223
|
-
"<span style=\"color:#{SUBTEXT};\"> in </span>" \
|
|
224
|
-
"<span style=\"color:#{GREEN};\">#{esc(func_name.to_s)}</span>" \
|
|
225
|
-
"</div>" \
|
|
226
|
-
"#{source}" \
|
|
227
|
-
"</div>"
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def collapsible(title, content, open_by_default: false)
|
|
231
|
-
open_attr = open_by_default ? " open" : ""
|
|
232
|
-
"<details style=\"margin-top:16px;\"#{open_attr}>" \
|
|
233
|
-
"<summary style=\"cursor:pointer;color:#{LAVENDER};font-weight:600;font-size:15px;" \
|
|
234
|
-
"padding:8px 0;user-select:none;\">#{esc(title)}</summary>" \
|
|
235
|
-
"<div style=\"padding:8px 0;\">#{content}</div>" \
|
|
236
|
-
"</details>"
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
def table(pairs)
|
|
240
|
-
return "<span style=\"color:#{SUBTEXT};\">None</span>" if pairs.empty?
|
|
241
|
-
|
|
242
|
-
rows = pairs.map do |key, val|
|
|
243
|
-
"<tr>" \
|
|
244
|
-
"<td style=\"color:#{PEACH};padding:4px 16px 4px 0;vertical-align:top;white-space:nowrap;\">#{esc(key)}</td>" \
|
|
245
|
-
"<td style=\"color:#{TEXT_COLOR};padding:4px 0;word-break:break-all;\">#{esc(val)}</td>" \
|
|
246
|
-
"</tr>"
|
|
247
|
-
end.join
|
|
248
|
-
"<table style=\"border-collapse:collapse;width:100%;\">#{rows}</table>"
|
|
249
|
-
end
|
|
250
|
-
end
|
|
251
|
-
end
|
|
252
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Tina4 Debug — Rich error overlay for development mode.
|
|
4
|
+
#
|
|
5
|
+
# Renders a professional, syntax-highlighted HTML error page when an unhandled
|
|
6
|
+
# exception occurs in a route handler.
|
|
7
|
+
#
|
|
8
|
+
# begin
|
|
9
|
+
# handler.call(request, response)
|
|
10
|
+
# rescue => e
|
|
11
|
+
# Tina4::ErrorOverlay.render_error_overlay(e, request: env)
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# Only activate when TINA4_DEBUG is true.
|
|
15
|
+
# In production, call Tina4::ErrorOverlay.render_production_error instead.
|
|
16
|
+
|
|
17
|
+
module Tina4
|
|
18
|
+
module ErrorOverlay
|
|
19
|
+
# ── Colour palette (Catppuccin Mocha) ──────────────────────────────
|
|
20
|
+
BG = "#1e1e2e"
|
|
21
|
+
SURFACE = "#313244"
|
|
22
|
+
OVERLAY_COLOR = "#45475a"
|
|
23
|
+
TEXT_COLOR = "#cdd6f4"
|
|
24
|
+
SUBTEXT = "#a6adc8"
|
|
25
|
+
RED = "#f38ba8"
|
|
26
|
+
YELLOW = "#f9e2af"
|
|
27
|
+
BLUE = "#89b4fa"
|
|
28
|
+
GREEN = "#a6e3a1"
|
|
29
|
+
LAVENDER = "#b4befe"
|
|
30
|
+
PEACH = "#fab387"
|
|
31
|
+
ERROR_LINE_BG = "rgba(243,139,168,0.15)"
|
|
32
|
+
|
|
33
|
+
CONTEXT_LINES = 7
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
# Render a rich HTML error overlay.
|
|
37
|
+
#
|
|
38
|
+
# @param exception [Exception] the caught exception
|
|
39
|
+
# @param request [Hash, nil] optional request details (Rack env or custom hash)
|
|
40
|
+
# @return [String] complete HTML page
|
|
41
|
+
def render_error_overlay(exception, request: nil)
|
|
42
|
+
exc_type = exception.class.name
|
|
43
|
+
exc_msg = exception.message
|
|
44
|
+
|
|
45
|
+
# ── Stack trace ──
|
|
46
|
+
frames_html = +""
|
|
47
|
+
backtrace = exception.backtrace || []
|
|
48
|
+
backtrace.each do |line|
|
|
49
|
+
file, lineno, method = parse_backtrace_line(line)
|
|
50
|
+
frames_html << format_frame(file, lineno, method)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# ── Request info ──
|
|
54
|
+
request_pairs = []
|
|
55
|
+
if request.is_a?(Hash)
|
|
56
|
+
request.each do |k, v|
|
|
57
|
+
key = k.to_s
|
|
58
|
+
if v.is_a?(Hash)
|
|
59
|
+
v.each { |hk, hv| request_pairs << ["#{key}.#{hk}", hv.to_s] }
|
|
60
|
+
elsif key.start_with?("HTTP_") || %w[REQUEST_METHOD REQUEST_URI SERVER_PROTOCOL
|
|
61
|
+
REMOTE_ADDR SERVER_PORT QUERY_STRING CONTENT_TYPE CONTENT_LENGTH
|
|
62
|
+
method url path].include?(key)
|
|
63
|
+
request_pairs << [key, v.to_s]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
request_section = request_pairs.empty? ? "" : collapsible("Request Details", table(request_pairs))
|
|
68
|
+
|
|
69
|
+
# ── Environment ──
|
|
70
|
+
env_pairs = [
|
|
71
|
+
["Framework", "Tina4 Ruby"],
|
|
72
|
+
["Version", defined?(Tina4::VERSION) ? Tina4::VERSION : "unknown"],
|
|
73
|
+
["Ruby", RUBY_VERSION],
|
|
74
|
+
["Platform", RUBY_PLATFORM],
|
|
75
|
+
["Debug", ENV.fetch("TINA4_DEBUG", "false")],
|
|
76
|
+
["Log Level", ENV.fetch("TINA4_LOG_LEVEL", "ERROR")]
|
|
77
|
+
]
|
|
78
|
+
env_section = collapsible("Environment", table(env_pairs))
|
|
79
|
+
stack_section = collapsible("Stack Trace", frames_html, open_by_default: true)
|
|
80
|
+
|
|
81
|
+
<<~HTML
|
|
82
|
+
<!DOCTYPE html>
|
|
83
|
+
<html lang="en">
|
|
84
|
+
<head>
|
|
85
|
+
<meta charset="utf-8">
|
|
86
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
87
|
+
<title>Tina4 Error — #{esc(exc_type)}</title>
|
|
88
|
+
<style>
|
|
89
|
+
*{margin:0;padding:0;box-sizing:border-box;}
|
|
90
|
+
body{background:#{BG};color:#{TEXT_COLOR};font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:24px;line-height:1.5;}
|
|
91
|
+
</style>
|
|
92
|
+
</head>
|
|
93
|
+
<body>
|
|
94
|
+
<div style="max-width:960px;margin:0 auto;">
|
|
95
|
+
<div style="margin-bottom:24px;">
|
|
96
|
+
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
|
|
97
|
+
<span style="background:#{RED};color:#{BG};padding:4px 12px;border-radius:4px;font-weight:700;font-size:13px;text-transform:uppercase;">Error</span>
|
|
98
|
+
<span style="color:#{SUBTEXT};font-size:14px;">Tina4 Debug Overlay</span>
|
|
99
|
+
</div>
|
|
100
|
+
<h1 style="color:#{RED};font-size:28px;font-weight:700;margin-bottom:8px;">#{esc(exc_type)}</h1>
|
|
101
|
+
<p style="color:#{TEXT_COLOR};font-size:18px;font-family:'SF Mono','Fira Code','Consolas',monospace;background:#{SURFACE};padding:12px 16px;border-radius:6px;border-left:4px solid #{RED};">#{esc(exc_msg)}</p>
|
|
102
|
+
</div>
|
|
103
|
+
#{stack_section}
|
|
104
|
+
#{request_section}
|
|
105
|
+
#{env_section}
|
|
106
|
+
<div style="margin-top:32px;padding-top:16px;border-top:1px solid #{OVERLAY_COLOR};color:#{SUBTEXT};font-size:12px;">
|
|
107
|
+
Tina4 Debug Overlay — This page is only shown in debug mode. Set TINA4_DEBUG=false in production.
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</body>
|
|
111
|
+
</html>
|
|
112
|
+
HTML
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Render a safe, generic error page for production.
|
|
116
|
+
def render_production_error(status_code: 500, message: "Internal Server Error", path: "")
|
|
117
|
+
# Determine color based on status code
|
|
118
|
+
code_color = case status_code
|
|
119
|
+
when 403 then "#f59e0b"
|
|
120
|
+
when 404 then "#3b82f6"
|
|
121
|
+
else "#ef4444"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
<<~HTML
|
|
125
|
+
<!DOCTYPE html>
|
|
126
|
+
<html lang="en">
|
|
127
|
+
<head>
|
|
128
|
+
<meta charset="utf-8">
|
|
129
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
130
|
+
<title>#{status_code} — #{esc(message)}</title>
|
|
131
|
+
<style>
|
|
132
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
133
|
+
body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
|
|
134
|
+
.error-card { background: #1e293b; border: 1px solid #334155; border-radius: 1rem; padding: 3rem; text-align: center; max-width: 520px; width: 90%; }
|
|
135
|
+
.error-code { font-size: 8rem; font-weight: 900; color: #{code_color}; opacity: 0.6; line-height: 1; margin-bottom: 0.5rem; }
|
|
136
|
+
.error-title { font-size: 1.5rem; font-weight: 700; margin-bottom: 0.75rem; }
|
|
137
|
+
.error-msg { color: #94a3b8; font-size: 1rem; margin-bottom: 1.5rem; line-height: 1.5; }
|
|
138
|
+
.error-path { font-family: 'SF Mono', monospace; background: #0f172a; color: #{code_color}; padding: 0.5rem 1rem; border-radius: 0.5rem; font-size: 0.85rem; word-break: break-all; margin-bottom: 1.5rem; display: inline-block; }
|
|
139
|
+
.error-home { display: inline-block; padding: 0.6rem 2rem; background: #3b82f6; color: #fff; text-decoration: none; border-radius: 0.5rem; font-size: 0.9rem; font-weight: 600; }
|
|
140
|
+
.error-home:hover { opacity: 0.9; }
|
|
141
|
+
.logo { font-size: 1.5rem; margin-bottom: 1rem; opacity: 0.5; }
|
|
142
|
+
</style>
|
|
143
|
+
</head>
|
|
144
|
+
<body>
|
|
145
|
+
<div class="error-card">
|
|
146
|
+
<div class="error-code">#{status_code}</div>
|
|
147
|
+
<div class="error-title">#{esc(message)}</div>
|
|
148
|
+
<div class="error-msg">Something went wrong while processing your request.</div>
|
|
149
|
+
#{path.to_s.empty? ? '' : "<div class=\"error-path\">#{esc(path)}</div><br>"}
|
|
150
|
+
<a href="/" class="error-home">Go Home</a>
|
|
151
|
+
</div>
|
|
152
|
+
</body>
|
|
153
|
+
</html>
|
|
154
|
+
HTML
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Return true if TINA4_DEBUG is enabled.
|
|
158
|
+
def is_debug_mode
|
|
159
|
+
Tina4::Env.is_truthy(ENV.fetch("TINA4_DEBUG", ""))
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
def esc(text)
|
|
165
|
+
text.to_s
|
|
166
|
+
.gsub("&", "&")
|
|
167
|
+
.gsub("<", "<")
|
|
168
|
+
.gsub(">", ">")
|
|
169
|
+
.gsub('"', """)
|
|
170
|
+
.gsub("'", "'")
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def parse_backtrace_line(line)
|
|
174
|
+
if line =~ /\A(.+):(\d+):in [`'](.+)'\z/
|
|
175
|
+
[$1, $2.to_i, $3]
|
|
176
|
+
elsif line =~ /\A(.+):(\d+)\z/
|
|
177
|
+
[$1, $2.to_i, "{main}"]
|
|
178
|
+
else
|
|
179
|
+
[line, 0, "{unknown}"]
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def read_source_lines(filename, lineno)
|
|
184
|
+
return [] unless filename && lineno.positive? && File.file?(filename) && File.readable?(filename)
|
|
185
|
+
|
|
186
|
+
all_lines = File.readlines(filename, chomp: true)
|
|
187
|
+
start_idx = [0, lineno - CONTEXT_LINES - 1].max
|
|
188
|
+
end_idx = [all_lines.length, lineno + CONTEXT_LINES].min
|
|
189
|
+
(start_idx...end_idx).map do |i|
|
|
190
|
+
num = i + 1
|
|
191
|
+
[num, all_lines[i] || "", num == lineno]
|
|
192
|
+
end
|
|
193
|
+
rescue StandardError
|
|
194
|
+
[]
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def format_source_block(filename, lineno)
|
|
198
|
+
lines = read_source_lines(filename, lineno)
|
|
199
|
+
return "" if lines.empty?
|
|
200
|
+
|
|
201
|
+
rows = lines.map do |num, text, is_error|
|
|
202
|
+
bg = is_error ? "background:#{ERROR_LINE_BG};" : ""
|
|
203
|
+
marker = is_error ? "▶" : " "
|
|
204
|
+
"<div style=\"#{bg}display:flex;padding:1px 0;\">" \
|
|
205
|
+
"<span style=\"color:#{YELLOW};min-width:3.5em;text-align:right;padding-right:1em;user-select:none;\">#{num}</span>" \
|
|
206
|
+
"<span style=\"color:#{RED};width:1.2em;user-select:none;\">#{marker}</span>" \
|
|
207
|
+
"<span style=\"color:#{TEXT_COLOR};white-space:pre-wrap;tab-size:4;\">#{esc(text)}</span>" \
|
|
208
|
+
"</div>"
|
|
209
|
+
end.join("\n")
|
|
210
|
+
|
|
211
|
+
"<div style=\"background:#{SURFACE};border-radius:6px;padding:12px;overflow-x:auto;" \
|
|
212
|
+
"font-family:'SF Mono','Fira Code','Consolas',monospace;font-size:13px;line-height:1.6;\">" \
|
|
213
|
+
"#{rows}</div>"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def format_frame(filename, lineno, func_name)
|
|
217
|
+
source = (filename && lineno.positive?) ? format_source_block(filename, lineno) : ""
|
|
218
|
+
"<div style=\"margin-bottom:16px;\">" \
|
|
219
|
+
"<div style=\"margin-bottom:4px;\">" \
|
|
220
|
+
"<span style=\"color:#{BLUE};\">#{esc(filename.to_s)}</span>" \
|
|
221
|
+
"<span style=\"color:#{SUBTEXT};\"> : </span>" \
|
|
222
|
+
"<span style=\"color:#{YELLOW};\">#{lineno}</span>" \
|
|
223
|
+
"<span style=\"color:#{SUBTEXT};\"> in </span>" \
|
|
224
|
+
"<span style=\"color:#{GREEN};\">#{esc(func_name.to_s)}</span>" \
|
|
225
|
+
"</div>" \
|
|
226
|
+
"#{source}" \
|
|
227
|
+
"</div>"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def collapsible(title, content, open_by_default: false)
|
|
231
|
+
open_attr = open_by_default ? " open" : ""
|
|
232
|
+
"<details style=\"margin-top:16px;\"#{open_attr}>" \
|
|
233
|
+
"<summary style=\"cursor:pointer;color:#{LAVENDER};font-weight:600;font-size:15px;" \
|
|
234
|
+
"padding:8px 0;user-select:none;\">#{esc(title)}</summary>" \
|
|
235
|
+
"<div style=\"padding:8px 0;\">#{content}</div>" \
|
|
236
|
+
"</details>"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def table(pairs)
|
|
240
|
+
return "<span style=\"color:#{SUBTEXT};\">None</span>" if pairs.empty?
|
|
241
|
+
|
|
242
|
+
rows = pairs.map do |key, val|
|
|
243
|
+
"<tr>" \
|
|
244
|
+
"<td style=\"color:#{PEACH};padding:4px 16px 4px 0;vertical-align:top;white-space:nowrap;\">#{esc(key)}</td>" \
|
|
245
|
+
"<td style=\"color:#{TEXT_COLOR};padding:4px 0;word-break:break-all;\">#{esc(val)}</td>" \
|
|
246
|
+
"</tr>"
|
|
247
|
+
end.join
|
|
248
|
+
"<table style=\"border-collapse:collapse;width:100%;\">#{rows}</table>"
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|