ruby_llm-agents 1.3.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -334
  3. data/app/controllers/concerns/ruby_llm/agents/sortable.rb +0 -1
  4. data/app/controllers/ruby_llm/agents/agents_controller.rb +5 -56
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +22 -106
  6. data/app/controllers/ruby_llm/agents/executions_controller.rb +4 -114
  7. data/app/controllers/ruby_llm/agents/tenants_controller.rb +30 -2
  8. data/app/helpers/ruby_llm/agents/application_helper.rb +19 -53
  9. data/app/models/ruby_llm/agents/execution/analytics.rb +13 -54
  10. data/app/models/ruby_llm/agents/execution/scopes.rb +61 -14
  11. data/app/models/ruby_llm/agents/execution.rb +46 -10
  12. data/app/models/ruby_llm/agents/execution_detail.rb +18 -0
  13. data/app/models/ruby_llm/agents/tenant/budgetable.rb +132 -24
  14. data/app/models/ruby_llm/agents/tenant/incrementable.rb +117 -0
  15. data/app/models/ruby_llm/agents/tenant/resettable.rb +128 -0
  16. data/app/models/ruby_llm/agents/tenant/trackable.rb +46 -12
  17. data/app/models/ruby_llm/agents/tenant.rb +2 -3
  18. data/app/models/ruby_llm/agents/tenant_budget.rb +6 -3
  19. data/app/services/ruby_llm/agents/agent_registry.rb +6 -112
  20. data/app/views/layouts/ruby_llm/agents/application.html.erb +87 -252
  21. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +71 -218
  22. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +20 -63
  23. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +44 -131
  24. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +16 -57
  25. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +39 -104
  26. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +29 -82
  27. data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +4 -14
  28. data/app/views/ruby_llm/agents/agents/index.html.erb +105 -274
  29. data/app/views/ruby_llm/agents/agents/show.html.erb +248 -378
  30. data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +29 -52
  31. data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +73 -99
  32. data/app/views/ruby_llm/agents/dashboard/index.html.erb +228 -433
  33. data/app/views/ruby_llm/agents/executions/_execution.html.erb +1 -1
  34. data/app/views/ruby_llm/agents/executions/_filters.html.erb +4 -25
  35. data/app/views/ruby_llm/agents/executions/_list.html.erb +111 -152
  36. data/app/views/ruby_llm/agents/executions/index.html.erb +5 -7
  37. data/app/views/ruby_llm/agents/executions/show.html.erb +528 -989
  38. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +5 -21
  39. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +70 -191
  40. data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +16 -44
  41. data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +12 -41
  42. data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +11 -65
  43. data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +6 -5
  44. data/app/views/ruby_llm/agents/system_config/show.html.erb +240 -351
  45. data/app/views/ruby_llm/agents/tenants/_form.html.erb +67 -77
  46. data/app/views/ruby_llm/agents/tenants/edit.html.erb +7 -9
  47. data/app/views/ruby_llm/agents/tenants/index.html.erb +100 -122
  48. data/app/views/ruby_llm/agents/tenants/show.html.erb +146 -336
  49. data/config/routes.rb +0 -13
  50. data/lib/generators/ruby_llm_agents/install_generator.rb +9 -14
  51. data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +2 -12
  52. data/lib/generators/ruby_llm_agents/restructure_generator.rb +0 -2
  53. data/lib/generators/ruby_llm_agents/templates/add_usage_counters_to_tenants_migration.rb.tt +37 -0
  54. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +1 -2
  55. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +1 -1
  56. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +0 -1
  57. data/lib/generators/ruby_llm_agents/templates/create_execution_details_migration.rb.tt +27 -0
  58. data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +25 -0
  59. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +0 -1
  60. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +9 -12
  61. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +40 -71
  62. data/lib/generators/ruby_llm_agents/templates/remove_agent_version_migration.rb.tt +13 -0
  63. data/lib/generators/ruby_llm_agents/templates/remove_workflow_columns_migration.rb.tt +19 -0
  64. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +2 -4
  65. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +0 -1
  66. data/lib/generators/ruby_llm_agents/templates/split_execution_details_migration.rb.tt +232 -0
  67. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +58 -262
  68. data/lib/ruby_llm/agents/audio/speaker.rb +0 -1
  69. data/lib/ruby_llm/agents/audio/transcriber.rb +0 -1
  70. data/lib/ruby_llm/agents/base_agent.rb +52 -6
  71. data/lib/ruby_llm/agents/core/base/callbacks.rb +142 -0
  72. data/lib/ruby_llm/agents/core/base.rb +23 -55
  73. data/lib/ruby_llm/agents/core/configuration.rb +58 -117
  74. data/lib/ruby_llm/agents/core/errors.rb +0 -58
  75. data/lib/ruby_llm/agents/core/instrumentation.rb +157 -110
  76. data/lib/ruby_llm/agents/core/llm_tenant.rb +8 -7
  77. data/lib/ruby_llm/agents/core/version.rb +1 -1
  78. data/lib/ruby_llm/agents/dsl/base.rb +157 -17
  79. data/lib/ruby_llm/agents/dsl/caching.rb +33 -2
  80. data/lib/ruby_llm/agents/dsl/reliability.rb +148 -0
  81. data/lib/ruby_llm/agents/dsl.rb +1 -2
  82. data/lib/ruby_llm/agents/image/analyzer/execution.rb +1 -2
  83. data/lib/ruby_llm/agents/image/background_remover/execution.rb +1 -2
  84. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +1 -13
  85. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -2
  86. data/lib/ruby_llm/agents/image/editor/dsl.rb +0 -14
  87. data/lib/ruby_llm/agents/image/editor/execution.rb +1 -10
  88. data/lib/ruby_llm/agents/image/editor.rb +0 -1
  89. data/lib/ruby_llm/agents/image/generator.rb +0 -21
  90. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +0 -13
  91. data/lib/ruby_llm/agents/image/pipeline/execution.rb +0 -1
  92. data/lib/ruby_llm/agents/image/transformer/dsl.rb +0 -13
  93. data/lib/ruby_llm/agents/image/transformer/execution.rb +1 -10
  94. data/lib/ruby_llm/agents/image/transformer.rb +0 -1
  95. data/lib/ruby_llm/agents/image/upscaler/execution.rb +1 -2
  96. data/lib/ruby_llm/agents/image/variator/execution.rb +1 -2
  97. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +78 -173
  98. data/lib/ruby_llm/agents/infrastructure/attempt_tracker.rb +1 -0
  99. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
  100. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
  101. data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
  102. data/lib/ruby_llm/agents/infrastructure/reliability.rb +37 -2
  103. data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
  104. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
  105. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
  106. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
  107. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
  108. data/lib/ruby_llm/agents/rails/engine.rb +6 -6
  109. data/lib/ruby_llm/agents/results/base.rb +1 -49
  110. data/lib/ruby_llm/agents/text/embedder.rb +0 -1
  111. data/lib/ruby_llm/agents.rb +1 -9
  112. data/lib/tasks/ruby_llm_agents.rake +34 -0
  113. metadata +12 -81
  114. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
  115. data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
  116. data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
  117. data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
  118. data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
  119. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
  120. data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
  121. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
  122. data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
  123. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
  124. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
  125. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
  126. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
  127. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
  128. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
  129. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
  130. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
  131. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
  132. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
  133. data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
  134. data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
  135. data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
  136. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
  137. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
  138. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
  139. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
  140. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
  141. data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
  142. data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
  143. data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
  144. data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
  145. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
  146. data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
  147. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
  148. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
  149. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
  150. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
  151. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
  152. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
  153. data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
  154. data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
  155. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
  156. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
  157. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
  158. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
  159. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
  160. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
  161. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
  162. data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
  163. data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
  164. data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
  165. data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
  166. data/lib/ruby_llm/agents/text/moderator.rb +0 -237
  167. data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
  168. data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
  169. data/lib/ruby_llm/agents/workflow/async.rb +0 -220
  170. data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
  171. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
  172. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
  173. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
  174. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
  175. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
  176. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
  177. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
  178. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
  179. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
  180. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
  181. data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
  182. data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
  183. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
  184. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
  185. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
  186. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
  187. data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
  188. data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
  189. data/lib/ruby_llm/agents/workflow/result.rb +0 -592
  190. data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
  191. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
  192. data/lib/ruby_llm/agents/workflow/wait_result.rb +0 -213
