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,295 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module Tina4
6
+ # Cross-engine SQL translator.
7
+ #
8
+ # Each database adapter calls the rules it needs. Rules are composable
9
+ # and stateless -- just string transforms.
10
+ #
11
+ # Also includes query caching with TTL support.
12
+ #
13
+ # Usage:
14
+ # translated = Tina4::SQLTranslator.limit_to_rows("SELECT * FROM users LIMIT 10 OFFSET 5")
15
+ # # => "SELECT * FROM users ROWS 6 TO 15"
16
+ #
17
+ class SQLTranslator
18
+ class << self
19
+ # Convert LIMIT/OFFSET to Firebird ROWS...TO syntax.
20
+ #
21
+ # LIMIT 10 OFFSET 5 => ROWS 6 TO 15
22
+ # LIMIT 10 => ROWS 1 TO 10
23
+ #
24
+ # @param sql [String]
25
+ # @return [String]
26
+ def limit_to_rows(sql)
27
+ # Try LIMIT X OFFSET Y first
28
+ if (m = sql.match(/\bLIMIT\s+(\d+)\s+OFFSET\s+(\d+)\s*$/i))
29
+ limit = m[1].to_i
30
+ offset = m[2].to_i
31
+ start_row = offset + 1
32
+ end_row = offset + limit
33
+ return sql[0...m.begin(0)] + "ROWS #{start_row} TO #{end_row}"
34
+ end
35
+
36
+ # Then try LIMIT X only
37
+ if (m = sql.match(/\bLIMIT\s+(\d+)\s*$/i))
38
+ limit = m[1].to_i
39
+ return sql[0...m.begin(0)] + "ROWS 1 TO #{limit}"
40
+ end
41
+
42
+ sql
43
+ end
44
+
45
+ # Convert LIMIT to MSSQL TOP syntax.
46
+ #
47
+ # SELECT ... LIMIT 10 => SELECT TOP 10 ...
48
+ # OFFSET queries are left unchanged (not supported by TOP).
49
+ #
50
+ # @param sql [String]
51
+ # @return [String]
52
+ def limit_to_top(sql)
53
+ if (m = sql.match(/\bLIMIT\s+(\d+)\s*$/i)) && !sql.match?(/\bOFFSET\b/i)
54
+ limit = m[1].to_i
55
+ body = sql[0...m.begin(0)].strip
56
+ return body.sub(/^(SELECT)\b/i, "\\1 TOP #{limit}")
57
+ end
58
+
59
+ sql
60
+ end
61
+
62
+ # Convert || concatenation to CONCAT() for MySQL/MSSQL.
63
+ #
64
+ # 'a' || 'b' || 'c' => CONCAT('a', 'b', 'c')
65
+ #
66
+ # @param sql [String]
67
+ # @return [String]
68
+ def concat_pipes_to_func(sql)
69
+ return sql unless sql.include?("||")
70
+
71
+ parts = sql.split("||")
72
+ if parts.length > 1
73
+ "CONCAT(#{parts.map(&:strip).join(', ')})"
74
+ else
75
+ sql
76
+ end
77
+ end
78
+
79
+ # Convert TRUE/FALSE to 1/0 for engines without boolean type.
80
+ #
81
+ # @param sql [String]
82
+ # @return [String]
83
+ def boolean_to_int(sql)
84
+ sql.gsub(/\bTRUE\b/i, "1").gsub(/\bFALSE\b/i, "0")
85
+ end
86
+
87
+ # Convert ILIKE to LOWER() LIKE LOWER() for engines without ILIKE.
88
+ #
89
+ # @param sql [String]
90
+ # @return [String]
91
+ def ilike_to_like(sql)
92
+ sql.gsub(/(\S+)\s+ILIKE\s+(\S+)/i) do
93
+ col = ::Regexp.last_match(1).strip
94
+ val = ::Regexp.last_match(2).strip
95
+ "LOWER(#{col}) LIKE LOWER(#{val})"
96
+ end
97
+ end
98
+
99
+ # Translate AUTOINCREMENT across engines in DDL.
100
+ #
101
+ # @param sql [String]
102
+ # @param engine [String] one of: mysql, postgresql, mssql, firebird, sqlite
103
+ # @return [String]
104
+ def auto_increment_syntax(sql, engine)
105
+ case engine
106
+ when "mysql"
107
+ sql.gsub("AUTOINCREMENT", "AUTO_INCREMENT")
108
+ when "postgresql"
109
+ sql.gsub(/INTEGER\s+PRIMARY\s+KEY\s+AUTOINCREMENT/i, "SERIAL PRIMARY KEY")
110
+ when "mssql"
111
+ sql.gsub(/AUTOINCREMENT/i, "IDENTITY(1,1)")
112
+ when "firebird"
113
+ sql.gsub(/\s*AUTOINCREMENT\b/i, "")
114
+ else
115
+ sql
116
+ end
117
+ end
118
+
119
+ # Convert ? placeholders to engine-specific style.
120
+ #
121
+ # ? => %s (MySQL, PostgreSQL)
122
+ # ? => :1, :2 (Oracle, Firebird)
123
+ #
124
+ # @param sql [String]
125
+ # @param style [String] target placeholder style: "%s" or ":"
126
+ # @return [String]
127
+ def placeholder_style(sql, style)
128
+ case style
129
+ when "%s"
130
+ sql.gsub("?", "%s")
131
+ when ":"
132
+ count = 0
133
+ sql.chars.map do |ch|
134
+ if ch == "?"
135
+ count += 1
136
+ ":#{count}"
137
+ else
138
+ ch
139
+ end
140
+ end.join
141
+ else
142
+ sql
143
+ end
144
+ end
145
+
146
+ # Generate a cache key for a query and its parameters.
147
+ #
148
+ # @param sql [String]
149
+ # @param params [Array, nil]
150
+ # @return [String]
151
+ def query_key(sql, params = nil)
152
+ raw = params ? "#{sql}|#{params.inspect}" : sql
153
+ "query:#{Digest::SHA256.hexdigest(raw)}"
154
+ end
155
+ end
156
+ end
157
+
158
+ # In-memory cache with TTL support for query results.
159
+ #
160
+ # Usage:
161
+ # cache = Tina4::QueryCache.new(default_ttl: 60, max_size: 1000)
162
+ # cache.set("key", "value", ttl: 30)
163
+ # cache.get("key") # => "value"
164
+ #
165
+ class QueryCache
166
+ CacheEntry = Struct.new(:value, :expires_at, :tags)
167
+
168
+ # @param default_ttl [Integer] default TTL in seconds (default: 300)
169
+ # @param max_size [Integer] maximum number of cache entries (default: 1000)
170
+ def initialize(default_ttl: 300, max_size: 1000)
171
+ @default_ttl = default_ttl
172
+ @max_size = max_size
173
+ @store = {}
174
+ @mutex = Mutex.new
175
+ end
176
+
177
+ # Store a value with optional TTL and tags.
178
+ #
179
+ # @param key [String]
180
+ # @param value [Object]
181
+ # @param ttl [Integer, nil] TTL in seconds (nil uses default)
182
+ # @param tags [Array<String>] optional tags for grouped invalidation
183
+ def set(key, value, ttl: nil, tags: [])
184
+ ttl ||= @default_ttl
185
+ expires_at = Time.now.to_f + ttl
186
+
187
+ @mutex.synchronize do
188
+ # Evict oldest if at capacity
189
+ if @store.size >= @max_size && !@store.key?(key)
190
+ oldest_key = @store.keys.first
191
+ @store.delete(oldest_key)
192
+ end
193
+ @store[key] = CacheEntry.new(value, expires_at, tags)
194
+ end
195
+ end
196
+
197
+ # Retrieve a cached value. Returns nil if expired or missing.
198
+ #
199
+ # @param key [String]
200
+ # @param default [Object] value to return if key is missing
201
+ # @return [Object, nil]
202
+ def get(key, default = nil)
203
+ @mutex.synchronize do
204
+ entry = @store[key]
205
+ return default unless entry
206
+
207
+ if Time.now.to_f > entry.expires_at
208
+ @store.delete(key)
209
+ return default
210
+ end
211
+
212
+ entry.value
213
+ end
214
+ end
215
+
216
+ # Check if a key exists and is not expired.
217
+ #
218
+ # @param key [String]
219
+ # @return [Boolean]
220
+ def has?(key)
221
+ @mutex.synchronize do
222
+ entry = @store[key]
223
+ return false unless entry
224
+
225
+ if Time.now.to_f > entry.expires_at
226
+ @store.delete(key)
227
+ return false
228
+ end
229
+
230
+ true
231
+ end
232
+ end
233
+
234
+ # Delete a key from the cache.
235
+ #
236
+ # @param key [String]
237
+ # @return [Boolean] true if the key was present
238
+ def delete(key)
239
+ @mutex.synchronize do
240
+ !@store.delete(key).nil?
241
+ end
242
+ end
243
+
244
+ # Clear all entries from the cache.
245
+ def clear
246
+ @mutex.synchronize { @store.clear }
247
+ end
248
+
249
+ # Clear all entries with a given tag.
250
+ #
251
+ # @param tag [String]
252
+ # @return [Integer] number of entries removed
253
+ def clear_tag(tag)
254
+ @mutex.synchronize do
255
+ keys_to_remove = @store.select { |_k, v| v.tags.include?(tag) }.keys
256
+ keys_to_remove.each { |k| @store.delete(k) }
257
+ keys_to_remove.size
258
+ end
259
+ end
260
+
261
+ # Remove all expired entries.
262
+ #
263
+ # @return [Integer] number of entries removed
264
+ def sweep
265
+ @mutex.synchronize do
266
+ now = Time.now.to_f
267
+ keys_to_remove = @store.select { |_k, v| now > v.expires_at }.keys
268
+ keys_to_remove.each { |k| @store.delete(k) }
269
+ keys_to_remove.size
270
+ end
271
+ end
272
+
273
+ # Fetch from cache, or compute and store.
274
+ #
275
+ # @param key [String]
276
+ # @param ttl [Integer] TTL in seconds
277
+ # @param block [Proc] factory to compute the value if not cached
278
+ # @return [Object]
279
+ def remember(key, ttl, &block)
280
+ cached = get(key)
281
+ return cached unless cached.nil?
282
+
283
+ value = block.call
284
+ set(key, value, ttl: ttl)
285
+ value
286
+ end
287
+
288
+ # Current number of entries in the cache.
289
+ #
290
+ # @return [Integer]
291
+ def size
292
+ @mutex.synchronize { @store.size }
293
+ end
294
+ end
295
+ end
@@ -33,16 +33,18 @@ module Tina4
33
33
  end
