UI Components & Loading States#
Overview#
Implement reusable UI components and loading state patterns for consistent user experience across the application. This ticket creates the foundational UI building blocks used by other tickets.
Scope#
Included:
- ViewComponents for all new UI patterns
- Stimulus controllers for interactivity (tabs, toasts)
- Skeleton loader components for loading states
- Toast notification system
- Component templates (ERB files)
- Component tests
Excluded:
- Integration with specific pages (handled by feature tickets)
- Existing components (StatusPill, ProgressBar already exist)
Acceptance Criteria#
ViewComponents#
AgentStatusCardComponent:
- Component created: app/components/agent_status_card_component.rb
- Template created: app/components/agent_status_card_component.html.erb
- Displays: status badge, agent name, hash rate, error indicator
- Uses existing StatusPillComponent for status badge
- Error indicator shows count if > 0 errors in last 24h
- Card links to agent detail page
- Component delegates to Turbo Stream broadcast partials for real-time updates
- Individual card sections update without full-page refresh
- State, last seen, hash rate, and errors broadcast independently
- Broadcast partials are minimal and don't reference current_user
- Stable DOM IDs enable targeted replacement
-
error_count_last_24hmethod removed from component class, moved to partial logic - Fragment cache key uses
cache: agent(notcache: true) to incorporatecache_key_with_versionwith agent'supdated_attimestamp, preventing stale cached cards when agent state or errors change
Agent Index Partials:
Four targeted partials handle independent Turbo Stream broadcasts for agent index cards:
-
_index_state.html.erb- Displays agent state badge with color coding (online/offline/error). UsesStatusPillComponentand has a stable DOM ID usingdom_id(agent, :index_state)pattern. -
_index_last_seen.html.erb- Shows relative time since last heartbeat ("X minutes ago" or "Not seen yet"). DOM ID:dom_id(agent, :index_last_seen). -
_index_hash_rate.html.erb- Displays current hash rate or "N/A" fallback. DOM ID:dom_id(agent, :index_hash_rate). -
_index_errors.html.erb- Shows error count badge for last 24 hours (excluding info severity). Entire<div>has DOM ID:dom_id(agent, :index_errors). Text turns red when error count > 0.
Each partial:
- Has a stable DOM ID for targeted replacement
- Is broadcast via
broadcast_replace_later_tocallback (runs in background job) - Runs WITHOUT access to
current_useror session (background job context) - Contains only the minimum HTML needed for that specific UI element
Broadcast Callbacks:
Agent model callbacks trigger targeted broadcasts:
after_update_commit :broadcast_index_state, if: -> { saved_change_to_state? }
after_update_commit :broadcast_index_last_seen, if: -> { saved_change_to_last_seen_at? }
AgentError model callback triggers Agent#broadcast_index_errors:
after_create_commit -> { agent.broadcast_index_errors }
The Agent#broadcast_index_errors method follows the same pattern as broadcast_index_state and broadcast_index_last_seen: it uses broadcast_replace_later_to with a stable DOM ID (dom_id(agent, :index_errors)) to update the error count display on agent index cards in real-time. Keeping the broadcast contract on the Agent model maintains consistency across all agent index card updates.
HashcatStatus updates trigger hash rate broadcasts via update_agent_metrics method (uses update_columns to bypass callbacks, then manually broadcasts).
AgentDetailTabsComponent:
- Component created: app/components/agent_detail_tabs_component.rb
- Template created: app/components/agent_detail_tabs_component.html.erb
- Renders slots: overview_tab, errors_tab, configuration_tab, capabilities_tab
- Integrates with Stimulus tabs controller
- Preserves tab structure for Turbo Stream broadcasts
CampaignProgressComponent:
- Component created: app/components/campaign_progress_component.rb
- Template created: app/components/campaign_progress_component.html.erb
- Displays: progress bar, percentage text, ETA text
- Uses existing ProgressBarComponent or extends it
- Handles missing ETA gracefully ("Calculating...")
ErrorModalComponent:
- Component created: app/components/error_modal_component.rb
- Template created: app/components/error_modal_component.html.erb
- Displays: error message, severity badge, timestamp
- Uses Bootstrap modal component
- Severity badges: fatal/error (danger), warning (warning), info (info)
SystemHealthCardComponent:
- Component created: app/components/system_health_card_component.rb
- Template created: app/components/system_health_card_component.html.erb
- Displays: service name, status badge, latency, error message
- Status variants: healthy (success), unhealthy (danger), checking (secondary)
- Icons: check-circle (healthy), x-circle (unhealthy)
TaskActionsComponent:
- Component created: app/components/task_actions_component.rb
- Template created: app/components/task_actions_component.html.erb
- Conditional buttons: cancel, retry, reassign, view logs, download results
- Buttons shown based on task state and user permissions
- Uses plain Bootstrap button classes (
btn btn-primary,btn btn-danger,btn-sm, etc.)
SkeletonLoaderComponent:
- Component created: app/components/skeleton_loader_component.rb
- Template created: app/components/skeleton_loader_component.html.erb
- Types supported: , ,
- Configurable count (default: 5)
- Uses CSS animations for shimmer effect
- Matches layout of actual content
ToastNotificationComponent:
- Component created: app/components/toast_notification_component.rb
- Template created: app/components/toast_notification_component.html.erb
- Variants: success, danger, warning, info
- Integrates with Stimulus toast controller
- Auto-dismiss after 5 seconds
- Positioned in top-right corner
Stimulus Controllers#
tabs_controller.js:
- Controller created: app/javascript/controllers/tabs_controller.js
- Targets: tab, panel
- Action: switch(event) - switches active tab
- Shows first tab by default on connect
- Updates active tab styling
- Hides/shows panels with d-none class
toast_controller.js:
- Controller created: app/javascript/controllers/toast_controller.js
- Values: autohide (boolean, default: true), delay (number, default: 5000)
- Initializes Bootstrap Toast on connect
- Auto-dismiss after delay
- Removes element from DOM after hidden
- Imports Bootstrap Toast component
Shared Partials#
- Toast partial created: app/views/shared/_toast.html.erb
- Toast container added to layout: file/views/layouts/application.html.erb
- Container positioned for toast display (top-right)
Testing#
- Component tests for all ViewComponents
- Stimulus controller tests for tabs_controller
- Stimulus controller tests for toast_controller
- Visual regression tests (optional, if using tools)
- Verify components work in air-gapped environment (no external assets)
Technical References#
- Core Flows: spec:50650885-e043-4e99-960b-672342fc4139/c565e255-83e7-4d16-a4ec-d45011fa5cad (Flow 6: Loading & Feedback Patterns)
- Tech Plan: spec:50650885-e043-4e99-960b-672342fc4139/f3c30678-d7af-45ab-a95b-0d0714906b9e (Component Architecture, Stimulus Controllers)
- Turbo Stream Broadcasts: doc.md (section "Turbo Stream Broadcasts")
- Agent Monitoring: ticket:50650885-e043-4e99-960b-672342fc4139/[Agent Monitoring & Real-Time Updates]
Dependencies#
Requires:
- None (foundational ticket)
Blocks:
- ticket:50650885-e043-4e99-960b-672342fc4139/[Agent Monitoring & Real-Time Updates] - Provides AgentStatusCardComponent, tabs controller
- ticket:50650885-e043-4e99-960b-672342fc4139/[Campaign Progress & ETA Display] - Provides CampaignProgressComponent, ErrorModalComponent
- ticket:50650885-e043-4e99-960b-672342fc4139/[Task Management Actions] - Provides TaskActionsComponent
- ticket:50650885-e043-4e99-960b-672342fc4139/[System Health Monitoring] - Provides SystemHealthCardComponent
Implementation Notes#
- Follow existing ViewComponent patterns in file/components/
- Use
dry-initializeroption syntax (already used in existing components) - Inherit from
ApplicationViewComponent - Use plain Bootstrap HTML and utility classes for layout/structure
- ViewComponents should render Bootstrap HTML directly (no abstraction layer)
- Use Bootstrap utility classes:
btn btn-primary,d-flex,gap-2,card,list-group, etc. - Test components in isolation before integrating
- Ensure all assets (CSS, JS) are bundled for air-gapped deployment
- Use Bootstrap 5 components (already in stack)
Component Development Guidelines:
The Railsboot component abstraction layer has been removed (PR #706). All ViewComponents now use plain ERB templates with Bootstrap utility classes directly.
- Button patterns:
<button class="btn btn-primary btn-sm">,<a href="..." class="btn btn-secondary"> - Layout utilities:
d-flex,justify-content-between,align-items-center,gap-2,gap-3 - Card structure:
<div class="card"><div class="card-body">...</div></div> - List groups:
<ul class="list-group"><li class="list-group-item">...</li></ul> - Reference implementations: See
agent_status_card_component.html.erb,agent_detail_tabs_component.html.erbfor examples of plain Bootstrap HTML patterns
For detailed component patterns and theming guidelines, reference doc.md sections on "Catppuccin Macchiato Theme" and "Layout Grid". For frontend gotchas (navbar buttons, z-index utilities, Railsboot removal), reference doc.md section "Frontend & Accessibility".
Turbo Stream Broadcast Pattern:
The AgentStatusCardComponent demonstrates a reusable pattern for real-time component updates:
-
Extract Minimal Partials - Create small partials (
_index_state.html.erb,_index_hash_rate.html.erb,_index_errors.html.erb) that wrap a single UI element with a stable DOM ID usingdom_id(record, :suffix). -
Model Callbacks - Use conditional
after_update_commitcallbacks (e.g.,if: -> { saved_change_to_state? }) to broadcast only when specific attributes change, reducing unnecessary updates. TheAgent#broadcast_index_errorsmethod is called from theAgentErrormodel'safter_create_commitcallback to keep the broadcast contract on the Agent model. -
Background Job Context - Broadcast partials run in background jobs WITHOUT access to
current_useror session. All data must come from the model or explicit locals. -
Targeted Replacement - Use
broadcast_replace_later_towith the stable DOM ID as the target to update individual sections without full-page refresh. -
Fragment Cache Invalidation - When using fragment caching on index cards, use
cache: agent(notcache: true) so thatcache_key_with_versionincorporates the agent'supdated_attimestamp. This ensures cached cards are invalidated when agent state or errors change, preventing stale content from being served.
This pattern extends to other real-time components. See the Attack model's broadcast_index_state implementation for a similar approach. For constraints and gotchas, reference doc.md section "Turbo Stream Broadcasts".
Estimated Effort#
2-3 days (8 components + 2 Stimulus controllers + templates + tests)