@@ -1,150 +1,140 @@
1
1
  <%# Form partial for tenant budget editing %>
2
- <%= form_with model: tenant, url: tenant_path(tenant), method: :patch, class: "space-y-6" do |f| %>
2
+ <%= form_with model: tenant, url: tenant_path(tenant), method: :patch, class: "space-y-8" do |f| %>
3
3
  <% if tenant.errors.any? %>
4
- <div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
5
- <div class="flex items-center">
6
- <svg class="w-5 h-5 text-red-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
7
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
8
- </svg>
9
- <h3 class="text-sm font-medium text-red-800 dark:text-red-200">
10
- Please fix the following errors:
11
- </h3>
12
- </div>
13
- <ul class="mt-2 ml-7 list-disc list-inside text-sm text-red-700 dark:text-red-300">
14
- <% tenant.errors.full_messages.each do |message| %>
15
- <li><%= message %></li>
16
- <% end %>
17
- </ul>
4
+ <div class="font-mono text-xs text-red-500 dark:text-red-400 space-y-1 py-2">
5
+ <% tenant.errors.full_messages.each do |message| %>
6
+ <div>· <%= message %></div>
7
+ <% end %>
18
8
  </div>
19
9
  <% end %>
20
10
 
21
- <!-- Basic Information -->
22
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
23
- <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Basic Information</h3>
11
+ <!-- ── basic info ──────────────────── -->
12
+ <div>
13
+ <div class="flex items-center gap-3 mb-4">
14
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">basic info</span>
15
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
16
+ </div>
24
17
 
