Module: SolidLoop::ApplicationHelper

Defined in:
app/helpers/solid_loop/application_helper.rb

Constant Summary collapse

INFRA_ERROR_PATTERNS =

Module-level so read models / KPIs can reuse the same rules as the view.

[
  /\AHTTP \d{3}\b/,               # provider returned a non-2xx envelope
  /\bFaraday::/,                  # transport / connection error class
  /\bJSON::ParserError\b/,        # unparseable provider response body
  /invalid_json_after_redaction/, # OpenRouter PII-redaction mangled the body
  /PII detected/i,
  /\b(?:timed out|timeout|connection (?:refused|reset))\b/i
].freeze
CODE_ERROR_PREFIX =

Agent-side code bugs — even when the text happens to mention "timeout" or "connection" — must NOT be mislabelled infra (e.g. a NoMethodError whose message references a timeout method). Checked before the infra patterns.

/\A(?:NoMethodError|NameError|ArgumentError|TypeError|KeyError|IndexError|RuntimeError|NotImplementedError|FrozenError|ZeroDivisionError|NoMatchingPatternError)\b/

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.classify_failure(error_message) ⇒ Object



62
63
64
65
66
67
68
# File 'app/helpers/solid_loop/application_helper.rb', line 62

def self.classify_failure(error_message)
  msg = error_message.to_s.strip
  return :agent if msg.blank?
  return :agent if msg.match?(CODE_ERROR_PREFIX)

  INFRA_ERROR_PATTERNS.any? { |re| msg.match?(re) } ? :infra : :agent
end

Instance Method Details

#sl_failure_kind(loop) ⇒ Object

---- Failure classification ------------------------------------------- A loop's terminal error is captured only as a free-text error_message (e.g. "HTTP 403: ...", "Faraday::ConnectionFailed: ...", or an agent-side exception). Classify without a schema change by pattern-matching that string: provider/transport problems (HTTP status, Faraday, JSON parse of the wire response, PII-redaction rejection) are INFRA failures — not agent bugs. Everything else that reached failed is an AGENT failure.

Returns :infra, :agent, or nil (loop is not failed / has no message).



41
42
43
44
45
# File 'app/helpers/solid_loop/application_helper.rb', line 41

def sl_failure_kind(loop)
  return nil unless loop.status == "failed"

  SolidLoop::ApplicationHelper.classify_failure(loop.error_message)
end

#sl_failure_reason(loop, limit: 140) ⇒ Object

A short, human failure reason for inline display / tooltips. Trims the noisy body so a table cell stays readable; full text is on the loop page.



72
73
74
75
76
77
78
# File 'app/helpers/solid_loop/application_helper.rb', line 72

def sl_failure_reason(loop, limit: 140)
  msg = loop.error_message.to_s.strip
  return nil if msg.empty?

  msg = msg.gsub(/\s+/, " ")
  msg.length > limit ? "#{msg[0, limit - 1]}" : msg
end

#sl_money(amount) ⇒ Object

---- Money ------------------------------------------------------------- One money formatter used everywhere in the admin so the same cost never reads as "$0.09" in one place, "$0.0248" in another and "$0.00029" in a third. Shows 2 dp for "normal" amounts but keeps enough significant figures that a sub-cent run never collapses to "$0.00".

1.2345   -> "$1.23"
0.0248   -> "$0.025"
0.00029  -> "$0.00029"
0.0      -> "$0.00"

-0.005 -> "-$0.0050"



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'app/helpers/solid_loop/application_helper.rb', line 14

def sl_money(amount)
  value = amount.to_f
  return "$0.00" if value.zero?

  abs = value.abs
  # Default to 2 dp so "normal" money reads $0.09 / $1.23. But when 2 dp
  # would lose precision (0.0248 -> 0.02) OR round a real amount away to
  # zero (0.00029 -> 0.00), extend precision to ~2 significant figures so
  # sub-cent runs stay legible instead of collapsing.
  precision = 2
  if abs < 0.1 && (abs - abs.round(2)).abs > Float::EPSILON
    precision = [(-Math.log10(abs)).floor + 2, 8].min
  end

  # Sign goes before the "$" (never "$-0.0050").
  "#{'-' if value.negative?}$#{number_with_precision(abs, precision: precision)}"
end

---- Subject linking --------------------------------------------------- A loop's polymorphic subject ("User #2") is an app record the engine can't route to. Host apps register a resolver returning a URL (and, optionally, a richer label) so the admin can deep-link to it. When no resolver is configured we degrade to the plain "Type #id" text.

SolidLoop.subject_resolver = ->(loop) {
{ url: main_app.user_path(loop.subject_id), label: loop.subject&.name }
}


89
90
91
92
93
94
95
96
97
# File 'app/helpers/solid_loop/application_helper.rb', line 89

def sl_subject_link(loop)
  return nil if loop.subject_type.blank? && loop.subject_id.blank?

  resolved = sl_resolve_subject(loop)
  label = resolved[:label].presence || "#{loop.subject_type} ##{loop.subject_id}"
  url = sl_safe_subject_url(resolved[:url])

  url ? link_to(label, url) : label
end