tina4ruby 3.10.23 → 3.10.30
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/README.md +1 -1
- data/lib/tina4/dev_admin.rb +180 -5
- data/lib/tina4/frond.rb +8 -14
- data/lib/tina4/orm.rb +34 -31
- data/lib/tina4/rack_app.rb +1 -1
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4.rb +1 -1
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9f356bd29729293ca6cbc614d058d1f36e587d24734ac95dc09dc2cb0dc8f106
|
|
4
|
+
data.tar.gz: 2fa1c1a2bd95599261b53b1790e27401abf8a9a9e308d03a05e33ccdafcbe7c2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ff0588fb70c881e2b1a0a696df8b2ae1f7067b61c85dc29e64268a4f19b26e552e018c22321574e45b97814f86d708691f78120c74c7aaac0c76ad77f45bdbf4
|
|
7
|
+
data.tar.gz: c64f603f1d3cf692cfbbf45e476e16f7c69be7e4a0d127f9d10b3144b64edf3b82704ad2affdfc7ea127cbd67bef51c55b0b7f5416e741ef1c1210a6d75affdb
|
data/README.md
CHANGED
data/lib/tina4/dev_admin.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require "digest"
|
|
5
|
+
require "tmpdir"
|
|
4
6
|
|
|
5
7
|
module Tina4
|
|
6
8
|
# Thread-safe in-memory message log for dev dashboard
|
|
@@ -104,6 +106,171 @@ module Tina4
|
|
|
104
106
|
end
|
|
105
107
|
end
|
|
106
108
|
|
|
109
|
+
# Thread-safe, file-persisted error tracker for the dev dashboard Error Tracker panel.
|
|
110
|
+
#
|
|
111
|
+
# Errors are stored in a JSON file in the system temp directory keyed by
|
|
112
|
+
# project path, so they survive across requests and server restarts.
|
|
113
|
+
# Duplicate errors (same type + message + file + line) are de-duplicated —
|
|
114
|
+
# the count increments and the entry is re-opened if it was resolved.
|
|
115
|
+
class ErrorTracker
|
|
116
|
+
MAX_ERRORS = 200
|
|
117
|
+
private_constant :MAX_ERRORS
|
|
118
|
+
|
|
119
|
+
def initialize
|
|
120
|
+
@mutex = Mutex.new
|
|
121
|
+
@errors = nil # lazy-loaded
|
|
122
|
+
@store_path = File.join(
|
|
123
|
+
Dir.tmpdir,
|
|
124
|
+
"tina4_dev_errors_#{Digest::MD5.hexdigest(Dir.pwd)}.json"
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Capture a Ruby error / exception into the tracker.
|
|
129
|
+
# @param error_type [String] e.g. "RuntimeError" or "NoMethodError"
|
|
130
|
+
# @param message [String] exception message
|
|
131
|
+
# @param traceback [String] formatted backtrace (optional)
|
|
132
|
+
# @param file [String] source file (optional)
|
|
133
|
+
# @param line [Integer] source line (optional)
|
|
134
|
+
def capture(error_type:, message:, traceback: "", file: "", line: 0)
|
|
135
|
+
@mutex.synchronize do
|
|
136
|
+
load_unlocked
|
|
137
|
+
fingerprint = Digest::MD5.hexdigest("#{error_type}|#{message}|#{file}|#{line}")
|
|
138
|
+
now = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
139
|
+
|
|
140
|
+
if @errors.key?(fingerprint)
|
|
141
|
+
@errors[fingerprint][:count] += 1
|
|
142
|
+
@errors[fingerprint][:last_seen] = now
|
|
143
|
+
@errors[fingerprint][:resolved] = false # re-open resolved duplicates
|
|
144
|
+
else
|
|
145
|
+
@errors[fingerprint] = {
|
|
146
|
+
id: fingerprint,
|
|
147
|
+
error_type: error_type,
|
|
148
|
+
message: message,
|
|
149
|
+
traceback: traceback,
|
|
150
|
+
file: file,
|
|
151
|
+
line: line,
|
|
152
|
+
first_seen: now,
|
|
153
|
+
last_seen: now,
|
|
154
|
+
count: 1,
|
|
155
|
+
resolved: false
|
|
156
|
+
}
|
|
157
|
+
end
|
|
158
|
+
save_unlocked
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Capture a Ruby exception object directly.
|
|
163
|
+
def capture_exception(exc)
|
|
164
|
+
capture(
|
|
165
|
+
error_type: exc.class.name,
|
|
166
|
+
message: exc.message,
|
|
167
|
+
traceback: (exc.backtrace || []).first(20).join("\n"),
|
|
168
|
+
file: (exc.backtrace_locations&.first&.path || ""),
|
|
169
|
+
line: (exc.backtrace_locations&.first&.lineno || 0)
|
|
170
|
+
)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Return all errors (newest first).
|
|
174
|
+
# @param include_resolved [Boolean]
|
|
175
|
+
def get(include_resolved: true)
|
|
176
|
+
@mutex.synchronize do
|
|
177
|
+
load_unlocked
|
|
178
|
+
entries = @errors.values
|
|
179
|
+
entries = entries.reject { |e| e[:resolved] } unless include_resolved
|
|
180
|
+
entries.sort_by { |e| e[:last_seen] }.reverse
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Count of unresolved errors.
|
|
185
|
+
def unresolved_count
|
|
186
|
+
@mutex.synchronize do
|
|
187
|
+
load_unlocked
|
|
188
|
+
@errors.count { |_, e| !e[:resolved] }
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Health summary (matches Python BrokenTracker interface).
|
|
193
|
+
def health
|
|
194
|
+
@mutex.synchronize do
|
|
195
|
+
load_unlocked
|
|
196
|
+
total = @errors.size
|
|
197
|
+
resolved = @errors.count { |_, e| e[:resolved] }
|
|
198
|
+
unresolved = total - resolved
|
|
199
|
+
{ total: total, unresolved: unresolved, resolved: resolved, healthy: unresolved.zero? }
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Mark a single error as resolved.
|
|
204
|
+
def resolve(id)
|
|
205
|
+
@mutex.synchronize do
|
|
206
|
+
load_unlocked
|
|
207
|
+
return false unless @errors.key?(id)
|
|
208
|
+
|
|
209
|
+
@errors[id][:resolved] = true
|
|
210
|
+
save_unlocked
|
|
211
|
+
true
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Remove all resolved errors.
|
|
216
|
+
def clear_resolved
|
|
217
|
+
@mutex.synchronize do
|
|
218
|
+
load_unlocked
|
|
219
|
+
@errors.reject! { |_, e| e[:resolved] }
|
|
220
|
+
save_unlocked
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Remove ALL errors.
|
|
225
|
+
def clear_all
|
|
226
|
+
@mutex.synchronize do
|
|
227
|
+
@errors = {}
|
|
228
|
+
save_unlocked
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Reset (for testing).
|
|
233
|
+
def reset!
|
|
234
|
+
@mutex.synchronize do
|
|
235
|
+
@errors = {}
|
|
236
|
+
File.delete(@store_path) if File.exist?(@store_path)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
private
|
|
241
|
+
|
|
242
|
+
def load_unlocked
|
|
243
|
+
return if @errors
|
|
244
|
+
|
|
245
|
+
if File.exist?(@store_path)
|
|
246
|
+
raw = File.read(@store_path) rescue nil
|
|
247
|
+
data = raw ? (JSON.parse(raw, symbolize_names: true) rescue nil) : nil
|
|
248
|
+
if data.is_a?(Array)
|
|
249
|
+
# Re-key by id
|
|
250
|
+
@errors = {}
|
|
251
|
+
data.each { |e| @errors[e[:id]] = e if e[:id] }
|
|
252
|
+
else
|
|
253
|
+
@errors = {}
|
|
254
|
+
end
|
|
255
|
+
else
|
|
256
|
+
@errors = {}
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def save_unlocked
|
|
261
|
+
# Trim to max, keeping newest last_seen
|
|
262
|
+
if @errors.size > MAX_ERRORS
|
|
263
|
+
sorted = @errors.values.sort_by { |e| e[:last_seen] }.last(MAX_ERRORS)
|
|
264
|
+
@errors = {}
|
|
265
|
+
sorted.each { |e| @errors[e[:id]] = e }
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
File.write(@store_path, JSON.generate(@errors.values))
|
|
269
|
+
rescue StandardError
|
|
270
|
+
# Best-effort persistence — never raise in a tracker
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
107
274
|
# Developer dashboard module - only active in debug mode
|
|
108
275
|
module DevAdmin
|
|
109
276
|
class << self
|
|
@@ -119,6 +286,10 @@ module Tina4
|
|
|
119
286
|
@mailbox ||= DevMailbox.new
|
|
120
287
|
end
|
|
121
288
|
|
|
289
|
+
def error_tracker
|
|
290
|
+
@error_tracker ||= ErrorTracker.new
|
|
291
|
+
end
|
|
292
|
+
|
|
122
293
|
def enabled?
|
|
123
294
|
Tina4::Env.truthy?(ENV["TINA4_DEBUG"])
|
|
124
295
|
end
|
|
@@ -163,13 +334,16 @@ module Tina4
|
|
|
163
334
|
messages = mailbox.inbox
|
|
164
335
|
json_response({ messages: messages, count: messages.size, unread: mailbox.unread_count })
|
|
165
336
|
when ["GET", "/__dev/api/broken"]
|
|
166
|
-
|
|
337
|
+
errors = error_tracker.get(include_resolved: true)
|
|
338
|
+
h = error_tracker.health
|
|
339
|
+
json_response({ errors: errors, count: errors.size, health: h })
|
|
167
340
|
when ["POST", "/__dev/api/broken/resolve"]
|
|
168
341
|
body = read_json_body(env)
|
|
169
|
-
|
|
170
|
-
|
|
342
|
+
id = body && body["id"]
|
|
343
|
+
resolved = id ? error_tracker.resolve(id) : false
|
|
344
|
+
json_response({ resolved: resolved, id: id })
|
|
171
345
|
when ["POST", "/__dev/api/broken/clear"]
|
|
172
|
-
|
|
346
|
+
error_tracker.clear_resolved
|
|
173
347
|
json_response({ cleared: true })
|
|
174
348
|
when ["GET", "/__dev/api/websockets"]
|
|
175
349
|
json_response({ connections: [], count: 0 })
|
|
@@ -301,7 +475,8 @@ module Tina4
|
|
|
301
475
|
uptime: (Time.now - (defined?(@boot_time) && @boot_time ? @boot_time : (@boot_time = Time.now))).round(1),
|
|
302
476
|
route_count: Tina4::Router.routes.size,
|
|
303
477
|
request_stats: request_inspector.stats,
|
|
304
|
-
message_counts: message_log.count
|
|
478
|
+
message_counts: message_log.count,
|
|
479
|
+
health: error_tracker.health
|
|
305
480
|
}
|
|
306
481
|
end
|
|
307
482
|
|
data/lib/tina4/frond.rb
CHANGED
|
@@ -197,20 +197,14 @@ module Tina4
|
|
|
197
197
|
raise "Template not found: #{path}" unless File.exist?(path)
|
|
198
198
|
|
|
199
199
|
debug_mode = ENV.fetch("TINA4_DEBUG", "").downcase == "true"
|
|
200
|
-
cached = @compiled[template]
|
|
201
200
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if cached[1] == mtime
|
|
207
|
-
return execute_cached(cached[0], context)
|
|
208
|
-
end
|
|
209
|
-
else
|
|
210
|
-
# Production: skip mtime check, cache is permanent
|
|
211
|
-
return execute_cached(cached[0], context)
|
|
212
|
-
end
|
|
201
|
+
unless debug_mode
|
|
202
|
+
# Production: use permanent cache (no filesystem checks)
|
|
203
|
+
cached = @compiled[template]
|
|
204
|
+
return execute_cached(cached[0], context) if cached
|
|
213
205
|
end
|
|
206
|
+
# Dev mode: skip cache entirely — always re-read and re-tokenize
|
|
207
|
+
# so edits to partials and extended base templates are detected
|
|
214
208
|
|
|
215
209
|
# Cache miss — load, tokenize, cache
|
|
216
210
|
source = File.read(path, encoding: "utf-8")
|
|
@@ -1429,7 +1423,7 @@ module Tina4
|
|
|
1429
1423
|
param_names.each_with_index do |pname, pi|
|
|
1430
1424
|
macro_ctx[pname] = pi < args.length ? args[pi] : nil
|
|
1431
1425
|
end
|
|
1432
|
-
engine.send(:render_tokens, captured_body.dup, macro_ctx)
|
|
1426
|
+
Tina4::SafeString.new(engine.send(:render_tokens, captured_body.dup, macro_ctx))
|
|
1433
1427
|
}
|
|
1434
1428
|
|
|
1435
1429
|
i
|
|
@@ -1486,7 +1480,7 @@ module Tina4
|
|
|
1486
1480
|
param_names.each_with_index do |pname, pi|
|
|
1487
1481
|
macro_ctx[pname] = pi < args.length ? args[pi] : nil
|
|
1488
1482
|
end
|
|
1489
|
-
engine.send(:render_tokens, body_tokens.dup, macro_ctx)
|
|
1483
|
+
Tina4::SafeString.new(engine.send(:render_tokens, body_tokens.dup, macro_ctx))
|
|
1490
1484
|
}
|
|
1491
1485
|
end
|
|
1492
1486
|
|
data/lib/tina4/orm.rb
CHANGED
|
@@ -388,21 +388,22 @@ module Tina4
|
|
|
388
388
|
pk = self.class.primary_key_field || :id
|
|
389
389
|
pk_value = __send__(pk)
|
|
390
390
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
391
|
+
self.class.db.transaction do |db|
|
|
392
|
+
if @persisted && pk_value
|
|
393
|
+
filter = { pk => pk_value }
|
|
394
|
+
data.delete(pk)
|
|
395
|
+
# Remove mapped primary key too
|
|
396
|
+
mapped_pk = self.class.field_mapping[pk.to_s]
|
|
397
|
+
data.delete(mapped_pk.to_sym) if mapped_pk
|
|
398
|
+
db.update(self.class.table_name, data, filter)
|
|
399
|
+
else
|
|
400
|
+
result = db.insert(self.class.table_name, data)
|
|
401
|
+
if result[:last_id] && respond_to?("#{pk}=")
|
|
402
|
+
__send__("#{pk}=", result[:last_id])
|
|
403
|
+
end
|
|
404
|
+
@persisted = true
|
|
402
405
|
end
|
|
403
|
-
@persisted = true
|
|
404
406
|
end
|
|
405
|
-
self.class.db.commit
|
|
406
407
|
true
|
|
407
408
|
rescue => e
|
|
408
409
|
@errors << e.message
|
|
@@ -414,17 +415,17 @@ module Tina4
|
|
|
414
415
|
pk_value = __send__(pk)
|
|
415
416
|
return false unless pk_value
|
|
416
417
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
418
|
+
self.class.db.transaction do |db|
|
|
419
|
+
if self.class.soft_delete
|
|
420
|
+
db.update(
|
|
421
|
+
self.class.table_name,
|
|
422
|
+
{ self.class.soft_delete_field => 1 },
|
|
423
|
+
{ pk => pk_value }
|
|
424
|
+
)
|
|
425
|
+
else
|
|
426
|
+
db.delete(self.class.table_name, { pk => pk_value })
|
|
427
|
+
end
|
|
426
428
|
end
|
|
427
|
-
self.class.db.commit
|
|
428
429
|
@persisted = false
|
|
429
430
|
true
|
|
430
431
|
end
|
|
@@ -434,8 +435,9 @@ module Tina4
|
|
|
434
435
|
pk_value = __send__(pk)
|
|
435
436
|
raise "Cannot delete: no primary key value" unless pk_value
|
|
436
437
|
|
|
437
|
-
self.class.db.
|
|
438
|
-
|
|
438
|
+
self.class.db.transaction do |db|
|
|
439
|
+
db.delete(self.class.table_name, { pk => pk_value })
|
|
440
|
+
end
|
|
439
441
|
@persisted = false
|
|
440
442
|
true
|
|
441
443
|
end
|
|
@@ -447,12 +449,13 @@ module Tina4
|
|
|
447
449
|
pk_value = __send__(pk)
|
|
448
450
|
raise "Cannot restore: no primary key value" unless pk_value
|
|
449
451
|
|
|
450
|
-
self.class.db.
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
452
|
+
self.class.db.transaction do |db|
|
|
453
|
+
db.update(
|
|
454
|
+
self.class.table_name,
|
|
455
|
+
{ self.class.soft_delete_field => 0 },
|
|
456
|
+
{ pk => pk_value }
|
|
457
|
+
)
|
|
458
|
+
end
|
|
456
459
|
__send__("#{self.class.soft_delete_field}=", 0) if respond_to?("#{self.class.soft_delete_field}=")
|
|
457
460
|
true
|
|
458
461
|
end
|
data/lib/tina4/rack_app.rb
CHANGED
|
@@ -433,7 +433,7 @@ module Tina4
|
|
|
433
433
|
<div class="hero">
|
|
434
434
|
<img src="/images/tina4-logo-icon.webp" class="logo" alt="Tina4">
|
|
435
435
|
<h1>Tina4Ruby</h1>
|
|
436
|
-
<p class="tagline">This
|
|
436
|
+
<p class="tagline">This Is Now A 4Framework</p>
|
|
437
437
|
<div class="actions">
|
|
438
438
|
<a href="https://tina4.com/ruby" class="btn" target="_blank">Website</a>
|
|
439
439
|
<a href="/__dev" class="btn">Dev Admin</a>
|
data/lib/tina4/version.rb
CHANGED
data/lib/tina4.rb
CHANGED
|
@@ -134,7 +134,7 @@ module Tina4
|
|
|
134
134
|
end
|
|
135
135
|
|
|
136
136
|
puts "#{color}#{BANNER}#{reset}"
|
|
137
|
-
puts " Tina4 Ruby v#{VERSION} — This
|
|
137
|
+
puts " Tina4 Ruby v#{VERSION} — This Is Now A 4Framework"
|
|
138
138
|
puts ""
|
|
139
139
|
puts " Server: http://#{display}:#{port} (#{server_name})"
|
|
140
140
|
puts " Swagger: http://localhost:#{port}/swagger"
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tina4ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.10.
|
|
4
|
+
version: 3.10.30
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tina4 Team
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-03-30 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: rack
|
|
@@ -399,6 +400,7 @@ licenses:
|
|
|
399
400
|
- MIT
|
|
400
401
|
metadata:
|
|
401
402
|
homepage_uri: https://tina4.com
|
|
403
|
+
post_install_message:
|
|
402
404
|
rdoc_options: []
|
|
403
405
|
require_paths:
|
|
404
406
|
- lib
|
|
@@ -413,7 +415,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
413
415
|
- !ruby/object:Gem::Version
|
|
414
416
|
version: '0'
|
|
415
417
|
requirements: []
|
|
416
|
-
rubygems_version: 4.
|
|
418
|
+
rubygems_version: 3.4.19
|
|
419
|
+
signing_key:
|
|
417
420
|
specification_version: 4
|
|
418
421
|
summary: Simple. Fast. Human. This is not a framework.
|
|
419
422
|
test_files: []
|