25
18
  <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
26
19
  <div>
27
- <%= f.label :name, "Display Name", class: "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" %>
20
+ <%= f.label :name, "display name", class: "block text-xs font-mono text-gray-500 dark:text-gray-400 mb-1" %>
28
21
  <%= f.text_field :name,
29
- class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-100",
22
+ class: "w-full px-3 py-2 bg-transparent border border-gray-200 dark:border-gray-800 rounded text-sm font-mono text-gray-900 dark:text-gray-100 focus:ring-1 focus:ring-gray-400 dark:focus:ring-gray-600 focus:border-gray-400 dark:focus:border-gray-600 placeholder-gray-400 dark:placeholder-gray-600",
30
23
  placeholder: "e.g., Acme Corporation" %>
31
- <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
32
- Human-readable name for this tenant. Falls back to tenant ID if not set.
24
+ <p class="mt-1 text-[10px] font-mono text-gray-400 dark:text-gray-600">
25
+ Falls back to tenant ID if not set.
33
26
  </p>
34
27
  </div>
35
28
 
36
29
  <div>
37
- <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Tenant ID</label>
38
- <p class="w-full px-3 py-2 bg-gray-50 dark:bg-gray-700/50 border border-gray-200 dark:border-gray-600 rounded-md text-gray-900 dark:text-gray-100 font-mono text-sm">
30
+ <label class="block text-xs font-mono text-gray-500 dark:text-gray-400 mb-1">tenant id</label>
31
+ <p class="w-full px-3 py-2 bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded text-sm font-mono text-gray-500 dark:text-gray-400">
39
32
  <%= tenant.tenant_id %>
40
33
  </p>
41
- <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
34
+ <p class="mt-1 text-[10px] font-mono text-gray-400 dark:text-gray-600">
42
35
  System identifier. Cannot be changed.
43
36
  </p>
44
37
  </div>
45
38
  </div>
46
39
  </div>
47
40
 
48
- <!-- Budget Limits (USD) -->
49
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
50
- <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Budget Limits (USD)</h3>
41
+ <!-- ── budget limits ──────────────────── -->
42
+ <div>
43
+ <div class="flex items-center gap-3 mb-4">
44
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">budget limits (usd)</span>
45
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
46
+ </div>
51
47
 
52
48
  <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
53
49
  <div>
54
- <%= f.label :daily_limit, "Daily Limit", class: "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" %>
50
+ <%= f.label :daily_limit, "daily limit", class: "block text-xs font-mono text-gray-500 dark:text-gray-400 mb-1" %>
55
51
  <div class="relative">
