rogiq 0.1.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.
- checksums.yaml +7 -0
- data/exe/rogiq +49 -0
- data/lib/rogiq/cli.rb +51 -0
- data/lib/rogiq/commands/accounts.rb +114 -0
- data/lib/rogiq/commands/ai.rb +71 -0
- data/lib/rogiq/commands/auth.rb +194 -0
- data/lib/rogiq/commands/base.rb +26 -0
- data/lib/rogiq/commands/billing.rb +90 -0
- data/lib/rogiq/commands/billing_stripe.rb +15 -0
- data/lib/rogiq/commands/clients.rb +117 -0
- data/lib/rogiq/commands/content.rb +71 -0
- data/lib/rogiq/commands/diagnose.rb +134 -0
- data/lib/rogiq/commands/jobs.rb +126 -0
- data/lib/rogiq/commands/queue.rb +115 -0
- data/lib/rogiq/commands/security.rb +33 -0
- data/lib/rogiq/commands/seo.rb +49 -0
- data/lib/rogiq/commands/seo_audit.rb +51 -0
- data/lib/rogiq/commands/status.rb +166 -0
- data/lib/rogiq/commands/sync.rb +60 -0
- data/lib/rogiq/config_store.rb +59 -0
- data/lib/rogiq/formatters.rb +103 -0
- data/lib/rogiq/helpers.rb +53 -0
- data/lib/rogiq/http_api.rb +81 -0
- data/lib/rogiq/rails_loader.rb +22 -0
- data/lib/rogiq/remote/accounts.rb +38 -0
- data/lib/rogiq/remote/ai.rb +50 -0
- data/lib/rogiq/remote/billing.rb +39 -0
- data/lib/rogiq/remote/clients.rb +44 -0
- data/lib/rogiq/remote/command_base.rb +41 -0
- data/lib/rogiq/remote/content.rb +37 -0
- data/lib/rogiq/remote/diagnose.rb +24 -0
- data/lib/rogiq/remote/jobs.rb +52 -0
- data/lib/rogiq/remote/queue.rb +72 -0
- data/lib/rogiq/remote/security.rb +26 -0
- data/lib/rogiq/remote/seo.rb +32 -0
- data/lib/rogiq/remote/status.rb +35 -0
- data/lib/rogiq/remote/sync.rb +49 -0
- data/lib/rogiq/remote_cli.rb +73 -0
- data/lib/rogiq/version.rb +5 -0
- metadata +96 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RogIQ
|
|
4
|
+
module Commands
|
|
5
|
+
class Clients < RogIQ::Commands::Base
|
|
6
|
+
desc "list", "List recent clients"
|
|
7
|
+
method_option :limit, type: :numeric, default: 25, aliases: "-n", desc: "Max records"
|
|
8
|
+
method_option :search, type: :string, desc: "Filter by name (ILIKE)"
|
|
9
|
+
def list
|
|
10
|
+
RogIQ.load_rails!
|
|
11
|
+
scope = ::Client.order(created_at: :desc)
|
|
12
|
+
scope = scope.where("name ILIKE ?", "%#{options[:search]}%") if options[:search].present?
|
|
13
|
+
rows = scope.limit(options[:limit]).map do |c|
|
|
14
|
+
slug = c.account&.slug
|
|
15
|
+
[c.id, c.name, slug, c.research_ready, c.created_at.iso8601]
|
|
16
|
+
end
|
|
17
|
+
fmt.output(headers: %w[id name account_slug research_ready created_at], rows: rows)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
desc "show IDENTIFIER", "Show client by uuid or name"
|
|
21
|
+
def show(identifier)
|
|
22
|
+
RogIQ.load_rails!
|
|
23
|
+
c = RogIQ::Helpers.resolve_client(identifier)
|
|
24
|
+
unless c
|
|
25
|
+
fmt.error_msg("Client not found: #{identifier}")
|
|
26
|
+
exit 1
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
payload = {
|
|
30
|
+
id: c.id,
|
|
31
|
+
name: c.name,
|
|
32
|
+
account_id: c.account_id,
|
|
33
|
+
account_slug: c.account&.slug,
|
|
34
|
+
website: c.website,
|
|
35
|
+
industry: c.industry,
|
|
36
|
+
research_ready: c.research_ready,
|
|
37
|
+
onboarding_stage: c.try(:onboarding_stage),
|
|
38
|
+
onboarding_version: c.try(:onboarding_version),
|
|
39
|
+
created_at: c.created_at,
|
|
40
|
+
updated_at: c.updated_at
|
|
41
|
+
}
|
|
42
|
+
fmt.output(payload)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
desc "research_status IDENTIFIER", "Print client_settings research_status JSON"
|
|
46
|
+
map "research-status" => :research_status
|
|
47
|
+
def research_status(identifier)
|
|
48
|
+
RogIQ.load_rails!
|
|
49
|
+
c = RogIQ::Helpers.resolve_client(identifier)
|
|
50
|
+
unless c
|
|
51
|
+
fmt.error_msg("Client not found")
|
|
52
|
+
exit 1
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
row = c.client_settings.find_by(setting_key: "research_status")
|
|
56
|
+
fmt.output(row ? row.setting_value : {})
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
desc "settings IDENTIFIER", "List all client_settings rows"
|
|
60
|
+
def settings(identifier)
|
|
61
|
+
RogIQ.load_rails!
|
|
62
|
+
c = RogIQ::Helpers.resolve_client(identifier)
|
|
63
|
+
unless c
|
|
64
|
+
fmt.error_msg("Client not found")
|
|
65
|
+
exit 1
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
rows = c.client_settings.order(:setting_key).map do |s|
|
|
69
|
+
val = s.setting_value
|
|
70
|
+
preview = val.is_a?(Hash) ? val.to_json.truncate(120) : val.to_s.truncate(120)
|
|
71
|
+
[s.setting_key, preview]
|
|
72
|
+
end
|
|
73
|
+
fmt.output(headers: %w[setting_key value_preview], rows: rows)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
desc "onboarding IDENTIFIER", "Latest onboarding evaluation / version summary"
|
|
77
|
+
def onboarding(identifier)
|
|
78
|
+
RogIQ.load_rails!
|
|
79
|
+
c = RogIQ::Helpers.resolve_client(identifier)
|
|
80
|
+
unless c
|
|
81
|
+
fmt.error_msg("Client not found")
|
|
82
|
+
exit 1
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
ev = c.onboarding_evaluations.order(created_at: :desc).first
|
|
86
|
+
ver = c.onboarding_versions.order(created_at: :desc).first
|
|
87
|
+
fmt.output(
|
|
88
|
+
{
|
|
89
|
+
client_id: c.id,
|
|
90
|
+
latest_evaluation: ev&.attributes,
|
|
91
|
+
latest_version: ver&.attributes
|
|
92
|
+
}.compact
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
desc "subdomain IDENTIFIER", "Account slug (white-label) + custom domains if any"
|
|
97
|
+
def subdomain(identifier)
|
|
98
|
+
RogIQ.load_rails!
|
|
99
|
+
c = RogIQ::Helpers.resolve_client(identifier)
|
|
100
|
+
unless c
|
|
101
|
+
fmt.error_msg("Client not found")
|
|
102
|
+
exit 1
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
acc = c.account
|
|
106
|
+
domains = acc ? ::CustomDomain.where(account_id: acc.id).limit(50).pluck(:domain, :status, :verified_at) : []
|
|
107
|
+
fmt.output(
|
|
108
|
+
{
|
|
109
|
+
account_slug: acc&.slug,
|
|
110
|
+
account_id: acc&.id,
|
|
111
|
+
custom_domains: domains.map { |d, st, v| { domain: d, status: st, verified_at: v } }
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RogIQ
|
|
4
|
+
module Commands
|
|
5
|
+
class Content < RogIQ::Commands::Base
|
|
6
|
+
desc "status CLIENT", "ContentItem counts by state"
|
|
7
|
+
def status(client_identifier)
|
|
8
|
+
RogIQ.load_rails!
|
|
9
|
+
client = RogIQ::Helpers.resolve_client(client_identifier)
|
|
10
|
+
unless client
|
|
11
|
+
fmt.error_msg("Client not found")
|
|
12
|
+
exit 1
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
counts = ::ContentItem.where(client_id: client.id).group(:state).count.sort_by { |_, n| -n }
|
|
16
|
+
fmt.output(headers: %w[state count], rows: counts.to_a)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
desc "heal CLIENT", "Enqueue DataHealingOrchestratorJob"
|
|
20
|
+
def heal(client_identifier)
|
|
21
|
+
RogIQ.load_rails!
|
|
22
|
+
client = RogIQ::Helpers.resolve_client(client_identifier)
|
|
23
|
+
unless client
|
|
24
|
+
fmt.error_msg("Client not found")
|
|
25
|
+
exit 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
::DataHealingOrchestratorJob.perform_later(client_id: client.id, account_id: client.account_id)
|
|
29
|
+
fmt.success("DataHealingOrchestratorJob enqueued for client #{client.id}.")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
desc "publish_queue", "Scheduled content publications (future)"
|
|
33
|
+
map "publish-queue" => :publish_queue
|
|
34
|
+
method_option :limit, type: :numeric, default: 40
|
|
35
|
+
def publish_queue
|
|
36
|
+
RogIQ.load_rails!
|
|
37
|
+
rows = ::ContentPublication.where(status: "scheduled")
|
|
38
|
+
.where("scheduled_at > ?", Time.current)
|
|
39
|
+
.order(:scheduled_at)
|
|
40
|
+
.limit(options[:limit])
|
|
41
|
+
.map do |p|
|
|
42
|
+
[
|
|
43
|
+
p.id,
|
|
44
|
+
p.account_id,
|
|
45
|
+
p.title.to_s.truncate(50),
|
|
46
|
+
p.scheduled_at&.iso8601,
|
|
47
|
+
p.platform
|
|
48
|
+
]
|
|
49
|
+
end
|
|
50
|
+
fmt.output(headers: %w[id account_id title scheduled_at platform], rows: rows)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
desc "failed CLIENT", "Content items in failed or rejected state"
|
|
54
|
+
method_option :limit, type: :numeric, default: 30
|
|
55
|
+
def failed(client_identifier)
|
|
56
|
+
RogIQ.load_rails!
|
|
57
|
+
client = RogIQ::Helpers.resolve_client(client_identifier)
|
|
58
|
+
unless client
|
|
59
|
+
fmt.error_msg("Client not found")
|
|
60
|
+
exit 1
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
rows = ::ContentItem.where(client_id: client.id, state: %w[failed rejected])
|
|
64
|
+
.order(updated_at: :desc)
|
|
65
|
+
.limit(options[:limit])
|
|
66
|
+
.map { |i| [i.id, i.title.to_s.truncate(60), i.state, i.updated_at.iso8601] }
|
|
67
|
+
fmt.output(headers: %w[id title state updated_at], rows: rows)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RogIQ
|
|
4
|
+
module Commands
|
|
5
|
+
class Diagnose < RogIQ::Commands::Base
|
|
6
|
+
desc "search_insights [CLIENT]", "Run diagnose_search_insights.rb (global DB report; client optional)"
|
|
7
|
+
map "search-insights" => :search_insights
|
|
8
|
+
def search_insights(*)
|
|
9
|
+
RogIQ.load_rails!
|
|
10
|
+
load File.join(RogIQ.repo_root, "diagnose_search_insights.rb")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
desc "genius CLIENT", "Run debug_genius_mode_goals.rb for a client id/name"
|
|
14
|
+
def genius(identifier)
|
|
15
|
+
RogIQ.load_rails!
|
|
16
|
+
client = RogIQ::Helpers.resolve_client(identifier)
|
|
17
|
+
unless client
|
|
18
|
+
fmt.error_msg("Client not found")
|
|
19
|
+
exit 1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
RogIQ::Helpers.with_saved_argv([ client.id.to_s ]) do
|
|
23
|
+
load File.join(RogIQ.repo_root, "debug_genius_mode_goals.rb")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
desc "subdomain CLIENT", "DNS/API checks using account slug (diagnose_subdomain_issues.rb)"
|
|
28
|
+
def subdomain(identifier)
|
|
29
|
+
RogIQ.load_rails!
|
|
30
|
+
client = RogIQ::Helpers.resolve_client(identifier)
|
|
31
|
+
unless client&.account&.slug
|
|
32
|
+
fmt.error_msg("Client or account slug not found")
|
|
33
|
+
exit 1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
RogIQ::Helpers.with_saved_argv([ client.account.slug ]) do
|
|
37
|
+
load File.join(RogIQ.repo_root, "diagnose_subdomain_issues.rb")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
desc "emergency CLIENT", "Emergency client diagnostic (jobs, research status, settings)"
|
|
42
|
+
def emergency(identifier)
|
|
43
|
+
RogIQ.load_rails!
|
|
44
|
+
client = RogIQ::Helpers.resolve_client(identifier)
|
|
45
|
+
unless client
|
|
46
|
+
fmt.error_msg("Client not found")
|
|
47
|
+
exit 1
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
fmt.say("\n" + "=" * 80)
|
|
51
|
+
fmt.say("EMERGENCY DIAGNOSTIC: #{client.name} (#{client.id})")
|
|
52
|
+
fmt.say("=" * 80)
|
|
53
|
+
|
|
54
|
+
fmt.say("\n1. CLIENT DATA")
|
|
55
|
+
fmt.say(" Name: #{client.name}")
|
|
56
|
+
fmt.say(" Website: #{client.website.presence || "EMPTY"}")
|
|
57
|
+
fmt.say(" Research Ready: #{client.research_ready}")
|
|
58
|
+
fmt.say(" Description: #{client.description.present? ? client.description[0, 100] + "…" : "EMPTY"}")
|
|
59
|
+
fmt.say(" Primary Color: #{client.primary_color || "EMPTY"}")
|
|
60
|
+
fmt.say(" Created: #{client.created_at} (#{((Time.current - client.created_at) / 60).round} min ago)")
|
|
61
|
+
|
|
62
|
+
fmt.say("\n2. RESEARCH STATUS")
|
|
63
|
+
research = client.client_settings.find_by(setting_key: "research_status")
|
|
64
|
+
if research
|
|
65
|
+
v = research.setting_value
|
|
66
|
+
fmt.say(" Status: #{v["status"]}")
|
|
67
|
+
fmt.say(" Website: #{v["website"] || "none"}")
|
|
68
|
+
fmt.say(" Updated: #{v["updated_at"]}")
|
|
69
|
+
else
|
|
70
|
+
fmt.say(" NO RESEARCH STATUS — after_create callback may not have run")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
fmt.say("\n3. SOLID QUEUE JOBS FOR THIS CLIENT")
|
|
74
|
+
jobs = ::SolidQueue::Job.where("arguments::text LIKE ?", "%#{client.id}%").order(created_at: :desc)
|
|
75
|
+
if jobs.empty?
|
|
76
|
+
fmt.say(" NO JOBS FOUND — callback not firing, SolidQueue not configured, or DB issue")
|
|
77
|
+
else
|
|
78
|
+
fmt.say(" Found #{jobs.count} job record(s):")
|
|
79
|
+
jobs.group_by(&:class_name).each do |name, list|
|
|
80
|
+
fmt.say(" #{name} (#{list.size} run(s)):")
|
|
81
|
+
list.first(3).each do |job|
|
|
82
|
+
status = job.finished_at ? "FINISHED" : (job.failed_at ? "FAILED" : "PENDING")
|
|
83
|
+
fmt.say(" #{status} — created #{job.created_at}, finished #{job.finished_at || "—"}")
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
fmt.say("\n4. KEY SETTINGS PRESENCE")
|
|
89
|
+
important = %w[website_content ai_insights onboarding_processed_data]
|
|
90
|
+
present = client.client_settings.pluck(:setting_key)
|
|
91
|
+
important.each do |key|
|
|
92
|
+
fmt.say(" #{key}: #{present.include?(key) ? "present" : "MISSING"}")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
fmt.say("\n" + "=" * 80)
|
|
96
|
+
fmt.say("END DIAGNOSTIC")
|
|
97
|
+
fmt.say("=" * 80)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
desc "zeitwerk", "Run zeitwerk:check with RAILS_ENV=production"
|
|
101
|
+
def zeitwerk
|
|
102
|
+
api = RogIQ.api_root
|
|
103
|
+
ok = system({ "RAILS_ENV" => "production" }, "bundle", "exec", "rails", "zeitwerk:check", chdir: api)
|
|
104
|
+
exit(ok ? 0 : 1)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
desc "pinecone", "Pinecone configuration summary"
|
|
108
|
+
def pinecone
|
|
109
|
+
RogIQ.load_rails!
|
|
110
|
+
if defined?(Rails.configuration.ai_engine)
|
|
111
|
+
cfg = Rails.configuration.ai_engine
|
|
112
|
+
fmt.output(
|
|
113
|
+
pinecone_enabled: cfg.try(:pinecone_enabled),
|
|
114
|
+
pinecone_index_name: cfg.try(:pinecone_index_name),
|
|
115
|
+
api_key_present: ENV["PINECONE_API_KEY"].present?
|
|
116
|
+
)
|
|
117
|
+
else
|
|
118
|
+
fmt.output(pinecone: "Rails.configuration.ai_engine not defined")
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
desc "migrations", "Pending migrations?"
|
|
123
|
+
def migrations
|
|
124
|
+
RogIQ.load_rails!
|
|
125
|
+
pending = ActiveRecord::Base.connection.pool.migration_context.pending_migration_versions
|
|
126
|
+
fmt.output(
|
|
127
|
+
pending_count: pending.size,
|
|
128
|
+
pending_versions: pending
|
|
129
|
+
)
|
|
130
|
+
exit(pending.any? ? 1 : 0)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RogIQ
|
|
4
|
+
module Commands
|
|
5
|
+
class Jobs < RogIQ::Commands::Base
|
|
6
|
+
desc "list", "Summarize Solid Queue job states"
|
|
7
|
+
method_option :failed, type: :boolean, default: false, desc: "Only failed executions (recent)"
|
|
8
|
+
method_option :class, type: :string, desc: "Filter by Active Job class name"
|
|
9
|
+
method_option :limit, type: :numeric, default: 50, desc: "Max rows for --failed list"
|
|
10
|
+
def list
|
|
11
|
+
RogIQ.load_rails!
|
|
12
|
+
unless ActiveRecord::Base.connection.data_source_exists?("solid_queue_jobs")
|
|
13
|
+
fmt.error_msg("solid_queue tables not present")
|
|
14
|
+
exit 1
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
if options[:failed]
|
|
18
|
+
scope = SolidQueue::FailedExecution.joins(:job).order("solid_queue_failed_executions.id DESC")
|
|
19
|
+
scope = scope.where(solid_queue_jobs: { class_name: options[:class] }) if options[:class].present?
|
|
20
|
+
rows = scope.limit(options[:limit]).map do |fe|
|
|
21
|
+
job = fe.job
|
|
22
|
+
msg = fe.error.is_a?(Hash) ? fe.error["message"] : fe.error.to_s
|
|
23
|
+
[fe.id, job&.class_name, msg.to_s.truncate(80)]
|
|
24
|
+
end
|
|
25
|
+
fmt.output(headers: %w[failed_execution_id class_name error_preview], rows: rows)
|
|
26
|
+
else
|
|
27
|
+
unfinished = SolidQueue::Job.where(finished_at: nil).group(:class_name).count.sort_by { |_, n| -n }
|
|
28
|
+
fmt.output(
|
|
29
|
+
headers: %w[class_name unfinished_count],
|
|
30
|
+
rows: unfinished
|
|
31
|
+
)
|
|
32
|
+
fmt.say("") unless options[:quiet]
|
|
33
|
+
fmt.output(
|
|
34
|
+
{
|
|
35
|
+
ready: SolidQueue::ReadyExecution.count,
|
|
36
|
+
scheduled: SolidQueue::ScheduledExecution.count,
|
|
37
|
+
failed: SolidQueue::FailedExecution.count,
|
|
38
|
+
claimed: SolidQueue::ClaimedExecution.count
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
desc "detail ID", "Show full error / metadata for a failed execution or job id"
|
|
45
|
+
map "inspect" => :detail
|
|
46
|
+
def detail(id)
|
|
47
|
+
RogIQ.load_rails!
|
|
48
|
+
fe = find_failed_execution(id)
|
|
49
|
+
unless fe
|
|
50
|
+
fmt.error_msg("Failed execution not found for id=#{id}")
|
|
51
|
+
exit 1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
job = fe.job
|
|
55
|
+
payload = {
|
|
56
|
+
failed_execution_id: fe.id,
|
|
57
|
+
job_id: job&.id,
|
|
58
|
+
class_name: job&.class_name,
|
|
59
|
+
queue_name: job&.queue_name,
|
|
60
|
+
created_at: job&.created_at,
|
|
61
|
+
error: fe.error,
|
|
62
|
+
arguments: job&.arguments
|
|
63
|
+
}
|
|
64
|
+
fmt.output(payload)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
desc "retry ID", "Retry a single failed execution (by failed_execution.id or job.id)"
|
|
68
|
+
map "retry" => :retry_one
|
|
69
|
+
def retry_one(id)
|
|
70
|
+
RogIQ.load_rails!
|
|
71
|
+
fe = find_failed_execution(id)
|
|
72
|
+
unless fe
|
|
73
|
+
fmt.error_msg("Failed execution not found for id=#{id}")
|
|
74
|
+
exit 1
|
|
75
|
+
end
|
|
76
|
+
fe.retry
|
|
77
|
+
fmt.success("Retried job #{fe.job_id}")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
desc "discard ID", "Discard a failed execution without retry"
|
|
81
|
+
map "discard" => :discard_one
|
|
82
|
+
method_option :yes, type: :boolean, default: false, aliases: "-y", desc: "Skip confirmation"
|
|
83
|
+
def discard_one(id)
|
|
84
|
+
RogIQ.load_rails!
|
|
85
|
+
fe = find_failed_execution(id)
|
|
86
|
+
unless fe
|
|
87
|
+
fmt.error_msg("Failed execution not found for id=#{id}")
|
|
88
|
+
exit 1
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
unless fmt.confirm!("Discard failed execution #{fe.id} (job #{fe.job_id})?", yes: options[:yes])
|
|
92
|
+
fmt.warn_msg("Aborted.")
|
|
93
|
+
return
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
fe.discard
|
|
97
|
+
fmt.success("Discarded.")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
desc "tail", "Poll failed executions every 3 seconds (Ctrl+C to stop)"
|
|
101
|
+
method_option :interval, type: :numeric, default: 3, desc: "Seconds between polls"
|
|
102
|
+
def tail
|
|
103
|
+
RogIQ.load_rails!
|
|
104
|
+
last_id = SolidQueue::FailedExecution.maximum(:id).to_i
|
|
105
|
+
fmt.success("Watching SolidQueue::FailedExecution after id=#{last_id}…")
|
|
106
|
+
loop do
|
|
107
|
+
sleep options[:interval]
|
|
108
|
+
rows = SolidQueue::FailedExecution.where("id > ?", last_id).order(:id)
|
|
109
|
+
rows.each do |fe|
|
|
110
|
+
last_id = fe.id
|
|
111
|
+
job = fe.job
|
|
112
|
+
msg = fe.error.is_a?(Hash) ? fe.error["message"] : fe.error
|
|
113
|
+
fmt.say("[#{fe.id}] #{job&.class_name}: #{msg}")
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def find_failed_execution(id)
|
|
121
|
+
SolidQueue::FailedExecution.find_by(id: id) ||
|
|
122
|
+
SolidQueue::FailedExecution.find_by(job_id: id)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RogIQ
|
|
4
|
+
module Commands
|
|
5
|
+
class Queue < RogIQ::Commands::Base
|
|
6
|
+
desc "stats", "Solid Queue counts (ready, scheduled, failed, claimed)"
|
|
7
|
+
method_option :by_class, type: :boolean, default: false, desc: "Break down unfinished jobs by Active Job class (--by-class)"
|
|
8
|
+
def stats
|
|
9
|
+
RogIQ.load_rails!
|
|
10
|
+
unless solid_queue_tables?
|
|
11
|
+
fmt.error_msg("solid_queue tables not present")
|
|
12
|
+
exit 1
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
if options[:by_class]
|
|
16
|
+
rows = SolidQueue::Job.where(finished_at: nil).group(:class_name).count.sort_by { |_, n| -n }
|
|
17
|
+
fmt.output(headers: %w[class_name unfinished_jobs], rows: rows.to_a)
|
|
18
|
+
else
|
|
19
|
+
payload = {
|
|
20
|
+
ready: SolidQueue::ReadyExecution.count,
|
|
21
|
+
scheduled: SolidQueue::ScheduledExecution.count,
|
|
22
|
+
failed: SolidQueue::FailedExecution.count,
|
|
23
|
+
claimed: SolidQueue::ClaimedExecution.count,
|
|
24
|
+
blocked: defined?(SolidQueue::BlockedExecution) ? SolidQueue::BlockedExecution.count : nil
|
|
25
|
+
}.compact
|
|
26
|
+
fmt.output(payload)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
desc "flush", "Discard failed executions (--class optional)"
|
|
31
|
+
method_option :class, type: :string, required: false, desc: "Limit to this Active Job class name"
|
|
32
|
+
method_option :yes, type: :boolean, default: false, aliases: "-y", desc: "Skip confirmation"
|
|
33
|
+
def flush
|
|
34
|
+
RogIQ.load_rails!
|
|
35
|
+
scope = SolidQueue::FailedExecution.joins(:job)
|
|
36
|
+
scope = scope.where(solid_queue_jobs: { class_name: options[:class] }) if options[:class].present?
|
|
37
|
+
|
|
38
|
+
n = scope.count
|
|
39
|
+
if n.zero?
|
|
40
|
+
fmt.success("No failed executions to discard.")
|
|
41
|
+
return
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
unless fmt.confirm!("Discard #{n} failed execution(s)?", yes: options[:yes])
|
|
45
|
+
fmt.warn_msg("Aborted.")
|
|
46
|
+
return
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
discarded = 0
|
|
50
|
+
scope.find_each do |fe|
|
|
51
|
+
fe.discard
|
|
52
|
+
discarded += 1
|
|
53
|
+
end
|
|
54
|
+
fmt.success("Discarded #{discarded} failed execution(s).")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
desc "retry_all", "Retry all failed jobs (optionally scoped with --class)"
|
|
58
|
+
map "retry-all" => :retry_all
|
|
59
|
+
method_option :class, type: :string, required: false, desc: "Only this Active Job class"
|
|
60
|
+
method_option :yes, type: :boolean, default: false, aliases: "-y", desc: "Skip confirmation"
|
|
61
|
+
def retry_all
|
|
62
|
+
RogIQ.load_rails!
|
|
63
|
+
scope = SolidQueue::FailedExecution.joins(:job)
|
|
64
|
+
scope = scope.where(solid_queue_jobs: { class_name: options[:class] }) if options[:class].present?
|
|
65
|
+
jobs = scope.includes(:job).map(&:job).uniq
|
|
66
|
+
if jobs.empty?
|
|
67
|
+
fmt.success("Nothing to retry.")
|
|
68
|
+
return
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
unless fmt.confirm!("Retry #{jobs.size} failed job(s)?", yes: options[:yes])
|
|
72
|
+
fmt.warn_msg("Aborted.")
|
|
73
|
+
return
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
SolidQueue::FailedExecution.retry_all(jobs)
|
|
77
|
+
fmt.success("Retried #{jobs.size} job(s).")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
desc "pause", "Pause a Solid Queue queue by name"
|
|
81
|
+
def pause(queue_name)
|
|
82
|
+
RogIQ.load_rails!
|
|
83
|
+
SolidQueue::Queue.find_by_name(queue_name).pause
|
|
84
|
+
fmt.success("Paused queue #{queue_name}")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
desc "resume", "Resume a paused Solid Queue queue"
|
|
88
|
+
def resume(queue_name)
|
|
89
|
+
RogIQ.load_rails!
|
|
90
|
+
SolidQueue::Queue.find_by_name(queue_name).resume
|
|
91
|
+
fmt.success("Resumed queue #{queue_name}")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
desc "drain", "Clear ready/scheduled/blocked (dangerous; wraps queue:drain rake task semantics)"
|
|
95
|
+
method_option :yes, type: :boolean, default: false, aliases: "-y", desc: "Skip confirmation"
|
|
96
|
+
def drain
|
|
97
|
+
RogIQ.load_rails!
|
|
98
|
+
unless fmt.confirm!("Clear ready, scheduled, and blocked executions?", yes: options[:yes])
|
|
99
|
+
fmt.warn_msg("Aborted.")
|
|
100
|
+
return
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
Rails.application.load_tasks
|
|
104
|
+
Rake::Task["queue:drain"].invoke
|
|
105
|
+
fmt.success("Drain complete.")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def solid_queue_tables?
|
|
111
|
+
ActiveRecord::Base.connection.data_source_exists?("solid_queue_jobs")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RogIQ
|
|
4
|
+
module Commands
|
|
5
|
+
class Security < RogIQ::Commands::Base
|
|
6
|
+
desc "scan", "Run Brakeman (requires dev/test Gemfile group)"
|
|
7
|
+
def scan
|
|
8
|
+
api = RogIQ.api_root
|
|
9
|
+
brakeman = File.join(api, "bin", "brakeman")
|
|
10
|
+
cmd = File.exist?(brakeman) ? [ brakeman.to_s, "-q" ] : %w[bundle exec brakeman -q]
|
|
11
|
+
ok = system(*cmd, chdir: api)
|
|
12
|
+
unless ok
|
|
13
|
+
fmt.error_msg("Brakeman failed or gem not available in this bundle group.")
|
|
14
|
+
exit 1
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
desc "audit", "Run bundler-audit"
|
|
19
|
+
def audit
|
|
20
|
+
api = RogIQ.api_root
|
|
21
|
+
ok = system("bundle", "exec", "bundle-audit", "check", chdir: api)
|
|
22
|
+
exit(ok ? 0 : 1)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
desc "policy", "rake security_policy:diagnose"
|
|
26
|
+
def policy
|
|
27
|
+
RogIQ.load_rails!
|
|
28
|
+
Rails.application.load_tasks
|
|
29
|
+
Rake::Task["security_policy:diagnose"].invoke
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RogIQ
|
|
4
|
+
module Commands
|
|
5
|
+
class SEO < RogIQ::Commands::Base
|
|
6
|
+
desc "audit SUBCOMMAND ...", "SEO audit (run, status)"
|
|
7
|
+
subcommand "audit", RogIQ::Commands::SEOAudit
|
|
8
|
+
|
|
9
|
+
desc "keywords CLIENT", "Track keywords for client"
|
|
10
|
+
def keywords(client_identifier)
|
|
11
|
+
RogIQ.load_rails!
|
|
12
|
+
client = RogIQ::Helpers.resolve_client(client_identifier)
|
|
13
|
+
unless client
|
|
14
|
+
fmt.error_msg("Client not found")
|
|
15
|
+
exit 1
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
rows = client.client_seo_track_keywords.order(:sort_order, :keyword).limit(200).map do |k|
|
|
19
|
+
[k.keyword, k.sort_order, k.source]
|
|
20
|
+
end
|
|
21
|
+
fmt.output(headers: %w[keyword sort_order source], rows: rows)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
desc "insights CLIENT", "Search insights counts"
|
|
25
|
+
def insights(client_identifier)
|
|
26
|
+
RogIQ.load_rails!
|
|
27
|
+
client = RogIQ::Helpers.resolve_client(client_identifier)
|
|
28
|
+
unless client
|
|
29
|
+
fmt.error_msg("Client not found")
|
|
30
|
+
exit 1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
scope = ::SearchInsight.where(client_id: client.id)
|
|
34
|
+
fmt.output(
|
|
35
|
+
total: scope.count,
|
|
36
|
+
by_status: scope.group(:status).count
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
desc "zeitwerk", "Run zeitwerk:check in production mode"
|
|
41
|
+
def zeitwerk
|
|
42
|
+
fmt.warn_msg("Running zeitwerk:check with RAILS_ENV=production …")
|
|
43
|
+
api = RogIQ.api_root
|
|
44
|
+
ok = system({ "RAILS_ENV" => "production" }, "bundle", "exec", "rails", "zeitwerk:check", chdir: api)
|
|
45
|
+
exit(ok ? 0 : 1)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|