34
34
  end
35
35
 
36
- def render_error(code)
36
+ def render_error(code, data = {})
37
37
  error_dirs = TEMPLATE_DIRS.map { |d| File.join(Dir.pwd, d, "errors") }
38
38
  error_dirs << File.join(File.dirname(__FILE__), "templates", "errors")
39
39
 
40
+ context = { "code" => code }.merge(data.transform_keys(&:to_s))
41
+
40
42
  error_dirs.each do |dir|
41
43
  %w[.twig .html .erb].each do |ext|
42
44
  path = File.join(dir, "#{code}#{ext}")
43
45
  if File.exist?(path)
44
46
  content = File.read(path)
45
- return TwigEngine.new({ "code" => code }, dir).render(content)
47
+ return TwigEngine.new(context, dir).render(content)
46
48
  end
47
49
  end
48
50
  end
@@ -66,10 +68,38 @@ module Tina4
66
68
  def default_error_html(code)
67
69
  messages = { 403 => "Forbidden", 404 => "Not Found", 500 => "Internal Server Error" }
68
70
  msg = messages[code] || "Error"
69
- "<!DOCTYPE html><html><head><title>#{code} #{msg}</title></head>" \
70
- "<body style='font-family:sans-serif;text-align:center;padding:50px;'>" \
71
- "<h1>#{code}</h1><p>#{msg}</p><hr>" \
72
- "<p style='color:#999;'>Tina4 Ruby v#{Tina4::VERSION}</p></body></html>"
71
+ colors = { 403 => "#f59e0b", 404 => "#3b82f6", 500 => "#ef4444" }
72
+ color = colors[code] || "#ef4444"
73
+ <<~HTML
74
+ <!DOCTYPE html>
75
+ <html lang="en">
76
+ <head>
77
+ <meta charset="utf-8">
78
+ <meta name="viewport" content="width=device-width, initial-scale=1">
79
+ <title>#{code} — #{msg}</title>
80
+ <style>
81
+ * { box-sizing: border-box; margin: 0; padding: 0; }
82
+ body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
83
+ .error-card { background: #1e293b; border: 1px solid #334155; border-radius: 1rem; padding: 3rem; text-align: center; max-width: 520px; width: 90%; }
84
+ .error-code { font-size: 8rem; font-weight: 900; color: #{color}; opacity: 0.6; line-height: 1; margin-bottom: 0.5rem; }
85
+ .error-title { font-size: 1.5rem; font-weight: 700; margin-bottom: 0.75rem; }
86
+ .error-msg { color: #94a3b8; font-size: 1rem; margin-bottom: 1.5rem; line-height: 1.5; }
87
+ .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; }
88
+ .error-home:hover { opacity: 0.9; }
89
+ .logo { font-size: 1.5rem; margin-bottom: 1rem; opacity: 0.5; }
90
+ </style>
91
+ </head>
92
+ <body>
93
+ <div class="error-card">
94
+ <div class="logo">T4</div>
95
+ <div class="error-code">#{code}</div>
96
+ <div class="error-title">#{msg}</div>
97
+ <div class="error-msg">Something went wrong while processing your request.</div>
98
+ <a href="/" class="error-home">Go Home</a>
99
+ </div>
100
+ </body>
101
+ </html>
102
+ HTML
73
103
  end
74
104
  end
75
105
 
@@ -19,8 +19,8 @@
19
19
  <footer class="container mt-4 py-3 text-center text-muted border-top">
20
20
  <p>Powered by Tina4 Ruby v{{ tina4_version }}</p>
21
21
  </footer>
22
- <script src="/js/tina4.js"></script>
23
- <script src="/js/tina4helper.js"></script>
22
+ <script src="/js/tina4.min.js"></script>
23
+ <script src="/js/frond.min.js"></script>
24
24
  {% block scripts %}{% endblock %}
25
25
  </body>
26
26
  </html>
@@ -0,0 +1,14 @@
1
+ {% extends "errors/base.twig" %}
2
+ {% block title %}Redirecting…{% endblock %}
3
+ {% block meta %}<meta http-equiv="refresh" content="0;url={{ redirect_url }}">{% endblock %}
4
+ {% block extra_styles %}
5
+ .spinner { display: inline-block; width: 1.5rem; height: 1.5rem; border: 3px solid var(--border); border-top-color: var(--primary); border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 1rem; }
6
+ @keyframes spin { to { transform: rotate(360deg); } }
7
+ {% endblock %}
8
+ {% block content %}
9
+ <div class="spinner"></div>
10
+ <div class="error-title">Redirecting…</div>
11
+ <div class="error-msg">You are being redirected to a new location.</div>
12
+ <div class="error-path" style="color:var(--primary)">{{ redirect_url }}</div>
13
+ <a href="{{ redirect_url }}" class="error-home">Click here if not redirected</a>
14
+ {% endblock %}
@@ -0,0 +1,9 @@
1
+ {% extends "errors/base.twig" %}
2
+ {% block title %}401 — Unauthorized{% endblock %}
3
+ {% block content %}
4
+ <div class="error-code" style="color:var(--danger)">401</div>
5
+ <div class="error-title">Unauthorized</div>
6
+ <div class="error-msg">You need to sign in to access this resource.</div>
7
+ <div class="error-path" style="color:var(--danger)">{{ path }}</div>
8
+ <a href="/" class="error-home">Go Home</a>
9
+ {% endblock %}
@@ -1,22 +1,29 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>403 Forbidden</title>
7
- <style>
8
- body { font-family: -apple-system, sans-serif; text-align: center; padding: 80px 20px; background: #f8f9fa; }
9
- h1 { font-size: 6rem; color: #dc3545; margin: 0; }
10
- p { font-size: 1.2rem; color: #6c757d; }
11
- a { color: #0d6efd; text-decoration: none; }
12
- .container { max-width: 600px; margin: 0 auto; }
13
- </style>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>403 Forbidden</title>
7
+ <style>
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
10
+ .error-card { background: #1e293b; border: 1px solid #334155; border-radius: 1rem; padding: 3rem; text-align: center; max-width: 520px; width: 90%; }
11
+ .error-code { font-size: 8rem; font-weight: 900; color: #f59e0b; opacity: 0.6; line-height: 1; margin-bottom: 0.5rem; }
12
+ .error-title { font-size: 1.5rem; font-weight: 700; margin-bottom: 0.75rem; }
13
+ .error-msg { color: #94a3b8; font-size: 1rem; margin-bottom: 1.5rem; line-height: 1.5; }
14
+ .error-path { font-family: 'SF Mono', monospace; background: #0f172a; color: #f59e0b; padding: 0.5rem 1rem; border-radius: 0.5rem; font-size: 0.85rem; word-break: break-all; margin-bottom: 1.5rem; display: inline-block; }
15
+ .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; }
16
+ .error-home:hover { opacity: 0.9; }
17
+ </style>
14
18
  </head>
15
19
  <body>
16
- <div class="container">
17
- <h1>403</h1>
18
- <p>Forbidden. You do not have permission to access this resource.</p>
19
- <p><a href="/">Go Home</a></p>
20
- </div>
20
+ <div class="error-card">
21
+ <div class="error-code">403</div>
22
+ <div class="error-title">Forbidden</div>
23
+ <div class="error-msg">You don't have permission to access this resource.</div>
24
+ <div class="error-path">{{ path }}</div>
25
+ <br>
26
+ <a href="/" class="error-home">Go Home</a>
27
+ </div>
21
28
  </body>
22
29
  </html>
@@ -1,22 +1,29 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>404 Not Found</title>
7
- <style>
8
- body { font-family: -apple-system, sans-serif; text-align: center; padding: 80px 20px; background: #f8f9fa; }
9
- h1 { font-size: 6rem; color: #ffc107; margin: 0; }
10
- p { font-size: 1.2rem; color: #6c757d; }
11
- a { color: #0d6efd; text-decoration: none; }
12
- .container { max-width: 600px; margin: 0 auto; }
13
- </style>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>404 Not Found</title>
7
+ <style>
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
10
+ .error-card { background: #1e293b; border: 1px solid #334155; border-radius: 1rem; padding: 3rem; text-align: center; max-width: 520px; width: 90%; }
11
+ .error-code { font-size: 8rem; font-weight: 900; color: #3b82f6; opacity: 0.6; line-height: 1; margin-bottom: 0.5rem; }
12
+ .error-title { font-size: 1.5rem; font-weight: 700; margin-bottom: 0.75rem; }
13
+ .error-msg { color: #94a3b8; font-size: 1rem; margin-bottom: 1.5rem; line-height: 1.5; }
14
+ .error-path { font-family: 'SF Mono', monospace; background: #0f172a; color: #3b82f6; padding: 0.5rem 1rem; border-radius: 0.5rem; font-size: 0.85rem; word-break: break-all; margin-bottom: 1.5rem; display: inline-block; }
15
+ .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; }
16
+ .error-home:hover { opacity: 0.9; }
17
+ </style>
14
18
  </head>
15
19
  <body>
16
- <div class="container">
17
- <h1>404</h1>
18
- <p>Page not found. The requested resource could not be located.</p>
19
- <p><a href="/">Go Home</a></p>
20
- </div>
20
+ <div class="error-card">
21
+ <div class="error-code">404</div>
22
+ <div class="error-title">Page Not Found</div>
23
+ <div class="error-msg">The page you're looking for doesn't exist or has been moved. Check the URL and try again.</div>
24
+ <div class="error-path">{{ path }}</div>
25
+ <br>
26
+ <a href="/" class="error-home">Go Home</a>
27
+ </div>
21
28
  </body>
22
29
  </html>
@@ -1,22 +1,38 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>500 Internal Server Error</title>
7
- <style>
8
- body { font-family: -apple-system, sans-serif; text-align: center; padding: 80px 20px; background: #f8f9fa; }
9
- h1 { font-size: 6rem; color: #dc3545; margin: 0; }
10
- p { font-size: 1.2rem; color: #6c757d; }
11
- a { color: #0d6efd; text-decoration: none; }
12
- .container { max-width: 600px; margin: 0 auto; }
13
- </style>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>500 Server Error</title>
7
+ <style>
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
10
+ .error-card { background: #1e293b; border: 1px solid #334155; border-radius: 1rem; padding: 3rem; text-align: left; max-width: 700px; width: 90%; }
11
+ .error-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem; }
12
+ .error-code { font-size: 3rem; font-weight: 900; color: #ef4444; opacity: 0.7; }
13
+ .error-title { font-size: 1.3rem; font-weight: 700; }
14
+ .error-msg { color: #94a3b8; font-size: 0.95rem; margin-bottom: 1.5rem; line-height: 1.5; }
15
+ .error-trace { background: #0f172a; border: 1px solid #334155; border-radius: 0.5rem; padding: 1rem; font-family: 'SF Mono', monospace; font-size: 0.8rem; line-height: 1.5; overflow-x: auto; max-height: 400px; overflow-y: auto; white-space: pre-wrap; color: #ef4444; margin-bottom: 1.5rem; }
16
+ .error-footer { display: flex; justify-content: space-between; align-items: center; }
17
+ .error-hint { color: #64748b; font-size: 0.75rem; }
18
+ .error-id { color: #64748b; font-family: 'SF Mono', monospace; font-size: 0.75rem; }
19
+ .error-home { display: inline-block; padding: 0.5rem 1.5rem; background: #3b82f6; color: #fff; text-decoration: none; border-radius: 0.5rem; font-size: 0.85rem; font-weight: 600; }
20
+ .error-home:hover { opacity: 0.9; }
21
+ </style>
14
22
  </head>
15
23
  <body>
16
- <div class="container">
17
- <h1>500</h1>
18
- <p>Internal Server Error. Something went wrong on our end.</p>
19
- <p><a href="/">Go Home</a></p>
20
- </div>
24
+ <div class="error-card">
25
+ <div class="error-header">
26
+ <div class="error-code">500</div>
27
+ <div class="error-title">Server Error</div>
28
+ </div>
29
+ <div class="error-msg">Something went wrong while processing your request.</div>
30
+ <pre class="error-trace">{{ error_message }}</pre>
31
+ <div class="error-footer">
32
+ <span class="error-hint">Fix the error and save to auto-reload</span>
33
+ <span class="error-id">{{ request_id }}</span>
34
+ <a href="/" class="error-home">Go Home</a>
35
+ </div>
36
+ </div>
21
37
  </body>
22
38
  </html>
@@ -0,0 +1,9 @@
1
+ {% extends "errors/base.twig" %}
2
+ {% block title %}502 — Bad Gateway{% endblock %}
3
+ {% block content %}
4
+ <div class="error-code" style="color:var(--danger)">502</div>
5
+ <div class="error-title">Bad Gateway</div>
6
+ <div class="error-msg">The upstream server returned an invalid response.</div>
7
+ <div class="error-path" style="color:var(--danger)">{{ path }}</div>
8
+ <a href="/" class="error-home">Go Home</a>
9
+ {% endblock %}
@@ -0,0 +1,12 @@
1
+ {% extends "errors/base.twig" %}
2
+ {% block title %}503 — Service Unavailable{% endblock %}
3
+ {% block extra_styles %}
4
+ .error-code { color: var(--warn); }
5
+ {% endblock %}
6
+ {% block content %}
7
+ <div class="error-code" style="color:var(--warn)">503</div>
8
+ <div class="error-title">Service Unavailable</div>
9
+ <div class="error-msg">The server is temporarily unavailable. Please try again shortly.</div>
10
+ <div class="error-path" style="color:var(--warn)">{{ path }}</div>
11
+ <a href="/" class="error-home">Go Home</a>
12
+ {% endblock %}
@@ -0,0 +1,37 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ {% block meta %}{% endblock %}
7
+ <title>{% block title %}Error{% endblock %}</title>
8
+ <style>
9
+ :root {
10
+ --bg: #0f172a; --surface: #1e293b; --border: #334155;
11
+ --text: #e2e8f0; --muted: #94a3b8; --primary: #3b82f6;
12
+ --success: #22c55e; --danger: #ef4444; --warn: #f59e0b; --info: #06b6d4;
13
+ --radius: 1rem; --mono: 'SF Mono', ui-monospace, monospace;
14
+ --font: system-ui, -apple-system, sans-serif;
15
+ }
16
+ * { box-sizing: border-box; margin: 0; padding: 0; }
17
+ body { font-family: var(--font); background: var(--bg); color: var(--text); min-height: 100vh; display: flex; align-items: center; justify-content: center; }
18
+ .error-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 3rem; text-align: center; max-width: 480px; width: 90%; }
19
+ .error-code { font-size: 5rem; font-weight: 800; opacity: 0.3; line-height: 1; }
20
+ .error-title { font-size: 1.5rem; font-weight: 600; margin: 0.5rem 0; }
21
+ .error-msg { color: var(--muted); font-size: 0.9rem; margin-bottom: 1.5rem; }
22
+ .error-path { font-family: var(--mono); background: var(--bg); padding: 0.5rem 1rem; border-radius: 0.5rem; font-size: 0.85rem; word-break: break-all; }
23
+ .error-home { display: inline-block; margin-top: 1.5rem; padding: 0.5rem 1.5rem; background: var(--primary); color: #fff; text-decoration: none; border-radius: 0.5rem; font-size: 0.85rem; }
24
+ .error-home:hover { opacity: 0.9; }
25
+ .logo { width: 3rem; height: 3rem; margin-bottom: 1rem; opacity: 0.5; }
26
+ {% block extra_styles %}{% endblock %}
27
+ </style>
28
+ </head>
29
+ <body>
30
+ {% block body %}
31
+ <div class="error-card">
32
+ <img src="/images/logo.svg" class="logo" alt="Tina4">
33
+ {% block content %}{% endblock %}
34
+ </div>
35
+ {% endblock %}
36
+ </body>
37
+ </html>
data/lib/tina4/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tina4
4
- VERSION = "0.5.2"
4
+ VERSION = "3.0.0"
5
5
  end