Files
redux-scraper/app/helpers/good_job_helper.rb
2025-08-14 17:03:50 +00:00

222 lines
5.4 KiB
Ruby

# typed: strict
# frozen_string_literal: true
module GoodJobHelper
extend T::Sig
extend T::Helpers
abstract!
class AnsiSegment < T::Struct
include T::Struct::ActsAsComparable
prop :text, String
prop :class_names, T::Array[String], default: []
prop :url, T.nilable(String), default: nil
end
# ANSI escape code pattern
ANSI_PATTERN = /\e\[([0-9;]*)m/
UUID_REGEX = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/
sig { params(text: String).returns(T::Array[AnsiSegment]) }
def parse_ansi(text)
segments = T.let([], T::Array[AnsiSegment])
current_classes = T::Array[String].new
# Split the text into parts based on ANSI codes
parts = text.split(ANSI_PATTERN)
# Process each part and its corresponding ANSI codes
parts.each_with_index do |part, index|
if index.even?
# This is text content
segments << AnsiSegment.new(
text: part,
class_names: current_classes.dup,
)
else
# This is an ANSI code
codes = part.split(";").map(&:to_i)
if codes == [0]
current_classes.clear
else
codes.each do |code|
class_name = ansi_code_to_class(code)
current_classes << class_name if class_name
end
end
end
end
segments.each_with_index do |s0, idx|
s1 = segments[idx + 1] || next
if s0.text == "[hle " && s1.text.match(/\d+/)
segments[idx + 1] = AnsiSegment.new(
text: s1.text,
class_names: s1.class_names,
url: Rails.application.routes.url_helpers.log_entry_path(s1.text),
)
end
end
# go through segments and detect UUIDs, splitting the segment at the uuid
# and adding them to the segments array. Should result in a <before>, <uuid>,
# <after> tuple.
segments.flat_map do |segment|
if (idx = segment.text.index(UUID_REGEX))
[
AnsiSegment.new(
text: segment.text[0...idx] || "",
class_names: segment.class_names,
),
AnsiSegment.new(
text: segment.text[idx...idx + 36] || "",
class_names: ["log-uuid"],
url: "/jobs/jobs/#{segment.text[idx...idx + 36]}",
),
AnsiSegment.new(
text: segment.text[idx + 36..] || "",
class_names: segment.class_names,
),
]
else
[segment]
end
end
end
class JobArg < T::Struct
const :key, Symbol
const :value, T.untyped
const :inferred, T::Boolean
end
sig { params(job: GoodJob::Job).returns(T::Array[JobArg]) }
def arguments_for_job(job)
begin
deserialized =
T.cast(
ActiveJob::Arguments.deserialize(job.serialized_params).to_h,
T::Hash[String, T.untyped],
)
rescue ActiveJob::DeserializationError => e
Rails.logger.error(
"error deserializing job arguments: #{e.class.name} - #{e.message}",
)
return [JobArg.new(key: :error, value: e.message, inferred: true)]
end
args_hash =
T.cast(deserialized["arguments"].first, T::Hash[Symbol, T.untyped])
args =
args_hash.map { |key, value| JobArg.new(key:, value:, inferred: false) }
if args_hash[:fa_id] && !args_hash[:post] &&
args << JobArg.new(
key: :post,
value:
Domain::Post::FaPost.find_by(fa_id: args_hash[:fa_id]) ||
"post not found",
inferred: true,
)
end
if args_hash[:url_name] && !args_hash[:user]
args << JobArg.new(
key: :user,
value:
Domain::User::FaUser.find_by(url_name: args_hash[:url_name]) ||
"user not found",
inferred: true,
)
end
if job_id = args_hash[:caused_by_job_id]
job = GoodJob::Job.find_by(id: job_id)
if job
args << JobArg.new(key: :caused_by_job, value: job, inferred: false)
args.delete_if { |arg| arg.key == :caused_by_job_id }
end
end
args.sort_by(&:key)
end
sig { params(state: String).returns(String) }
def post_file_state_badge_class(state)
case state
when "ok"
"bg-success"
when "terminal_error"
"bg-danger"
when "retryable_error"
"bg-warning text-dark"
when "pending"
"bg-info"
else
"bg-secondary"
end
end
sig { params(state: String).returns(String) }
def post_file_state_icon_class(state)
base =
case state
when "ok"
"fa-check"
when "terminal_error"
"fa-circle-xmark"
when "retryable_error"
"fa-triangle-exclamation"
when "pending"
"fa-clock"
else
"fa-question"
end
"fa-solid #{base}"
end
private
sig { params(code: Integer).returns(T.nilable(String)) }
def ansi_code_to_class(code)
case code
when 1
"ansi-bold"
when 30
"ansi-black"
when 31
"ansi-red"
when 32
"ansi-green"
when 33
"ansi-yellow"
when 34
"ansi-blue"
when 35
"ansi-magenta"
when 36
"ansi-cyan"
when 37
"ansi-white"
when 90
"ansi-bright-black"
when 91
"ansi-bright-red"
when 92
"ansi-bright-green"
when 93
"ansi-bright-yellow"
when 94
"ansi-bright-blue"
when 95
"ansi-bright-magenta"
when 96
"ansi-bright-cyan"
when 97
"ansi-bright-white"
else
nil
end
end
end