Module: ChronoForge::Dashboard::DashboardHelper

Defined in:
app/helpers/chrono_forge/dashboard/dashboard_helper.rb

Constant Summary collapse

STATE_ORDER =

Display order for state counts: active work first, terminal last. Any unknown states are appended so a new core state never silently vanishes.

%w[running idle stalled failed completed].freeze
KIND_LABELS =

Short, readable label for a parsed step kind.

{
  execute: "execute", sleep: "wait", wait: "wait until", continue: "continue if",
  repeat_coordination: "repeat", repeat_run: "run", lifecycle: "workflow",
  branch: "branch", merge: "merge", unknown: "step"
}.freeze
META_SKIP =

Human-friendly [label, value] pairs of a step’s metadata for the timeline — surfaces things like a wait’s resume time, a wait_until timeout, or a durably_repeat’s last execution. Keys are humanized; values are stringified (the view truncates). Blank values are dropped. Internal bookkeeping surfaced elsewhere (the linked error is rendered inline; branch poll state + spawn cursors show in the Branches panel), so they’d just be noise in the timeline’s metadata line. poll_token is the merge poller’s fencing token — pure plumbing, never user-facing.

%w[error_log_id poll poll_token cursors].freeze

Instance Method Summary collapse

Instance Method Details

#cf_absolute_time?Boolean

Whether the viewer prefers absolute timestamps (cookie-persisted nav toggle).

Returns:

  • (Boolean)


45
46
47
# File 'app/helpers/chrono_forge/dashboard/dashboard_helper.rb', line 45

def cf_absolute_time?
  cookies[:cf_time_format] == "absolute"
end

#cf_ago(t) ⇒ Object

A timestamp shown relative (“3 minutes ago”) or absolute (raw ISO8601) per the viewer’s preference, with the other form available on hover.



66
67
68
69
70
71
72
# File 'app/helpers/chrono_forge/dashboard/dashboard_helper.rb', line 66

def cf_ago(t)
  return "" unless t
  rel = "#{time_ago_in_words(t)} ago"
  abs = t.iso8601
  shown, hover = cf_absolute_time? ? [abs, rel] : [rel, abs]
  tag.span(shown, title: hover, class: "cursor-help")
end

#cf_badge(state) ⇒ Object



12
13
14
# File 'app/helpers/chrono_forge/dashboard/dashboard_helper.rb', line 12

def cf_badge(state)
  tag.span(state, class: "cf-pill cf-pill-#{state}")
end

#cf_bar_width(value, max) ⇒ Object

Class name for a stacked-bar segment, width quantized to 5% steps so it stays CSP-safe (no inline style — see .cf-bar-ChronoForge::Dashboard::DashboardHelper.0.0..100 in tailwind.css).



93
94
95
96
# File 'app/helpers/chrono_forge/dashboard/dashboard_helper.rb', line 93

def cf_bar_width(value, max)
  pct = (max.to_f.zero? ? 0 : (value / max.to_f * 100))
  "cf-bar-#{(pct / 5).round * 5}"
end

#cf_capped(count, cap) ⇒ Object

A capped count: shows “5000+” once the count saturates its cap.



40
41
42
# File 'app/helpers/chrono_forge/dashboard/dashboard_helper.rb', line 40

def cf_capped(count, cap)
  (count >= cap) ? "#{cap}+" : count.to_s
end

#cf_chip(extra = nil) ⇒ Object

Shared “chip” treatment for inline nav/action links (metrics, details, repetitions, open, pagination, back) — a subtle bordered button, never an underlined text link. Pass extra utility classes (margins, truncation).



19
20
21
# File 'app/helpers/chrono_forge/dashboard/dashboard_helper.rb', line 19

def cf_chip(extra = nil)
  ["inline-flex items-center rounded-md border border-zinc-200 px-2 py-0.5 text-xs text-zinc-600 hover:bg-zinc-50", extra].compact.join(" ")
end

#cf_dot(state) ⇒ Object



31
32
33
# File 'app/helpers/chrono_forge/dashboard/dashboard_helper.rb', line 31

def cf_dot(state)
  tag.span(class: "cf-dot cf-dot-#{state}")
end

#cf_duration(from, to) ⇒ Object

Human duration between two times (e.g. “1m 04s”); “—” if unfinished.



75
76
77
78
# File 'app/helpers/chrono_forge/dashboard/dashboard_helper.rb', line 75

def cf_duration(from, to)
  return "" unless from && to
  cf_secs((to - from).to_i)
end

#cf_kind_label(kind) ⇒ Object



122
123
124
# File 'app/helpers/chrono_forge/dashboard/dashboard_helper.rb', line 122

def cf_kind_label(kind)
  KIND_LABELS.fetch(kind, kind.to_s)