56
- <span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-500 dark:text-gray-400">$</span>
52
+ <span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 dark:text-gray-600 font-mono text-sm">$</span>
57
53
  <%= f.number_field :daily_limit,
58
- class: "w-full pl-7 pr-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-100",
54
+ class: "w-full pl-7 pr-3 py-2 bg-transparent border border-gray-200 dark:border-gray-800 rounded text-sm font-mono text-gray-900 dark:text-gray-100 focus:ring-1 focus:ring-gray-400 dark:focus:ring-gray-600 focus:border-gray-400 dark:focus:border-gray-600 placeholder-gray-400 dark:placeholder-gray-600",
59
55
  step: "0.01",
60
56
  min: "0",
61
- placeholder: "e.g., 50.00" %>
57
+ placeholder: "50.00" %>
62
58
  </div>
63
- <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
64
- Maximum daily spend in USD. Leave blank for no limit.
59
+ <p class="mt-1 text-[10px] font-mono text-gray-400 dark:text-gray-600">
60
+ Leave blank for no limit.
65
61
  </p>
66
62
  </div>
67
63
 
68
64
  <div>
69
- <%= f.label :monthly_limit, "Monthly Limit", class: "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" %>
65
+ <%= f.label :monthly_limit, "monthly limit", class: "block text-xs font-mono text-gray-500 dark:text-gray-400 mb-1" %>
70
66
  <div class="relative">
71
- <span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-500 dark:text-gray-400">$</span>
67
+ <span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400 dark:text-gray-600 font-mono text-sm">$</span>
72
68
  <%= f.number_field :monthly_limit,
73
- class: "w-full pl-7 pr-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-100",
69
+ class: "w-full pl-7 pr-3 py-2 bg-transparent border border-gray-200 dark:border-gray-800 rounded text-sm font-mono text-gray-900 dark:text-gray-100 focus:ring-1 focus:ring-gray-400 dark:focus:ring-gray-600 focus:border-gray-400 dark:focus:border-gray-600 placeholder-gray-400 dark:placeholder-gray-600",
74
70
  step: "0.01",
75
71
  min: "0",
76
- placeholder: "e.g., 500.00" %>
72
+ placeholder: "500.00" %>
77
73
  </div>
78
- <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
79
- Maximum monthly spend in USD. Leave blank for no limit.
74
+ <p class="mt-1 text-[10px] font-mono text-gray-400 dark:text-gray-600">
75
+ Leave blank for no limit.
80
76
  </p>
81
77
  </div>
82
78
  </div>
83
79
  </div>
84
80
 
85
- <!-- Token Limits -->
86
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
87
- <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Token Limits</h3>
81
+ <!-- ── token limits ──────────────────── -->
82
+ <div>
83
+ <div class="flex items-center gap-3 mb-4">
84
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">token limits</span>
85
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
86
+ </div>
88
87
 
89
88
  <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
90
89
  <div>
91
- <%= f.label :daily_token_limit, "Daily Token Limit", class: "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" %>
90
+ <%= f.label :daily_token_limit, "daily token limit", class: "block text-xs font-mono text-gray-500 dark:text-gray-400 mb-1" %>
92
91
  <%= f.number_field :daily_token_limit,
93
- class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-100",
92
+ class: "w-full px-3 py-2 bg-transparent border border-gray-200 dark:border-gray-800 rounded text-sm font-mono text-gray-900 dark:text-gray-100 focus:ring-1 focus:ring-gray-400 dark:focus:ring-gray-600 focus:border-gray-400 dark:focus:border-gray-600 placeholder-gray-400 dark:placeholder-gray-600",
94
93
  step: "1",
95
94
  min: "0",
96
- placeholder: "e.g., 1000000" %>
97
- <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
98
- Maximum daily token usage. Leave blank for no limit.
95
+ placeholder: "1000000" %>
96
+ <p class="mt-1 text-[10px] font-mono text-gray-400 dark:text-gray-600">
97
+ Leave blank for no limit.
99
98
  </p>
100
99
  </div>
101
100
 
102
101
  <div>
103
- <%= f.label :monthly_token_limit, "Monthly Token Limit", class: "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" %>
102
+ <%= f.label :monthly_token_limit, "monthly token limit", class: "block text-xs font-mono text-gray-500 dark:text-gray-400 mb-1" %>
104
103
  <%= f.number_field :monthly_token_limit,
