tina4ruby 3.13.132 → 3.13.133
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 +90 -0
- data/lib/tina4/dev_admin.rb +256 -157
- data/lib/tina4/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 83f372e751a027c81f97a04d5002b5dd756f16d9f94bc9a9b771c820f0878619
|
|
4
|
+
data.tar.gz: 6436d18bc08e5dd6f5b6232feb90a7e2650a728064420f12e505eb279e3b941c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8c214ae801254e82779984e62aabf4c07895c45109e25a75a28ea48aecb41aef707edfdbaed6a86db94e458b3bfdf06df783fc1e398bd7e77a1bc7796cc967b8
|
|
7
|
+
data.tar.gz: e98653bdc54a68d5c9c205645651b0b682ce1d23912d5dc437d774161636d09abfe1225fc01366ddcc2f0ad38f69fe2bd9984dc77579541f5876bae41e973c4b
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,96 @@ number means the same thing everywhere.
|
|
|
6
6
|
**The authoritative release notes for every shipped version live in the documentation:**
|
|
7
7
|
https://tina4.com/ruby/36-releases
|
|
8
8
|
|
|
9
|
+
## 3.13.133
|
|
10
|
+
|
|
11
|
+
Maintainability pass, no behavior changes. Swagger.generate decomposed into cohesive
|
|
12
|
+
helpers (Python 64->6, PHP 83->6, Node 52->2 cyclomatic; Ruby already lean) with the
|
|
13
|
+
OpenAPI document byte-for-byte identical. Ruby dev-admin handle_request split from a
|
|
14
|
+
138-branch dispatcher into per-concern methods (CC 138->11); routes, responses, and
|
|
15
|
+
the localhost/auth gate unchanged. Full notes: https://tina4.com/python/36-releases
|
|
16
|
+
|
|
17
|
+
## 3.13.132
|
|
18
|
+
|
|
19
|
+
ORM reads return a ModelCollection carrying the query total (ADR-0064): where,
|
|
20
|
+
select, find (filter form), all, and with_trashed hand back the page of models AND
|
|
21
|
+
the total for the filter, so you paginate without a second query. get_total_records()
|
|
22
|
+
/ to_paginate() (Python, Ruby); getTotalRecords() / toPaginate() (PHP, Node). The
|
|
23
|
+
total reuses the count the query already ran - zero extra queries. Python's
|
|
24
|
+
where(..., with_count=True) tuple is removed; PHP's return type changes from a bare
|
|
25
|
+
array to an array-compatible ModelCollection object (toArray() escape hatch). Also:
|
|
26
|
+
the dev server binds dual-stack loopback so localhost works on Windows; the dev-admin
|
|
27
|
+
agent chat surface is removed and the dashboard no longer self-reloads; Firebird DDL
|
|
28
|
+
no longer reads a row count and Firebird adapters close cleanly on GC (Python); the
|
|
29
|
+
Valkey session handler subclasses Redis and shared fetch/execute/Frond loops are
|
|
30
|
+
hoisted (behaviour unchanged). Full notes: https://tina4.com/python/36-releases
|
|
31
|
+
|
|
32
|
+
## 3.13.131
|
|
33
|
+
|
|
34
|
+
AI skills for a global team: plain English, short replies, effort matched to the
|
|
35
|
+
task, ask-before-guess, and a grounding fallback for tools without the coder server.
|
|
36
|
+
|
|
37
|
+
## 3.13.130
|
|
38
|
+
|
|
39
|
+
AI skills discipline pass: objective on ideas (disagree when a design is weak),
|
|
40
|
+
claim only what was verified, and chat instead of narrate.
|
|
41
|
+
|
|
42
|
+
## 3.13.129
|
|
43
|
+
|
|
44
|
+
tina4 generate model stops silently pluralising a reserved-word table. It prints a
|
|
45
|
+
note and offers --table-name, so a downstream route or query does not fail with no
|
|
46
|
+
clue the table was renamed.
|
|
47
|
+
|
|
48
|
+
## 3.13.128
|
|
49
|
+
|
|
50
|
+
tina4 lint: one command runs the project linter (ruff, phpcs, rubocop, or eslint),
|
|
51
|
+
installs it as a dev dependency on demand, and falls back to a zero-dependency syntax
|
|
52
|
+
baseline with --no-install.
|
|
53
|
+
|
|
54
|
+
## 3.13.127
|
|
55
|
+
|
|
56
|
+
A bare TRUE or FALSE now reaches SQL Server and Firebird as 1/0 in every framework.
|
|
57
|
+
Node, PHP and Ruby had skipped the boolean translation on SQL Server, and Ruby on
|
|
58
|
+
Firebird too.
|
|
59
|
+
|
|
60
|
+
## 3.13.126
|
|
61
|
+
|
|
62
|
+
The fake-data seeder reads the column name: a products.name column fills with product
|
|
63
|
+
names, not person names.
|
|
64
|
+
|
|
65
|
+
## 3.13.125
|
|
66
|
+
|
|
67
|
+
Scaffolded migrations apply on every engine. tina4 generate migration wrote
|
|
68
|
+
SQLite-only DDL (TEXT, REAL, CREATE TABLE IF NOT EXISTS); the generators now emit
|
|
69
|
+
engine-correct types so a migration runs on Firebird and the rest.
|
|
70
|
+
|
|
71
|
+
## 3.13.124
|
|
72
|
+
|
|
73
|
+
The database session backend works on Firebird in all four frameworks, with a
|
|
74
|
+
session-engine test in each so the gap cannot reopen.
|
|
75
|
+
|
|
76
|
+
## 3.13.123
|
|
77
|
+
|
|
78
|
+
tina4 generate queue <topic> returns the generate_v1_1 envelope: edit_hints[] naming
|
|
79
|
+
the handler line to fill and next[] naming what to run after.
|
|
80
|
+
|
|
81
|
+
## 3.13.122
|
|
82
|
+
|
|
83
|
+
The default Content-Security-Policy (default-src 'self') no longer fails silently:
|
|
84
|
+
when an app relies on something the policy blocks, the framework surfaces it instead
|
|
85
|
+
of leaving a blank page.
|
|
86
|
+
|
|
87
|
+
## 3.13.121
|
|
88
|
+
|
|
89
|
+
Every migration surface speaks the ADR-0063 generate_v1_1 envelope - the CLI
|
|
90
|
+
migrate:create, the MCP migration_create, and the rest, not just tina4 generate
|
|
91
|
+
migration. Adds a pre-tag release guard.
|
|
92
|
+
|
|
93
|
+
## 3.13.120
|
|
94
|
+
|
|
95
|
+
Scaffolding envelope v1.1 (ADR-0063): every tina4 generate verb reports what to edit
|
|
96
|
+
next and what to do after, not just where files went. Introduces the tina4-architect
|
|
97
|
+
skill.
|
|
98
|
+
|
|
9
99
|
## 3.13.119
|
|
10
100
|
|
|
11
101
|
Skill repair by @MichaelC8E (#44, merged as c1b8c43d5) plus a
|
data/lib/tina4/dev_admin.rb
CHANGED
|
@@ -395,6 +395,34 @@ module Tina4
|
|
|
395
395
|
denial = dev_mutation_denial(env)
|
|
396
396
|
return denial if denial
|
|
397
397
|
|
|
398
|
+
# Exact [method, path] dispatch, split by concern. Every route is an
|
|
399
|
+
# exact pair (no overlaps), so the groups form a partition: the first
|
|
400
|
+
# group that matches answers, and a group returns nil when it holds no
|
|
401
|
+
# matching route. Behaviour is identical to the former single 138-branch
|
|
402
|
+
# case — the only handler that legitimately returns nil is an MCP route
|
|
403
|
+
# with the capability disabled (with_mcp_gate), for which the original
|
|
404
|
+
# case also fell through to nil (RackApp then 404s it).
|
|
405
|
+
dispatch_dashboard(method, path, env) ||
|
|
406
|
+
dispatch_inspectors(method, path, env) ||
|
|
407
|
+
dispatch_mailbox(method, path, env) ||
|
|
408
|
+
dispatch_queue(method, path, env) ||
|
|
409
|
+
dispatch_data(method, path, env) ||
|
|
410
|
+
dispatch_config(method, path, env) ||
|
|
411
|
+
dispatch_files(method, path, env) ||
|
|
412
|
+
dispatch_mcp(method, path, env)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
private
|
|
417
|
+
|
|
418
|
+
# ── /__dev request dispatch (split from a single 138-CC case) ──────
|
|
419
|
+
# Each dispatch_* owns one concern's exact [method, path] routes and
|
|
420
|
+
# returns a Rack triple, or nil when it holds no matching route. Branch
|
|
421
|
+
# bodies are the former case's, moved verbatim.
|
|
422
|
+
|
|
423
|
+
# Dashboard shell, injected-toolbar assets, the reload trigger and the
|
|
424
|
+
# core status / routes / system panels.
|
|
425
|
+
def dispatch_dashboard(method, path, env)
|
|
398
426
|
case [method, path]
|
|
399
427
|
when ["GET", "/__dev"], ["GET", "/__dev/"]
|
|
400
428
|
serve_dashboard
|
|
@@ -413,54 +441,24 @@ module Tina4
|
|
|
413
441
|
when ["GET", "/__dev/api/mtime"]
|
|
414
442
|
json_response({ mtime: @reload_mtime || 0, file: @reload_file || "" })
|
|
415
443
|
when ["POST", "/__dev/api/reload"]
|
|
416
|
-
|
|
417
|
-
@reload_mtime = Time.now.to_i
|
|
418
|
-
@reload_file = body["file"] || ""
|
|
419
|
-
reload_type = body["type"] || "reload"
|
|
420
|
-
Tina4::Log.info("External reload trigger: #{reload_type}#{@reload_file.empty? ? '' : " (#{@reload_file})"}")
|
|
421
|
-
# Re-discover so files dropped into src/routes/ register without
|
|
422
|
-
# a server restart. Idempotent — already-loaded files are skipped,
|
|
423
|
-
# changed files are re-loaded (mtime-tracked).
|
|
424
|
-
begin
|
|
425
|
-
Tina4::Router.rescan_routes!
|
|
426
|
-
rescue StandardError => e
|
|
427
|
-
Tina4::Log.error("Re-discover on reload failed: #{e.message}")
|
|
428
|
-
end
|
|
429
|
-
# Keep the code Context index LIVE on the same reload trigger: reindex
|
|
430
|
-
# just the changed file (UPSERT) so the dev-MCP code_search reflects
|
|
431
|
-
# the edit immediately. Only touches an already-built index
|
|
432
|
-
# (existing_context never creates one); guarded so a context failure
|
|
433
|
-
# never breaks the reload.
|
|
434
|
-
begin
|
|
435
|
-
unless @reload_file.to_s.empty?
|
|
436
|
-
ctx = Tina4::Context.existing_context
|
|
437
|
-
ctx&.reindex_file(@reload_file)
|
|
438
|
-
end
|
|
439
|
-
rescue StandardError => e
|
|
440
|
-
Tina4::Log.error("Context reindex on reload failed: #{e.message}")
|
|
441
|
-
end
|
|
442
|
-
# WebSocket-primary reload: push an instant message to every browser
|
|
443
|
-
# connected on /__dev_reload. The toolbar client (and the dev-admin
|
|
444
|
-
# dashboard) act on this immediately — the mtime poll above is only a
|
|
445
|
-
# fallback for when the socket is down. CSS changes swap stylesheets;
|
|
446
|
-
# everything else triggers a full page reload. We normalise the wire
|
|
447
|
-
# `type` to "css"/"reload" (the clients only react to css/reload/change)
|
|
448
|
-
# but still echo the caller's original type in the HTTP response.
|
|
449
|
-
# Wrapped so a broadcast failure — or zero connected clients — never
|
|
450
|
-
# 500s the reload endpoint.
|
|
451
|
-
begin
|
|
452
|
-
ws_type = reload_type == "css" ? "css" : "reload"
|
|
453
|
-
Tina4::DevReload.broadcast(
|
|
454
|
-
JSON.generate({ type: ws_type, file: @reload_file, mtime: @reload_mtime })
|
|
455
|
-
)
|
|
456
|
-
rescue StandardError => e
|
|
457
|
-
Tina4::Log.error("Dev-reload WebSocket broadcast failed: #{e.message}")
|
|
458
|
-
end
|
|
459
|
-
json_response({ ok: true, type: reload_type })
|
|
444
|
+
handle_reload(env)
|
|
460
445
|
when ["GET", "/__dev/api/status"]
|
|
461
446
|
json_response(status_payload)
|
|
462
447
|
when ["GET", "/__dev/api/routes"]
|
|
463
448
|
json_response(routes_payload)
|
|
449
|
+
when ["GET", "/__dev/api/system"]
|
|
450
|
+
json_response(system_payload)
|
|
451
|
+
when ["GET", "/__dev/api/version-check"]
|
|
452
|
+
json_response(version_check_payload)
|
|
453
|
+
when ["GET", "/__dev/api/git/status"]
|
|
454
|
+
json_response(git_status_payload)
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# Observability panels: message log, request inspector and live
|
|
459
|
+
# WebSocket connections. (Mailbox + error tracker live in dispatch_mailbox.)
|
|
460
|
+
def dispatch_inspectors(method, path, env)
|
|
461
|
+
case [method, path]
|
|
464
462
|
when ["GET", "/__dev/api/messages"]
|
|
465
463
|
category = query_param(env, "category")
|
|
466
464
|
messages = message_log.get(category: category)
|
|
@@ -471,68 +469,31 @@ module Tina4
|
|
|
471
469
|
category = body["category"] if body
|
|
472
470
|
message_log.clear(category: category)
|
|
473
471
|
json_response({ cleared: true })
|
|
472
|
+
when ["GET", "/__dev/api/messages/search"]
|
|
473
|
+
keyword = query_param(env, "q") || query_param(env, "keyword") || ""
|
|
474
|
+
all_messages = message_log.get
|
|
475
|
+
filtered = keyword.empty? ? all_messages : all_messages.select { |m| m[:message].to_s.downcase.include?(keyword.downcase) }
|
|
476
|
+
json_response({ messages: filtered, count: filtered.size, keyword: keyword })
|
|
474
477
|
when ["GET", "/__dev/api/requests"]
|
|
475
478
|
limit = (query_param(env, "limit") || 50).to_i
|
|
476
479
|
json_response({ requests: request_inspector.get(limit: limit), stats: request_inspector.stats })
|
|
477
480
|
when ["POST", "/__dev/api/requests/clear"]
|
|
478
481
|
request_inspector.clear
|
|
479
482
|
json_response({ cleared: true })
|
|
480
|
-
when ["GET", "/__dev/api/system"]
|
|
481
|
-
json_response(system_payload)
|
|
482
|
-
when ["GET", "/__dev/api/queue/topics"]
|
|
483
|
-
json_response({ topics: queue_topics })
|
|
484
|
-
when ["GET", "/__dev/api/queue/dead-letters"]
|
|
485
|
-
topic = query_param(env, "topic") || "default"
|
|
486
|
-
jobs = begin
|
|
487
|
-
queue_dead_letters(topic)
|
|
488
|
-
rescue StandardError
|
|
489
|
-
[]
|
|
490
|
-
end
|
|
491
|
-
json_response({ jobs: jobs, count: jobs.size, topic: topic })
|
|
492
|
-
when ["GET", "/__dev/api/queue"]
|
|
493
|
-
topic = query_param(env, "topic") || "default"
|
|
494
|
-
stats = { pending: 0, completed: 0, failed: 0, reserved: 0 }
|
|
495
|
-
jobs = []
|
|
496
|
-
begin
|
|
497
|
-
if defined?(Tina4::Queue)
|
|
498
|
-
queue = Tina4::Queue.new(backend: :file, topic: topic)
|
|
499
|
-
# Queue#size is keyword-only (def size(status:)). Calling it
|
|
500
|
-
# positionally raises ArgumentError, which the rescue below
|
|
501
|
-
# swallows — silently zeroing every stat. Use keyword form.
|
|
502
|
-
stats = {
|
|
503
|
-
pending: queue.size(status: "pending"),
|
|
504
|
-
completed: queue.size(status: "completed"),
|
|
505
|
-
failed: queue.size(status: "failed"),
|
|
506
|
-
reserved: queue.size(status: "reserved"),
|
|
507
|
-
}
|
|
508
|
-
jobs = queue_jobs(topic, query_param(env, "status"))
|
|
509
|
-
end
|
|
510
|
-
rescue StandardError
|
|
511
|
-
# fall through to empty stats
|
|
512
|
-
end
|
|
513
|
-
json_response({ jobs: jobs, stats: stats })
|
|
514
|
-
when ["GET", "/__dev/api/mailbox"]
|
|
515
|
-
messages = mailbox.inbox
|
|
516
|
-
json_response({ messages: messages, count: messages.size, unread: mailbox.unread_count })
|
|
517
|
-
when ["GET", "/__dev/api/broken"]
|
|
518
|
-
errors = error_tracker.get(include_resolved: true)
|
|
519
|
-
h = error_tracker.health
|
|
520
|
-
json_response({ errors: errors, count: errors.size, health: h })
|
|
521
|
-
when ["POST", "/__dev/api/broken/resolve"]
|
|
522
|
-
body = read_json_body(env)
|
|
523
|
-
id = body && body["id"]
|
|
524
|
-
resolved = id ? error_tracker.resolve(id) : false
|
|
525
|
-
json_response({ resolved: resolved, id: id })
|
|
526
|
-
when ["POST", "/__dev/api/broken/clear"]
|
|
527
|
-
# "Clear All" button — flush every tracked error, not only the
|
|
528
|
-
# ones individually marked resolved. Matches PHP/Python.
|
|
529
|
-
error_tracker.clear_all
|
|
530
|
-
json_response({ cleared: true })
|
|
531
483
|
when ["GET", "/__dev/api/websockets"]
|
|
532
484
|
json_response(websockets_payload)
|
|
533
485
|
when ["POST", "/__dev/api/websockets/disconnect"]
|
|
534
486
|
body = read_json_body(env) || {}
|
|
535
487
|
json_response(websockets_disconnect(body))
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# Dev mailbox (email capture) and the error tracker panel.
|
|
492
|
+
def dispatch_mailbox(method, path, env)
|
|
493
|
+
case [method, path]
|
|
494
|
+
when ["GET", "/__dev/api/mailbox"]
|
|
495
|
+
messages = mailbox.inbox
|
|
496
|
+
json_response({ messages: messages, count: messages.size, unread: mailbox.unread_count })
|
|
536
497
|
when ["GET", "/__dev/api/mailbox/read"]
|
|
537
498
|
message_id = query_param(env, "id")
|
|
538
499
|
message = mailbox.read(message_id)
|
|
@@ -550,11 +511,39 @@ module Tina4
|
|
|
550
511
|
when ["POST", "/__dev/api/mailbox/clear"]
|
|
551
512
|
mailbox.clear
|
|
552
513
|
json_response({ cleared: true })
|
|
553
|
-
when ["GET", "/__dev/api/
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
514
|
+
when ["GET", "/__dev/api/broken"]
|
|
515
|
+
errors = error_tracker.get(include_resolved: true)
|
|
516
|
+
h = error_tracker.health
|
|
517
|
+
json_response({ errors: errors, count: errors.size, health: h })
|
|
518
|
+
when ["POST", "/__dev/api/broken/resolve"]
|
|
519
|
+
body = read_json_body(env)
|
|
520
|
+
id = body && body["id"]
|
|
521
|
+
resolved = id ? error_tracker.resolve(id) : false
|
|
522
|
+
json_response({ resolved: resolved, id: id })
|
|
523
|
+
when ["POST", "/__dev/api/broken/clear"]
|
|
524
|
+
# "Clear All" button — flush every tracked error, not only the
|
|
525
|
+
# ones individually marked resolved. Matches PHP/Python.
|
|
526
|
+
error_tracker.clear_all
|
|
527
|
+
json_response({ cleared: true })
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
# Queue panel: topic list, dead letters, per-topic overview and the
|
|
532
|
+
# retry / purge / replay mutations.
|
|
533
|
+
def dispatch_queue(method, path, env)
|
|
534
|
+
case [method, path]
|
|
535
|
+
when ["GET", "/__dev/api/queue/topics"]
|
|
536
|
+
json_response({ topics: queue_topics })
|
|
537
|
+
when ["GET", "/__dev/api/queue/dead-letters"]
|
|
538
|
+
topic = query_param(env, "topic") || "default"
|
|
539
|
+
jobs = begin
|
|
540
|
+
queue_dead_letters(topic)
|
|
541
|
+
rescue StandardError
|
|
542
|
+
[]
|
|
543
|
+
end
|
|
544
|
+
json_response({ jobs: jobs, count: jobs.size, topic: topic })
|
|
545
|
+
when ["GET", "/__dev/api/queue"]
|
|
546
|
+
queue_overview(env)
|
|
558
547
|
when ["POST", "/__dev/api/queue/retry"]
|
|
559
548
|
body = read_json_body(env) || {}
|
|
560
549
|
json_response(queue_retry(body))
|
|
@@ -564,18 +553,18 @@ module Tina4
|
|
|
564
553
|
when ["POST", "/__dev/api/queue/replay"]
|
|
565
554
|
body = read_json_body(env) || {}
|
|
566
555
|
json_response(queue_replay(body))
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
# Data + tooling: table browser, seeding, the run-chips (migrate / test /
|
|
560
|
+
# seed), the SQL console, table list, the gallery and the metrics panels.
|
|
561
|
+
def dispatch_data(method, path, env)
|
|
562
|
+
case [method, path]
|
|
567
563
|
when ["GET", "/__dev/api/table"]
|
|
568
564
|
table_name = query_param(env, "name")
|
|
569
565
|
json_response(table_detail_payload(table_name))
|
|
570
566
|
when ["POST", "/__dev/api/seed"]
|
|
571
|
-
|
|
572
|
-
table_name = (body && body["table"]) || ""
|
|
573
|
-
count = (body && body["count"]) || 10
|
|
574
|
-
seed = body && body["seed"]
|
|
575
|
-
seed = (Integer(seed) rescue nil) unless seed.nil?
|
|
576
|
-
clear = body && (body["clear"] == true || body["clear"].to_s == "true")
|
|
577
|
-
strict = body && (body["strict"] == true || body["strict"].to_s == "true")
|
|
578
|
-
json_response(seed_table_data(table_name, count.to_i, seed: seed, clear: clear, strict: strict))
|
|
567
|
+
handle_seed(env)
|
|
579
568
|
when ["POST", "/__dev/api/tool"]
|
|
580
569
|
body = read_json_body(env)
|
|
581
570
|
tool = (body && body["tool"]) || ""
|
|
@@ -590,6 +579,28 @@ module Tina4
|
|
|
590
579
|
json_response(run_tests_payload)
|
|
591
580
|
when ["POST", "/__dev/api/seed/run"]
|
|
592
581
|
json_response(run_seeds_payload)
|
|
582
|
+
when ["POST", "/__dev/api/query"]
|
|
583
|
+
body = read_json_body(env)
|
|
584
|
+
sql = (body && (body["query"] || body["sql"])) || ""
|
|
585
|
+
json_response(run_query(sql))
|
|
586
|
+
when ["GET", "/__dev/api/tables"]
|
|
587
|
+
json_response(tables_payload)
|
|
588
|
+
when ["GET", "/__dev/api/gallery"]
|
|
589
|
+
json_response(gallery_list)
|
|
590
|
+
when ["POST", "/__dev/api/gallery/deploy"]
|
|
591
|
+
body = read_json_body(env)
|
|
592
|
+
name = (body && body["name"]) || ""
|
|
593
|
+
json_response(gallery_deploy(name))
|
|
594
|
+
when ["GET", "/__dev/api/metrics/full"]
|
|
595
|
+
metrics_full_response
|
|
596
|
+
when ["GET", "/__dev/api/metrics/file"]
|
|
597
|
+
metrics_file_response(env)
|
|
598
|
+
end
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
# Grounding token panel and the database-connection editor.
|
|
602
|
+
def dispatch_config(method, path, env)
|
|
603
|
+
case [method, path]
|
|
593
604
|
# Grounding panel — configure the tina4-coder MCP token used for
|
|
594
605
|
# live-docs grounding. Self-contained (.env read/write); no proxy.
|
|
595
606
|
when ["GET", "/__dev/api/grounding/status"]
|
|
@@ -605,40 +616,13 @@ module Tina4
|
|
|
605
616
|
when ["POST", "/__dev/api/connections/save"]
|
|
606
617
|
body = read_json_body(env)
|
|
607
618
|
handle_connections_save(body)
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
json_response(gallery_list)
|
|
616
|
-
when ["POST", "/__dev/api/gallery/deploy"]
|
|
617
|
-
body = read_json_body(env)
|
|
618
|
-
name = (body && body["name"]) || ""
|
|
619
|
-
json_response(gallery_deploy(name))
|
|
620
|
-
when ["GET", "/__dev/api/version-check"]
|
|
621
|
-
json_response(version_check_payload)
|
|
622
|
-
when ["GET", "/__dev/api/metrics/full"]
|
|
623
|
-
# No fallback (ADR-0002). A missing or stale CLI is a 503 naming the
|
|
624
|
-
# install command, never zeros that read as a healthy codebase.
|
|
625
|
-
begin
|
|
626
|
-
json_response(Tina4::Metrics.full_analysis)
|
|
627
|
-
rescue Tina4::MetricsEngineError => e
|
|
628
|
-
json_response({ "error" => e.message }, 503)
|
|
629
|
-
end
|
|
630
|
-
when ["GET", "/__dev/api/metrics/file"]
|
|
631
|
-
file_path = (query_param(env, "path") || "").to_s
|
|
632
|
-
begin
|
|
633
|
-
json_response(Tina4::Metrics.file_detail(file_path))
|
|
634
|
-
rescue Tina4::MetricsEngineError => e
|
|
635
|
-
# A bad path is the caller's mistake (404); anything else is the
|
|
636
|
-
# engine being unavailable (503).
|
|
637
|
-
bad_path = e.message.include?("no such file") ||
|
|
638
|
-
e.message.include?("not a file") ||
|
|
639
|
-
e.message.include?("needs a path")
|
|
640
|
-
json_response({ "error" => e.message }, bad_path ? 404 : 503)
|
|
641
|
-
end
|
|
619
|
+
end
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
# File browser + editor (read / raw / save / rename / delete) and the
|
|
623
|
+
# dependency search / install surface.
|
|
624
|
+
def dispatch_files(method, path, env)
|
|
625
|
+
case [method, path]
|
|
642
626
|
when ["GET", "/__dev/api/files"]
|
|
643
627
|
json_response(files_list(env))
|
|
644
628
|
when ["GET", "/__dev/api/file"]
|
|
@@ -662,8 +646,13 @@ module Tina4
|
|
|
662
646
|
when ["POST", "/__dev/api/deps/install"]
|
|
663
647
|
body = read_json_body(env) || {}
|
|
664
648
|
json_response(deps_install(body))
|
|
665
|
-
|
|
666
|
-
|
|
649
|
+
end
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
# The MCP surfaces (REST shim + JSON-RPC + SSE), scaffolding, and the
|
|
653
|
+
# live-docs (Live API RAG) + GraphQL-schema endpoints.
|
|
654
|
+
def dispatch_mcp(method, path, env)
|
|
655
|
+
case [method, path]
|
|
667
656
|
# All four MCP surfaces (REST shim + JSON-RPC + SSE) go through
|
|
668
657
|
# with_mcp_gate: capability (Tina4.mcp_enabled?) decides whether MCP
|
|
669
658
|
# runs at all (off → route behaves as unmounted, nil → RackApp 404,
|
|
@@ -705,23 +694,133 @@ module Tina4
|
|
|
705
694
|
when ["GET", "/__dev/api/docs/.well-known.json"]
|
|
706
695
|
json_response(docs_well_known_payload)
|
|
707
696
|
when ["GET", "/__dev/api/graphql/schema"]
|
|
708
|
-
|
|
709
|
-
gql = Tina4::GraphQL.new
|
|
710
|
-
# Auto-discover and register all ORM subclasses
|
|
711
|
-
ObjectSpace.each_object(Class).select { |c| c < Tina4::ORM }.each do |model_class|
|
|
712
|
-
gql.from_orm(model_class.new)
|
|
713
|
-
end
|
|
714
|
-
json_response({ schema: gql.introspect, sdl: gql.schema_sdl })
|
|
715
|
-
rescue => e
|
|
716
|
-
json_response({ error: e.message }, 400)
|
|
717
|
-
end
|
|
718
|
-
else
|
|
719
|
-
nil
|
|
697
|
+
graphql_schema_response
|
|
720
698
|
end
|
|
721
699
|
end
|
|
722
700
|
|
|
701
|
+
# POST /__dev/api/reload — external reload trigger. Bumps the mtime
|
|
702
|
+
# counter, re-discovers routes, keeps the live docs index fresh, and
|
|
703
|
+
# broadcasts a WebSocket reload to connected browsers.
|
|
704
|
+
def handle_reload(env)
|
|
705
|
+
body = read_json_body(env) || {}
|
|
706
|
+
@reload_mtime = Time.now.to_i
|
|
707
|
+
@reload_file = body["file"] || ""
|
|
708
|
+
reload_type = body["type"] || "reload"
|
|
709
|
+
Tina4::Log.info("External reload trigger: #{reload_type}#{@reload_file.empty? ? '' : " (#{@reload_file})"}")
|
|
710
|
+
# Re-discover so files dropped into src/routes/ register without
|
|
711
|
+
# a server restart. Idempotent — already-loaded files are skipped,
|
|
712
|
+
# changed files are re-loaded (mtime-tracked).
|
|
713
|
+
begin
|
|
714
|
+
Tina4::Router.rescan_routes!
|
|
715
|
+
rescue StandardError => e
|
|
716
|
+
Tina4::Log.error("Re-discover on reload failed: #{e.message}")
|
|
717
|
+
end
|
|
718
|
+
# Keep the code Context index LIVE on the same reload trigger: reindex
|
|
719
|
+
# just the changed file (UPSERT) so the dev-MCP code_search reflects
|
|
720
|
+
# the edit immediately. Only touches an already-built index
|
|
721
|
+
# (existing_context never creates one); guarded so a context failure
|
|
722
|
+
# never breaks the reload.
|
|
723
|
+
begin
|
|
724
|
+
unless @reload_file.to_s.empty?
|
|
725
|
+
ctx = Tina4::Context.existing_context
|
|
726
|
+
ctx&.reindex_file(@reload_file)
|
|
727
|
+
end
|
|
728
|
+
rescue StandardError => e
|
|
729
|
+
Tina4::Log.error("Context reindex on reload failed: #{e.message}")
|
|
730
|
+
end
|
|
731
|
+
# WebSocket-primary reload: push an instant message to every browser
|
|
732
|
+
# connected on /__dev_reload. The toolbar client (and the dev-admin
|
|
733
|
+
# dashboard) act on this immediately — the mtime poll above is only a
|
|
734
|
+
# fallback for when the socket is down. CSS changes swap stylesheets;
|
|
735
|
+
# everything else triggers a full page reload. We normalise the wire
|
|
736
|
+
# `type` to "css"/"reload" (the clients only react to css/reload/change)
|
|
737
|
+
# but still echo the caller's original type in the HTTP response.
|
|
738
|
+
# Wrapped so a broadcast failure — or zero connected clients — never
|
|
739
|
+
# 500s the reload endpoint.
|
|
740
|
+
begin
|
|
741
|
+
ws_type = reload_type == "css" ? "css" : "reload"
|
|
742
|
+
Tina4::DevReload.broadcast(
|
|
743
|
+
JSON.generate({ type: ws_type, file: @reload_file, mtime: @reload_mtime })
|
|
744
|
+
)
|
|
745
|
+
rescue StandardError => e
|
|
746
|
+
Tina4::Log.error("Dev-reload WebSocket broadcast failed: #{e.message}")
|
|
747
|
+
end
|
|
748
|
+
json_response({ ok: true, type: reload_type })
|
|
749
|
+
end
|
|
723
750
|
|
|
724
|
-
|
|
751
|
+
# GET /__dev/api/queue — per-topic stats + job list. Queue#size is
|
|
752
|
+
# keyword-only; the rescue swallows a backend error into empty stats.
|
|
753
|
+
def queue_overview(env)
|
|
754
|
+
topic = query_param(env, "topic") || "default"
|
|
755
|
+
stats = { pending: 0, completed: 0, failed: 0, reserved: 0 }
|
|
756
|
+
jobs = []
|
|
757
|
+
begin
|
|
758
|
+
if defined?(Tina4::Queue)
|
|
759
|
+
queue = Tina4::Queue.new(backend: :file, topic: topic)
|
|
760
|
+
# Queue#size is keyword-only (def size(status:)). Calling it
|
|
761
|
+
# positionally raises ArgumentError, which the rescue below
|
|
762
|
+
# swallows — silently zeroing every stat. Use keyword form.
|
|
763
|
+
stats = {
|
|
764
|
+
pending: queue.size(status: "pending"),
|
|
765
|
+
completed: queue.size(status: "completed"),
|
|
766
|
+
failed: queue.size(status: "failed"),
|
|
767
|
+
reserved: queue.size(status: "reserved"),
|
|
768
|
+
}
|
|
769
|
+
jobs = queue_jobs(topic, query_param(env, "status"))
|
|
770
|
+
end
|
|
771
|
+
rescue StandardError
|
|
772
|
+
# fall through to empty stats
|
|
773
|
+
end
|
|
774
|
+
json_response({ jobs: jobs, stats: stats })
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
# POST /__dev/api/seed — seed fake rows into a table via the shared
|
|
778
|
+
# resilient seed_table helper.
|
|
779
|
+
def handle_seed(env)
|
|
780
|
+
body = read_json_body(env)
|
|
781
|
+
table_name = (body && body["table"]) || ""
|
|
782
|
+
count = (body && body["count"]) || 10
|
|
783
|
+
seed = body && body["seed"]
|
|
784
|
+
seed = (Integer(seed) rescue nil) unless seed.nil?
|
|
785
|
+
clear = body && (body["clear"] == true || body["clear"].to_s == "true")
|
|
786
|
+
strict = body && (body["strict"] == true || body["strict"].to_s == "true")
|
|
787
|
+
json_response(seed_table_data(table_name, count.to_i, seed: seed, clear: clear, strict: strict))
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
# GET /__dev/api/metrics/full — native metrics (ADR-0002). No fallback:
|
|
791
|
+
# a missing/stale CLI is a 503 naming the install command, never zeros
|
|
792
|
+
# that read as a healthy codebase.
|
|
793
|
+
def metrics_full_response
|
|
794
|
+
json_response(Tina4::Metrics.full_analysis)
|
|
795
|
+
rescue Tina4::MetricsEngineError => e
|
|
796
|
+
json_response({ "error" => e.message }, 503)
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
# GET /__dev/api/metrics/file — metrics for one file. A bad path is the
|
|
800
|
+
# caller's mistake (404); anything else is the engine being unavailable
|
|
801
|
+
# (503).
|
|
802
|
+
def metrics_file_response(env)
|
|
803
|
+
file_path = (query_param(env, "path") || "").to_s
|
|
804
|
+
json_response(Tina4::Metrics.file_detail(file_path))
|
|
805
|
+
rescue Tina4::MetricsEngineError => e
|
|
806
|
+
bad_path = e.message.include?("no such file") ||
|
|
807
|
+
e.message.include?("not a file") ||
|
|
808
|
+
e.message.include?("needs a path")
|
|
809
|
+
json_response({ "error" => e.message }, bad_path ? 404 : 503)
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
# GET /__dev/api/graphql/schema — introspect every ORM subclass into a
|
|
813
|
+
# GraphQL schema + SDL.
|
|
814
|
+
def graphql_schema_response
|
|
815
|
+
gql = Tina4::GraphQL.new
|
|
816
|
+
# Auto-discover and register all ORM subclasses
|
|
817
|
+
ObjectSpace.each_object(Class).select { |c| c < Tina4::ORM }.each do |model_class|
|
|
818
|
+
gql.from_orm(model_class.new)
|
|
819
|
+
end
|
|
820
|
+
json_response({ schema: gql.introspect, sdl: gql.schema_sdl })
|
|
821
|
+
rescue => e
|
|
822
|
+
json_response({ error: e.message }, 400)
|
|
823
|
+
end
|
|
725
824
|
|
|
726
825
|
def query_param(env, key)
|
|
727
826
|
qs = env["QUERY_STRING"] || ""
|
data/lib/tina4/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tina4ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.13.
|
|
4
|
+
version: 3.13.133
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tina4 Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|