end

#cf_latency_summary(latencies) ⇒ Object

Concise latency summary (avg + most recent) from a list of run seconds.



109
110
111
112
113
# File 'app/helpers/chrono_forge/dashboard/dashboard_helper.rb', line 109

def cf_latency_summary(latencies)
  return "" if latencies.blank?
  avg = (latencies.sum.to_f / latencies.size).round
  "avg #{avg}s · last #{latencies.last}s"
end

#cf_meta_pairs(metadata) ⇒ Object



136
137
138
139
140
141
# File 'app/helpers/chrono_forge/dashboard/dashboard_helper.rb', line 136

def cf_meta_pairs()
  return [] unless .is_a?(Hash)
  
    .reject { |k, v| v.nil? || v == "" || META_SKIP.include?(k.to_s) }
    .map { |k, v| [k.to_s.tr("_", " "), v.to_s] }
end

#cf_pct(rate) ⇒ Object

A rate (0.0–1.0) as a percentage; “—” if nil. Keeps tiny non-zero rates visible (a 0.0008% workflow-failure rate shows “<0.01%”, never “0%”).



100
101
102
103
104
105
106
# File 'app/helpers/chrono_forge/dashboard/dashboard_helper.rb', line 100

def cf_pct(rate)
  return "" if rate.nil?
  pct = rate * 100
  return "0%" if pct.zero?
  return "<0.01%" if pct < 0.01
  (pct < 1) ? "#{pct.round(2)}%" : "#{pct.round}%"
end

#cf_poll_intervalObject



53
54
55
56
57
# File 'app/helpers/chrono_forge/dashboard/dashboard_helper.rb', line 53

def cf_poll_interval
  raw = cookies[:cf_poll_interval]
  return raw.to_i if raw.present? && raw.match?(/\A\d+\z/)
  ChronoForge::Dashboard.config.polling_interval.to_i
end

#cf_poll_label(secs) ⇒ Object



59
60
61
62
# File 'app/helpers/chrono_forge/dashboard/dashboard_helper.rb', line 59

def cf_poll_label(secs)
  return "off" if secs.zero?
  (secs % 60 == 0) ? "#{secs / 60}m" : "#{secs}s"
end

#cf_poll_optionsObject

Auto-refresh interval in seconds (0 = off). A cookie-persisted nav control overrides the configured default per viewer; options come from config.



51
# File 'app/helpers/chrono_forge/dashboard/dashboard_helper.rb', line 51

def cf_poll_options = ChronoForge::Dashboard.config.polling_interval_options

#cf_secs(secs) ⇒ Object

Human duration from a number of seconds, scaled to the two most-significant units (e.g. “45s”, “1m 04s”, “3h 12m”, “2d 21h”); “—” if nil.



82
83
84
85
86
87
88
89
# File 'app/helpers/chrono_forge/dashboard/dashboard_helper.rb', line 82

def cf_secs(secs)
  return "" if secs.nil?
  secs = secs.to_i
  return "#{secs}s" if secs < 60
  return "#{secs / 60}m #{(secs % 60).to_s.rjust(2, "0")}s" if secs < 3600
  return "#{secs / 3600}h #{(secs % 3600 / 60).to_s.rjust(2, "0")}m" if secs < 86400
  "#{secs / 86400}d #{(secs % 86400 / 3600).to_s.rjust(2, "0")}h"
end

#cf_state_badge(workflow, wait = nil) ⇒ Object

State badge, upgraded to “scheduled” for an idle workflow parked on a wait whose wake time is still in the future — so genuinely-scheduled work doesn’t read as “stuck idle”.



26
27
28
29
# File 'app/helpers/chrono_forge/dashboard/dashboard_helper.rb', line 26

def cf_state_badge(workflow, wait = nil)
  return cf_badge("scheduled") if workflow.idle? && wait&.scheduled?
  cf_badge(workflow.state)
end

#cf_state_order(keys) ⇒ Object



8
9
10
# File 'app/helpers/chrono_forge/dashboard/dashboard_helper.rb', line 8

def cf_state_order(keys)
  (STATE_ORDER & keys) + (keys - STATE_ORDER)
end

#cf_status_color(status) ⇒ Object

Text color for an execution-log status (pending/completed/failed).



144
145
146
147
148
149
150
# File 'app/helpers/chrono_forge/dashboard/dashboard_helper.rb', line 144

def cf_status_color(status)
  case status
  when "completed" then "text-emerald-600"
  when "failed" then "text-rose-600"
  else "text-zinc-500"
  end
end

#cf_time(t) ⇒ Object



35
36
37
# File 'app/helpers/chrono_forge/dashboard/dashboard_helper.rb', line 35

def cf_time(t)
  t&.iso8601 || ""
end