105
- class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-100",
104
+ class: "w-full px-3 py-2 bg-transparent border border-gray-200 dark:border-gray-800 rounded text-sm font-mono text-gray-900 dark:text-gray-100 focus:ring-1 focus:ring-gray-400 dark:focus:ring-gray-600 focus:border-gray-400 dark:focus:border-gray-600 placeholder-gray-400 dark:placeholder-gray-600",
106
105
  step: "1",
107
106
  min: "0",
108
- placeholder: "e.g., 10000000" %>
109
- <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
110
- Maximum monthly token usage. Leave blank for no limit.
107
+ placeholder: "10000000" %>
108
+ <p class="mt-1 text-[10px] font-mono text-gray-400 dark:text-gray-600">
109
+ Leave blank for no limit.
111
110
  </p>
112
111
  </div>
113
112
  </div>
114
113
  </div>
115
114
 
116
- <!-- Enforcement -->
117
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
118
- <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Enforcement</h3>
115
+ <!-- ── enforcement ──────────────────── -->
116
+ <div>
117
+ <div class="flex items-center gap-3 mb-4">
118
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">enforcement</span>
119
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
120
+ </div>
119
121
 
120
- <div>
121
- <%= f.label :enforcement, "Enforcement Mode", class: "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" %>
122
+ <div class="max-w-md">
123
+ <%= f.label :enforcement, "enforcement mode", class: "block text-xs font-mono text-gray-500 dark:text-gray-400 mb-1" %>
122
124
  <%= f.select :enforcement,
123
125
  options_for_select([
124
- ["None - No enforcement", "none"],
125
- ["Soft - Log warnings but allow requests", "soft"],
126
- ["Hard - Block requests when limits exceeded", "hard"]
126
+ ["none tracking only", "none"],
127
+ ["soft log warnings, allow requests", "soft"],
128
+ ["hard block requests when exceeded", "hard"]
127
129
  ], tenant.enforcement || "soft"),
128
130
  {},
129
- class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-100" %>
130
- <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
131
- Controls what happens when budget limits are exceeded.
132
- </p>
133
- </div>
134
-
135
- <div class="mt-4 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
136
- <h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Enforcement Mode Details</h4>
137
- <ul class="text-xs text-gray-600 dark:text-gray-400 space-y-1">
138
- <li><strong>None:</strong> Budget tracking only, no enforcement actions taken.</li>
139
- <li><strong>Soft:</strong> Logs warnings and triggers alerts when limits are exceeded, but allows requests to proceed.</li>
140
- <li><strong>Hard:</strong> Blocks agent executions when budget limits are exceeded. Use with caution.</li>
141
- </ul>
131
+ class: "w-full px-3 py-2 bg-transparent border border-gray-200 dark:border-gray-800 rounded text-sm font-mono text-gray-900 dark:text-gray-100 focus:ring-1 focus:ring-gray-400 dark:focus:ring-gray-600 focus:border-gray-400 dark:focus:border-gray-600" %>
142
132
  </div>
143
133
  </div>
144
134
 
145
135
  <!-- Actions -->
146
- <div class="flex items-center justify-end space-x-4">
147
- <%= link_to "Cancel", tenant_path(tenant), class: "px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors" %>
148
- <%= f.submit "Save Changes", class: "px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 cursor-pointer transition-colors" %>
136
+ <div class="flex items-center gap-4 pt-4 border-t border-gray-200 dark:border-gray-800">
137
+ <%= f.submit "save changes", class: "px-4 py-2 text-xs font-mono text-white bg-gray-900 dark:bg-gray-100 dark:text-gray-900 rounded hover:bg-gray-700 dark:hover:bg-gray-300 cursor-pointer transition-colors" %>
138
+ <%= link_to "cancel", tenant_path(tenant), class: "text-xs font-mono text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300" %>
149
139
  </div>
150
140
  <% end %>
