tina4ruby 3.13.93 → 3.13.96
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 +883 -0
- data/README.md +1 -1
- data/lib/tina4/auth.rb +166 -87
- data/lib/tina4/auto_crud.rb +29 -32
- data/lib/tina4/cache_backends/base_backend.rb +19 -0
- data/lib/tina4/cache_backends/database_backend.rb +29 -0
- data/lib/tina4/cache_backends/memcached_backend.rb +124 -13
- data/lib/tina4/cache_backends/memory_backend.rb +15 -0
- data/lib/tina4/cache_backends/redis_backend.rb +173 -52
- data/lib/tina4/cache_backends.rb +10 -1
- data/lib/tina4/cli.rb +35 -43
- data/lib/tina4/cors.rb +186 -30
- data/lib/tina4/database/sqlite3_adapter.rb +4 -1
- data/lib/tina4/database.rb +458 -48
- data/lib/tina4/database_adapter.rb +178 -0
- data/lib/tina4/database_result.rb +63 -17
- data/lib/tina4/database_url.rb +363 -0
- data/lib/tina4/dev.rb +0 -1
- data/lib/tina4/dev_admin.rb +118 -20
- data/lib/tina4/dev_mailbox.rb +5 -1
- data/lib/tina4/dispatch_pipeline.rb +605 -0
- data/lib/tina4/docstore.rb +274 -60
- data/lib/tina4/drivers/firebird_driver.rb +118 -4
- data/lib/tina4/drivers/mongodb_driver.rb +19 -4
- data/lib/tina4/drivers/mssql_driver.rb +73 -10
- data/lib/tina4/drivers/mysql_driver.rb +71 -4
- data/lib/tina4/drivers/odbc_driver.rb +40 -4
- data/lib/tina4/drivers/postgres_driver.rb +97 -10
- data/lib/tina4/drivers/sqlite_driver.rb +25 -3
- data/lib/tina4/env.rb +176 -34
- data/lib/tina4/field_types.rb +12 -0
- data/lib/tina4/frond.rb +102 -10
- data/lib/tina4/health.rb +30 -14
- data/lib/tina4/job.rb +15 -5
- data/lib/tina4/log.rb +236 -32
- data/lib/tina4/mcp.rb +11 -5
- data/lib/tina4/messenger.rb +317 -82
- data/lib/tina4/metrics.rb +179 -891
- data/lib/tina4/middleware.rb +191 -56
- data/lib/tina4/migration.rb +17 -1
- data/lib/tina4/orm.rb +114 -17
- data/lib/tina4/public/css/tina4.min.css +1 -1
- data/lib/tina4/queue.rb +154 -9
- data/lib/tina4/queue_backends/kafka_backend.rb +191 -2
- data/lib/tina4/queue_backends/lite_backend.rb +121 -25
- data/lib/tina4/queue_backends/mongo_backend.rb +146 -10
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +194 -1
- data/lib/tina4/rack_app.rb +94 -316
- data/lib/tina4/request.rb +48 -8
- data/lib/tina4/response.rb +42 -1
- data/lib/tina4/response_cache.rb +142 -24
- data/lib/tina4/router.rb +141 -12
- data/lib/tina4/session.rb +243 -29
- data/lib/tina4/session_handlers/database_handler.rb +185 -20
- data/lib/tina4/session_handlers/file_handler.rb +113 -21
- data/lib/tina4/session_handlers/memcached_handler.rb +183 -0
- data/lib/tina4/session_handlers/mongo_handler.rb +232 -15
- data/lib/tina4/session_handlers/mongo_wire_client.rb +300 -0
- data/lib/tina4/session_handlers/redis_handler.rb +20 -6
- data/lib/tina4/session_handlers/valkey_handler.rb +18 -4
- data/lib/tina4/shutdown.rb +180 -30
- data/lib/tina4/sql_translator.rb +110 -0
- data/lib/tina4/swagger.rb +50 -18
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +28 -6
- data/lib/tina4.rb +301 -38
- metadata +35 -17
- data/lib/tina4/scss_compiler.rb +0 -349
data/lib/tina4/dev_admin.rb
CHANGED
|
@@ -467,18 +467,13 @@ module Tina4
|
|
|
467
467
|
when ["GET", "/__dev/api/system"]
|
|
468
468
|
json_response(system_payload)
|
|
469
469
|
when ["GET", "/__dev/api/queue/topics"]
|
|
470
|
-
|
|
471
|
-
topics = Dir.exist?(queue_dir) ? Dir.children(queue_dir).select { |d| File.directory?(File.join(queue_dir, d)) }.sort : []
|
|
472
|
-
topics = ["default"] if topics.empty?
|
|
473
|
-
json_response({ topics: topics })
|
|
470
|
+
json_response({ topics: queue_topics })
|
|
474
471
|
when ["GET", "/__dev/api/queue/dead-letters"]
|
|
475
472
|
topic = query_param(env, "topic") || "default"
|
|
476
|
-
jobs =
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
rescue StandardError => e
|
|
481
|
-
jobs = []
|
|
473
|
+
jobs = begin
|
|
474
|
+
queue_dead_letters(topic)
|
|
475
|
+
rescue StandardError
|
|
476
|
+
[]
|
|
482
477
|
end
|
|
483
478
|
json_response({ jobs: jobs, count: jobs.size, topic: topic })
|
|
484
479
|
when ["GET", "/__dev/api/queue"]
|
|
@@ -492,15 +487,14 @@ module Tina4
|
|
|
492
487
|
# positionally raises ArgumentError, which the rescue below
|
|
493
488
|
# swallows — silently zeroing every stat. Use keyword form.
|
|
494
489
|
stats = {
|
|
495
|
-
pending: queue.
|
|
496
|
-
completed: queue.
|
|
497
|
-
failed: queue.
|
|
498
|
-
reserved: queue.
|
|
490
|
+
pending: queue.size(status: "pending"),
|
|
491
|
+
completed: queue.size(status: "completed"),
|
|
492
|
+
failed: queue.size(status: "failed"),
|
|
493
|
+
reserved: queue.size(status: "reserved"),
|
|
499
494
|
}
|
|
500
|
-
jobs
|
|
501
|
-
jobs.concat(queue.dead_letters.map { |j| j.merge(status: "dead_letter") }) if queue.respond_to?(:dead_letters)
|
|
495
|
+
jobs = queue_jobs(topic, query_param(env, "status"))
|
|
502
496
|
end
|
|
503
|
-
rescue StandardError
|
|
497
|
+
rescue StandardError
|
|
504
498
|
# fall through to empty stats
|
|
505
499
|
end
|
|
506
500
|
json_response({ jobs: jobs, stats: stats })
|
|
@@ -631,10 +625,25 @@ module Tina4
|
|
|
631
625
|
when ["GET", "/__dev/api/metrics"]
|
|
632
626
|
json_response(Tina4::Metrics.quick_metrics)
|
|
633
627
|
when ["GET", "/__dev/api/metrics/full"]
|
|
634
|
-
|
|
628
|
+
# No fallback (ADR-0002). A missing or stale CLI is a 503 naming the
|
|
629
|
+
# install command, never zeros that read as a healthy codebase.
|
|
630
|
+
begin
|
|
631
|
+
json_response(Tina4::Metrics.full_analysis)
|
|
632
|
+
rescue Tina4::MetricsEngineError => e
|
|
633
|
+
json_response({ "error" => e.message }, 503)
|
|
634
|
+
end
|
|
635
635
|
when ["GET", "/__dev/api/metrics/file"]
|
|
636
636
|
file_path = (query_param(env, "path") || "").to_s
|
|
637
|
-
|
|
637
|
+
begin
|
|
638
|
+
json_response(Tina4::Metrics.file_detail(file_path))
|
|
639
|
+
rescue Tina4::MetricsEngineError => e
|
|
640
|
+
# A bad path is the caller's mistake (404); anything else is the
|
|
641
|
+
# engine being unavailable (503).
|
|
642
|
+
bad_path = e.message.include?("no such file") ||
|
|
643
|
+
e.message.include?("not a file") ||
|
|
644
|
+
e.message.include?("needs a path")
|
|
645
|
+
json_response({ "error" => e.message }, bad_path ? 404 : 503)
|
|
646
|
+
end
|
|
638
647
|
when ["GET", "/__dev/api/thoughts"]
|
|
639
648
|
json_response(thoughts_payload)
|
|
640
649
|
when ["POST", "/__dev/api/supervise/create"]
|
|
@@ -805,7 +814,16 @@ module Tina4
|
|
|
805
814
|
platform: RUBY_PLATFORM,
|
|
806
815
|
debug: ENV["TINA4_DEBUG"] || "false",
|
|
807
816
|
log_level: ENV["TINA4_LOG_LEVEL"] || "ERROR",
|
|
808
|
-
|
|
817
|
+
# REDACTED, not raw. Measured 2026-08-02: this served
|
|
818
|
+
# TINA4_DATABASE_URL verbatim, so GET /__dev/api/status answered any
|
|
819
|
+
# caller that could reach the debug port with the database password.
|
|
820
|
+
# It is a display field - nothing round-trips it back - so the safe
|
|
821
|
+
# form is strictly better here. (The connections EDITOR at
|
|
822
|
+
# /__dev/api/connections still returns the raw URL on purpose: it
|
|
823
|
+
# populates a form that is saved back to .env, and redacting it there
|
|
824
|
+
# would write *** into the file. That one needs an unchanged-sentinel
|
|
825
|
+
# design, in all four frameworks.)
|
|
826
|
+
database: (ENV["TINA4_DATABASE_URL"].to_s.empty? ? "not configured" : Tina4::DatabaseUrl.redact(ENV["TINA4_DATABASE_URL"])),
|
|
809
827
|
db_tables: db_table_count,
|
|
810
828
|
uptime: (Time.now - (defined?(@boot_time) && @boot_time ? @boot_time : (@boot_time = Time.now))).round(1),
|
|
811
829
|
route_count: Tina4::Router.routes.size,
|
|
@@ -1053,6 +1071,86 @@ module Tina4
|
|
|
1053
1071
|
{ ok: false, error: e.message }
|
|
1054
1072
|
end
|
|
1055
1073
|
|
|
1074
|
+
# ── Queue panel: the list must describe the set the stats count ──
|
|
1075
|
+
#
|
|
1076
|
+
# Every reader below goes through Tina4::Queue.base_path / .topic_path,
|
|
1077
|
+
# or through the backend itself — the SAME answers the lite backend
|
|
1078
|
+
# writes to and Queue#size counts. These handlers used to re-derive the
|
|
1079
|
+
# path as
|
|
1080
|
+
# Dir.pwd/data/queue, which no Ruby app ever wrote to, so the topic list
|
|
1081
|
+
# could not name a real topic and the job list described a store that was
|
|
1082
|
+
# not the one being counted.
|
|
1083
|
+
#
|
|
1084
|
+
# Each job appears EXACTLY ONCE, in the bucket its OWN stat counts it in:
|
|
1085
|
+
#
|
|
1086
|
+
# stats.pending <- <base>/<topic>/*.queue-data
|
|
1087
|
+
# stats.reserved <- <base>/<topic>/reserved/*.queue-data
|
|
1088
|
+
# stats.failed <- <base>/dead_letter/*.queue-data tagged with topic
|
|
1089
|
+
# stats.completed <- always 0; the file backend deletes on complete
|
|
1090
|
+
#
|
|
1091
|
+
# so sum(stats) == jobs.length by construction on a quiescent store, and
|
|
1092
|
+
# every ?status= filter returns exactly what its own stat counts.
|
|
1093
|
+
|
|
1094
|
+
# Topic directories in the real store. The shared dead-letter directory
|
|
1095
|
+
# is a sibling of the topic directories, not a topic.
|
|
1096
|
+
def queue_topics
|
|
1097
|
+
base = Tina4::Queue.base_path
|
|
1098
|
+
return ["default"] unless Dir.exist?(base)
|
|
1099
|
+
|
|
1100
|
+
topics = Dir.children(base)
|
|
1101
|
+
.select { |entry| File.directory?(File.join(base, entry)) }
|
|
1102
|
+
.reject { |entry| entry == Tina4::Queue::DEAD_LETTER_DIRNAME }
|
|
1103
|
+
.sort
|
|
1104
|
+
topics.empty? ? ["default"] : topics
|
|
1105
|
+
rescue StandardError
|
|
1106
|
+
["default"]
|
|
1107
|
+
end
|
|
1108
|
+
|
|
1109
|
+
# Jobs for +topic+, optionally narrowed to one bucket.
|
|
1110
|
+
def queue_jobs(topic, status_filter = nil)
|
|
1111
|
+
wanted = status_filter.to_s.strip
|
|
1112
|
+
topic_dir = Tina4::Queue.topic_path(topic)
|
|
1113
|
+
jobs = []
|
|
1114
|
+
jobs.concat(read_queue_dir(topic_dir, "pending")) if wanted.empty? || wanted == "pending"
|
|
1115
|
+
if wanted.empty? || wanted == "reserved"
|
|
1116
|
+
jobs.concat(read_queue_dir(File.join(topic_dir, "reserved"), "reserved"))
|
|
1117
|
+
end
|
|
1118
|
+
jobs.concat(queue_dead_letters(topic)) if wanted.empty? || wanted == "failed" || wanted == "dead"
|
|
1119
|
+
jobs
|
|
1120
|
+
end
|
|
1121
|
+
|
|
1122
|
+
# Read one queue directory the way Queue#size COUNTS it. size globs the
|
|
1123
|
+
# files without parsing them, so a job file that cannot be parsed still
|
|
1124
|
+
# counts — and must therefore still be LISTED, or the panel shows a total
|
|
1125
|
+
# it cannot account for. Listing it is also the only way an operator ever
|
|
1126
|
+
# learns the file is there.
|
|
1127
|
+
def read_queue_dir(dir, status)
|
|
1128
|
+
return [] unless Dir.exist?(dir)
|
|
1129
|
+
|
|
1130
|
+
Tina4::Queue.job_files(dir).filter_map do |file|
|
|
1131
|
+
begin
|
|
1132
|
+
JSON.parse(File.read(file)).merge("status" => status)
|
|
1133
|
+
rescue JSON::ParserError
|
|
1134
|
+
{ "id" => File.basename(file, Tina4::Queue::JOB_EXTENSION), "status" => status,
|
|
1135
|
+
"error" => "unreadable job file" }
|
|
1136
|
+
rescue Errno::ENOENT
|
|
1137
|
+
nil # consumed between the glob and the read
|
|
1138
|
+
end
|
|
1139
|
+
end
|
|
1140
|
+
end
|
|
1141
|
+
|
|
1142
|
+
# Every dead letter for +topic+ — the same set stats.failed counts.
|
|
1143
|
+
#
|
|
1144
|
+
# max_retries: 0 is the established "no attempt-count filter" spelling
|
|
1145
|
+
# (the `tina4ruby queue retry` command uses it for the same reason). The
|
|
1146
|
+
# DEFAULT filters on the max_retries of the Queue this dev admin happened
|
|
1147
|
+
# to construct, which is 3 and has nothing to do with the app's: an app
|
|
1148
|
+
# configured max_retries: 1 dead-letters a job at ONE attempt, and every
|
|
1149
|
+
# one of those was counted by stats.failed and never listed.
|
|
1150
|
+
def queue_dead_letters(topic)
|
|
1151
|
+
dev_queue(topic).dead_letters(max_retries: 0).map { |job| job.merge("status" => "dead_letter") }
|
|
1152
|
+
end
|
|
1153
|
+
|
|
1056
1154
|
# ── Queue run-chips (Tier 3) ───────────────────────────────────
|
|
1057
1155
|
|
|
1058
1156
|
# A file-backed dev Queue for +topic+ (matches the GET /queue handler).
|
data/lib/tina4/dev_mailbox.rb
CHANGED
|
@@ -16,7 +16,10 @@ module Tina4
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
# Capture an outgoing email to the local filesystem instead of sending
|
|
19
|
-
|
|
19
|
+
# text: carries the plain-text alternative onto the dev path. Every framework
|
|
20
|
+
# used to drop it, so what you inspected in the mailbox was not what would have
|
|
21
|
+
# been sent -- a mailbox that shows you a different message is worse than none.
|
|
22
|
+
def capture(to:, subject:, body:, html: false, text: nil, cc: [], bcc: [],
|
|
20
23
|
reply_to: nil, from_address: nil, from_name: nil, attachments: [])
|
|
21
24
|
msg_id = SecureRandom.uuid
|
|
22
25
|
timestamp = Time.now
|
|
@@ -30,6 +33,7 @@ module Tina4
|
|
|
30
33
|
reply_to: reply_to,
|
|
31
34
|
subject: subject,
|
|
32
35
|
body: body,
|
|
36
|
+
text: text,
|
|
33
37
|
html: html,
|
|
34
38
|
attachments: store_attachments(msg_id, attachments),
|
|
35
39
|
read: false,
|