Module: Sessions::EngineHelper

Defined in:
app/helpers/sessions/engine_helper.rb

Overview

View helpers for the devices page (and for the host-renderable partials).

Constant Summary collapse

DEVICE_ICONS =
{
  "desktop" => "🖥",
  "smartphone" => "📱",
  "tablet" => "📱",
  "native_ios" => "📱",
  "native_android" => "📱",
  "bot" => "🤖"
}.freeze
DEVICE_ICON_NAMES =

Semantic icon names (Heroicons vocabulary — map them onto whatever icon helper your app uses) so custom views don’t re-derive the device_type → icon mapping the gem already knows.

{
  "desktop" => "computer-desktop",
  "smartphone" => "device-phone-mobile",
  "tablet" => "device-tablet",
  "native_ios" => "device-phone-mobile",
  "native_android" => "device-phone-mobile",
  "bot" => "bug-ant"
}.freeze
EVENT_ICON_NAMES =
{
  "login" => "check-circle",
  "failed_login" => "x-circle",
  "logout" => "arrow-right-on-rectangle",
  "revoked" => "no-symbol",
  "expired" => "clock"
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.engine_mount_nameObject

The name of the route that mounts Sessions::Engine in the host app (“sessions” for a plain mount, the ‘as:` value otherwise). Memoized per-process; in development a changed mount name reloads routes and to_prepare re-touches this helper file, clearing the memo.



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'app/helpers/sessions/engine_helper.rb', line 74

def self.engine_mount_name
  return @engine_mount_name if defined?(@engine_mount_name)

  @engine_mount_name = begin
    route = Rails.application.routes.routes.find do |candidate|
      # The mount sits behind a Constraints wrapper; unwrap a bounded
      # number of times, checking BEFORE each unwrap — Rails::Engine
      # itself responds to #app (its middleware stack), so an unguarded
      # `while app.respond_to?(:app)` would walk straight past it.
      app = candidate.app
      matched = false
      3.times do
        matched = (app == Sessions::Engine)
        break if matched || !app.respond_to?(:app)

        app = app.app
      end
      matched
    end
    route&.name
  rescue StandardError
    nil
  end
end

.reset_engine_mount_name!Object



99
100
101
# File 'app/helpers/sessions/engine_helper.rb', line 99

def self.reset_engine_mount_name!
  remove_instance_variable(:@engine_mount_name) if defined?(@engine_mount_name)
end

Instance Method Details

#sessions_device_icon(session) ⇒ Object



36
37
38
# File 'app/helpers/sessions/engine_helper.rb', line 36

def sessions_device_icon(session)
  DEVICE_ICONS.fetch(session.device_type.to_s, "🌐")
end

#sessions_device_icon_name(session) ⇒ Object



40
41
42
# File 'app/helpers/sessions/engine_helper.rb', line 40

def sessions_device_icon_name(session)
  DEVICE_ICON_NAMES.fetch(session.device_type.to_s, "globe-alt")
end

#sessions_engine_routesObject

The engine’s route proxy when it’s mounted, nil otherwise — partials rendered inside a host that didn’t mount the engine simply omit the revoke buttons. The proxy method is named after the mount (‘sessions` by default, or whatever `as:` was given), so it’s DISCOVERED from the host’s named routes rather than assumed.



65
66
67
68
# File 'app/helpers/sessions/engine_helper.rb', line 65

def sessions_engine_routes
  name = Sessions::EngineHelper.engine_mount_name
  name && respond_to?(name) ? public_send(name) : nil
end

#sessions_event_icon_name(event) ⇒ Object



44
45
46
# File 'app/helpers/sessions/engine_helper.rb', line 44

def sessions_event_icon_name(event)
  EVENT_ICON_NAMES.fetch(event.event.to_s, "information-circle")
end

#sessions_format_date(date) ⇒ Object

“Signed in May 2, 2026” — localized when the host bundles date formats (rails-i18n or its own locale files), with a safe fallback so a bare host never 500s over a missing ‘date.formats.long`. nil-safe: without the guard, I18n.l(nil) raises I18n::ArgumentError and the rescue would then call nil.strftime — a trap for custom views passing a nullable column.



109
110
111
112
113
114
115
# File 'app/helpers/sessions/engine_helper.rb', line 109

def sessions_format_date(date)
  return nil unless date

  I18n.l(date, format: :long)
rescue I18n::MissingTranslationData, I18n::ArgumentError
  date.strftime("%Y-%m-%d")
end

#sessions_format_time(time) ⇒ Object



117
118
119
120
121
122
123
# File 'app/helpers/sessions/engine_helper.rb', line 117

def sessions_format_time(time)
  return nil unless time

  I18n.l(time, format: :short)
rescue I18n::MissingTranslationData, I18n::ArgumentError
  time.strftime("%Y-%m-%d %H:%M")
end

#sessions_last_active_in_words(session) ⇒ Object

“Active now” within the touch window, else “Active 3 minutes ago”.



49
50
51
52
53
54
55
56
57
58
# File 'app/helpers/sessions/engine_helper.rb', line 49

def sessions_last_active_in_words(session)
  # active_now? owns the window (config.touch_every) — the badge can't
  # honestly claim more freshness than the throttle records.
  return t("sessions.devices.active_now") if session.respond_to?(:active_now?) && session.active_now?

  time = session.last_active_at
  return nil unless time

  t("sessions.devices.active_ago", time: time_ago_in_words(time))
end