@@ -1,13 +1,11 @@
1
- <%= render "ruby_llm/agents/shared/breadcrumbs", items: [
2
- { label: "Dashboard", path: ruby_llm_agents.root_path },
3
- { label: "Tenants", path: ruby_llm_agents.tenants_path },
4
- { label: @tenant.display_name, path: ruby_llm_agents.tenant_path(@tenant) },
5
- { label: "Edit" }
6
- ] %>
1
+ <!-- Back link -->
2
+ <nav class="font-mono text-xs mb-6">
3
+ <%= link_to "← #{@tenant.display_name}", ruby_llm_agents.tenant_path(@tenant), class: "text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300" %>
4
+ </nav>
7
5
 
8
- <div class="mb-6">
9
- <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Edit Tenant</h1>
10
- <p class="text-gray-500 dark:text-gray-400 mt-1">Update budget configuration for <%= @tenant.display_name %></p>
6
+ <div class="flex items-center gap-3 mb-6">
7
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">edit tenant</span>
8
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
11
9
  </div>
12
10
 
13
11
  <%= render "form", tenant: @tenant %>
@@ -1,132 +1,110 @@
1
- <div class="mb-6">
2
- <div class="flex items-center gap-2">
3
- <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Tenants</h1>
1
+ <%
2
+ sc = @sort_params[:column]
3
+ sd = @sort_params[:direction]
4
+ sort_url = ->(col) {
5
+ dir = (col == sc && sd == "asc") ? "desc" : "asc"
6
+ url_for(request.query_parameters.merge(sort: col, direction: dir))
7
+ }
8
+ sort_active = ->(col) { col == sc ? "text-gray-600 dark:text-gray-300" : "" }
9
+ sort_arrow = ->(col) { col == sc ? (sd == "asc" ? " ↑" : " ↓") : "" }
10
+ %>
11
+
12
+ <!-- ── tenants ──────────────────────────── -->
13
+ <div class="flex items-center gap-3 mb-4">
14
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">tenants</span>
15
+ <div class="font-mono text-xs text-gray-400 dark:text-gray-500 flex items-center gap-1.5">
16
+ <span><%= @tenants.size %></span>
4
17
  <%= render "ruby_llm/agents/shared/doc_link" %>
5
18
  </div>
6
- <p class="text-gray-500 dark:text-gray-400 mt-1">Manage tenant budget configurations</p>
19
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
20
+
21
+ <!-- Search -->
22
+ <%= form_with url: tenants_path, method: :get, class: "flex items-center", data: { turbo: false } do %>
23
+ <% request.query_parameters.except("q").each do |key, value| %>
24
+ <input type="hidden" name="<%= key %>" value="<%= value %>">
25
+ <% end %>
26
+ <input type="text"
27
+ name="q"
28
+ value="<%= @search_query %>"
29
+ placeholder="search tenants…"
30
+ class="w-40 px-2 py-0.5 bg-transparent border border-gray-200 dark:border-gray-800 rounded text-xs font-mono text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-600 focus:ring-1 focus:ring-gray-400 dark:focus:ring-gray-600 focus:border-gray-400 dark:focus:border-gray-600"
31
+ autocomplete="off">
32
+ <% end %>
7
33
  </div>
8
34
 
9
35
  <% if @tenants.empty? %>
10
- <div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-8 text-center">
11
- <svg class="w-12 h-12 mx-auto text-gray-400 dark:text-gray-500 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
12
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
13
- </svg>
14
- <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-1">No tenants configured</h3>
15
- <p class="text-gray-500 dark:text-gray-400">Tenant budgets will appear here once created via the API or configuration.</p>
36
+ <div class="font-mono text-xs text-gray-400 dark:text-gray-600 py-4 px-2">
37
+ <% if @search_query.present? %>
38
+ no tenants matching "<%= @search_query %>"
39
+ <% else %>
40
+ no tenants configured
41
+ <% end %>
16
42
  </div>
17
43
  <% else %>
