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`.



106
107
108
109
110
# File 'app/helpers/sessions/engine_helper.rb', line 106

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

#sessions_format_time(time) ⇒ Object



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

def sessions_format_time(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