18
- <div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 overflow-hidden">
19
- <div class="overflow-x-auto">
20
- <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
21
- <thead class="bg-gray-50 dark:bg-gray-900/50">
22
- <tr>
23
- <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
24
- Tenant
25
- </th>
26
- <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
27
- Daily Budget
28
- </th>
29
- <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
30
- Monthly Budget
31
- </th>
32
- <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
33
- Daily Tokens
34
- </th>
35
- <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
36
- Monthly Tokens
37
- </th>
38
- <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
39
- Enforcement
40
- </th>
41
- <th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
42
- Actions
43
- </th>
44
- </tr>
45
- </thead>
46
- <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
47
- <% @tenants.each do |tenant| %>
48
- <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors cursor-pointer" data-href="<%= tenant_path(tenant) %>">
49
- <td class="px-6 py-4 whitespace-nowrap">
50
- <div class="flex items-center">
51
- <div class="flex-shrink-0 h-8 w-8 rounded-full bg-blue-100 dark:bg-blue-900/50 flex items-center justify-center">
52
- <svg class="w-4 h-4 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
53
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
54
- </svg>
55
- </div>
56
- <div class="ml-3">
57
- <div class="text-sm font-medium text-gray-900 dark:text-gray-100">
58
- <%= tenant.display_name %>
59
- </div>
60
- <% if tenant.name.present? && tenant.name != tenant.tenant_id %>
61
- <div class="text-xs text-gray-500 dark:text-gray-400 font-mono">
62
- <%= tenant.tenant_id %>
63
- </div>
64
- <% end %>
65
- </div>
66
- </div>
67
- </td>
68
- <td class="px-6 py-4 whitespace-nowrap">
69
- <% if tenant.effective_daily_limit %>
70
- <span class="text-sm font-medium text-gray-900 dark:text-gray-100">
71
- $<%= number_with_precision(tenant.effective_daily_limit, precision: 2) %>
72
- </span>
73
- <% else %>
74
- <span class="text-sm text-gray-400 dark:text-gray-500">No limit</span>
75
- <% end %>
76
- </td>
77
- <td class="px-6 py-4 whitespace-nowrap">
78
- <% if tenant.effective_monthly_limit %>
79
- <span class="text-sm font-medium text-gray-900 dark:text-gray-100">
80
- $<%= number_with_precision(tenant.effective_monthly_limit, precision: 2) %>
81
- </span>
82
- <% else %>
83
- <span class="text-sm text-gray-400 dark:text-gray-500">No limit</span>
84
- <% end %>
85
- </td>
86
- <td class="px-6 py-4 whitespace-nowrap">
87
- <% if tenant.effective_daily_token_limit %>
88
- <span class="text-sm font-medium text-gray-900 dark:text-gray-100">
89
- <%= number_with_delimiter(tenant.effective_daily_token_limit) %>
90
- </span>
91
- <% else %>
92
- <span class="text-sm text-gray-400 dark:text-gray-500">No limit</span>
93
- <% end %>
94
- </td>
95
- <td class="px-6 py-4 whitespace-nowrap">
96
- <% if tenant.effective_monthly_token_limit %>
97
- <span class="text-sm font-medium text-gray-900 dark:text-gray-100">
98
- <%= number_with_delimiter(tenant.effective_monthly_token_limit) %>
99
- </span>
100
- <% else %>
101
- <span class="text-sm text-gray-400 dark:text-gray-500">No limit</span>
102
- <% end %>
103
- </td>
104
- <td class="px-6 py-4 whitespace-nowrap">
105
- <%
106
- enforcement = tenant.effective_enforcement.to_s
107
- badge_class = case enforcement
108
- when "hard" then "bg-red-100 dark:bg-red-900/50 text-red-800 dark:text-red-200"
109
- when "soft" then "bg-yellow-100 dark:bg-yellow-900/50 text-yellow-800 dark:text-yellow-200"
110
- else "bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400"
111
- end
112
- %>
113
- <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium <%= badge_class %>">
114
- <%= enforcement.capitalize %>
115
- </span>
116
- </td>
117
- <td class="px-6 py-4 whitespace-nowrap text-right text-sm">
118
- <%= link_to tenant_path(tenant), class: "text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300" do %>
119
- View
120
- <% end %>
121
- <span class="text-gray-300 dark:text-gray-600 mx-2">|</span>
122
- <%= link_to edit_tenant_path(tenant), class: "text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300" do %>
123
- Edit
124
- <% end %>
125
- </td>
126
- </tr>
44
+ <!-- Column headers -->
45
+ <div class="flex items-center gap-3 px-2 -mx-2 font-mono text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-wider mb-1">
46
+ <%= link_to "name#{sort_arrow.call('name')}".html_safe, sort_url.call("name"),
47
+ class: "flex-[2] min-w-0 hover:text-gray-600 dark:hover:text-gray-300 #{sort_active.call('name')}" %>
48
+ <%= link_to "daily#{sort_arrow.call('daily_limit')}".html_safe, sort_url.call("daily_limit"),
49
+ class: "w-20 flex-shrink-0 text-right hidden sm:block hover:text-gray-600 dark:hover:text-gray-300 #{sort_active.call('daily_limit')}" %>
50
+ <%= link_to "monthly#{sort_arrow.call('monthly_limit')}".html_safe, sort_url.call("monthly_limit"),
51
+ class: "w-20 flex-shrink-0 text-right hidden sm:block hover:text-gray-600 dark:hover:text-gray-300 #{sort_active.call('monthly_limit')}" %>
52
+ <%= link_to "enforcement#{sort_arrow.call('enforcement')}".html_safe, sort_url.call("enforcement"),
53
+ class: "w-16 flex-shrink-0 hidden md:block hover:text-gray-600 dark:hover:text-gray-300 #{sort_active.call('enforcement')}" %>
54
+ <span class="w-16 flex-shrink-0 text-right hidden md:block">cost</span>
55
+ <span class="w-24 flex-shrink-0 text-right">last run</span>
56
+ </div>
57
+
58
+ <!-- Data rows -->
59
+ <div class="font-mono text-xs space-y-px">
60
+ <% @tenants.each do |tenant| %>
61
+ <%
62
+ enforcement = tenant.effective_enforcement.to_s
63
+ enforcement_badge = case enforcement
64
+ when "hard" then "badge-error"
65
+ when "soft" then "badge-timeout"
66
+ else ""
67
+ end
68
+ last_execution = tenant.executions.order(created_at: :desc).pick(:created_at)
69
+ %>
70
+ <div class="group flex items-center gap-3 py-1.5 px-2 -mx-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800/50 cursor-pointer"
71
+ onclick="window.location='<%= tenant_path(tenant) %>'">
72
+ <span class="flex-[2] min-w-0 truncate">
73
+ <span class="text-gray-900 dark:text-gray-200"><%= tenant.display_name %></span>
74
+ <% if tenant.name.present? && tenant.name != tenant.tenant_id %>
75
+ <span class="text-gray-400 dark:text-gray-600 ml-1 text-[10px]"><%= tenant.tenant_id %></span>
76
+ <% end %>
77
+ </span>
78
+ <span class="w-20 flex-shrink-0 text-right hidden sm:block">
79
+ <% if tenant.effective_daily_limit %>
80
+ <span class="text-gray-500 dark:text-gray-400">$<%= number_with_precision(tenant.effective_daily_limit, precision: 2) %></span>
81
+ <% else %>
82
+ <span class="text-gray-300 dark:text-gray-700">—</span>
83
+ <% end %>
84
+ </span>
85
+ <span class="w-20 flex-shrink-0 text-right hidden sm:block">
86
+ <% if tenant.effective_monthly_limit %>
87
+ <span class="text-gray-500 dark:text-gray-400">$<%= number_with_precision(tenant.effective_monthly_limit, precision: 2) %></span>
88
+ <% else %>
89
+ <span class="text-gray-300 dark:text-gray-700">—</span>
90
+ <% end %>
91
+ </span>
92
+ <span class="w-16 flex-shrink-0 hidden md:block">
93
+ <% if enforcement_badge.present? %>
94
+ <span class="badge badge-sm <%= enforcement_badge %>"><%= enforcement %></span>
95
+ <% else %>
96
+ <span class="text-gray-300 dark:text-gray-700">none</span>
97
+ <% end %>
98
+ </span>
99
+ <span class="w-16 flex-shrink-0 text-right text-gray-500 dark:text-gray-400 hidden md:inline">$<%= number_with_precision(tenant.cost, precision: 2) %></span>
100
+ <span class="w-24 flex-shrink-0 text-gray-400 dark:text-gray-600 text-right whitespace-nowrap">
101
+ <% if last_execution %>
102
+ <%= time_ago_in_words(last_execution) %>
103
+ <% else %>
104
+
127
105
  <% end %>
128
- </tbody>
129
- </table>
130
- </div>
106
+ </span>
107
+ </div>
108
+ <% end %>
131
109
  </div>
132
110
  <% end %>