Files
sure/app/views/settings/llm_usages/show.html.erb
Guillem Arias Fauste ea51612ac7 refactor(views): migrate 6 residual inline alerts to DS::Alert (#1933)
* refactor(views): migrate 6 residual inline alerts to DS::Alert

PR #1731 extended DS::Alert and migrated 9 inline alert blocks. Six
hand-rolled alert blocks slipped through that sweep and stayed on raw
palette tokens with no `theme-dark:` variants:

- `app/views/settings/llm_usages/show.html.erb` — "About Cost Estimates"
  blue info block. Most visible offender: `bg-blue-50 border border-blue-200`
  + `text-blue-900 / text-blue-700 / text-blue-600` rendered as a bright
  white-blue island in dark mode (the bug spotted on the LLM usage page).
- `app/views/accounts/confirm_unlink.html.erb` — yellow warning with
  bullet list.
- `app/views/oidc_accounts/new_user.html.erb` — blue info heading.
- `app/views/oidc_accounts/link.html.erb` — two blocks (yellow verify
  warning + blue create info). Also flips the file's pre-existing
  `text-gray-600` hint paragraph to `text-secondary` (caught by the
  `DeprecatedClasses` erb_lint rule on save).
- `app/views/rules/confirm.html.erb` — AI cost notice.
- `app/views/rules/confirm_all.html.erb` — AI cost notice.

All six migrate to `DS::Alert.new(title:, variant:)` (with a block content
slot for the rich/conditional bodies). DS::Alert resolves `bg-info/10`,
`border-info/20`, etc. from the `@theme` semantic tokens, so dark mode
now renders a subtle blue/yellow tint over the page surface instead of
a hardcoded light-mode pill.

Out of scope (left as-is, not alert-shaped):

- `app/views/assistant_messages/_tool_calls.html.erb` — a tool-call
  display panel (not an alert; needs its own token sweep).
- `app/views/import/rows/_form.html.erb` — inline cell-error tooltip
  (`bg-red-50 border border-red-200`) — also not alert-shaped; a future
  PR can swap it to `bg-destructive/10 border-destructive-subtle` once
  #1932 lands.

Surfaced while scanning DS drift for the LLM usage page bug. Tracking
issue: #1715 (closed but conceptually relevant) / #1911 (active drift
patrol).

* fix(oidc): keep alert description in <p>, retarget tests for DS::Alert title

CI on #1933 caught three test failures introduced by migrating the
two OIDC link alerts and the verify-redirect copy from hand-rolled
`<h3>` / `<p>` markup to `DS::Alert`:

1. `OidcAccountsControllerTest#test_should_show_create_account_option_for_new_user`
2. `OidcAccountsControllerTest#test_does_not_show_create_account_button_when_JIT_link-only_mode`
3. `SessionsControllerTest#test_redirects_to_account_linking_when_no_OIDC_identity_exists`

DS::Alert renders its `title:` slot as a `<p>` (semantically the alert
heading lives on the container's `aria-labelledby`, not on a heading
tag) and renders block / message content directly inside a `<div>`,
not a `<p>`. The pre-migration markup used `<h3>` for the heading and
`<p class="...text-blue-700">` for the description, so the tests
above asserted those specific tags.

Two fixes:

- `app/views/oidc_accounts/link.html.erb` — wrap the html_safe
  description bodies in explicit `<p>` tags inside the DS::Alert
  block. Restores the `<p>` element the session-redirect test asserts
  on, and keeps the description as a semantic paragraph rather than
  a bare text node inside the alert container.
- `test/controllers/oidc_accounts_controller_test.rb` — flip the two
  `assert_select "h3", text: "Create New Account"` calls to match the
  DS::Alert title `<p>`. The test was asserting an implementation
  detail of the pre-migration markup; switching to the new tag keeps
  the assertion meaningful (the heading text still has to render)
  without re-introducing an `<h3>` outside of DS::Alert.

* fix(test): match Create New Account title with regex (sr-only "Info:" prefix)

DS::Alert prepends `<span class="sr-only">Info:</span>` inside the
title `<p>`, so the full text content is "Info: Create New Account",
not "Create New Account". `assert_select "p", text: "Create New Account"`
requires an exact text match and rejected the prefixed string. Switch
to a regex match — keeps the heading-text assertion meaningful without
coupling to the screen-reader prefix.
2026-05-23 09:23:30 +02:00

176 lines
8.3 KiB
Plaintext

<%= content_for :page_title, t(".page_title") %>
<div class="bg-container rounded-xl shadow-border-xs p-4">
<div class="mb-6">
<p class="text-sm text-secondary"><%= t(".subtitle") %></p>
</div>
<!-- Date Range Filter -->
<div class="mb-6">
<%= form_with url: settings_llm_usage_path, method: :get, class: "flex gap-4 items-end flex-wrap" do |f| %>
<div class="w-full md:w-auto">
<%= f.label :start_date, t(".start_date"), class: "block text-sm font-medium text-primary mb-1" %>
<%= f.date_field :start_date, value: @start_date, class: "rounded-lg border border-primary px-3 py-2 text-sm bg-container-inset text-primary w-full" %>
</div>
<div class="w-full md:w-auto">
<%= f.label :end_date, t(".end_date"), class: "block text-sm font-medium text-primary mb-1" %>
<%= f.date_field :end_date, value: @end_date, class: "rounded-lg border border-primary px-3 py-2 text-sm bg-container-inset text-primary w-full" %>
</div>
<%= render DS::Button.new(variant: :primary, size: :md, type: "submit", text: t(".filter"), class: "md:w-auto w-full justify-center") %>
<% end %>
</div>
<!-- Statistics Summary -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div class="bg-container-inset rounded-lg p-4">
<div class="flex items-center gap-2 mb-2">
<%= icon "activity", class: "w-5 h-5 text-secondary" %>
<p class="text-xs font-medium text-secondary uppercase"><%= t(".total_requests") %></p>
</div>
<p class="text-2xl font-semibold text-primary"><%= number_with_delimiter(@statistics[:total_requests]) %></p>
</div>
<div class="bg-container-inset rounded-lg p-4">
<div class="flex items-center gap-2 mb-2">
<%= icon "hash", class: "w-5 h-5 text-secondary" %>
<p class="text-xs font-medium text-secondary uppercase"><%= t(".total_tokens") %></p>
</div>
<p class="text-2xl font-semibold text-primary"><%= number_with_delimiter(@statistics[:total_tokens]) %></p>
<p class="text-xs text-secondary mt-1">
<%= number_with_delimiter(@statistics[:total_prompt_tokens]) %> <%= t(".prompt") %> /
<%= number_with_delimiter(@statistics[:total_completion_tokens]) %> <%= t(".completion") %>
</p>
</div>
<div class="bg-container-inset rounded-lg p-4">
<div class="flex items-center gap-2 mb-2">
<%= icon "dollar-sign", class: "w-5 h-5 text-secondary" %>
<p class="text-xs font-medium text-secondary uppercase"><%= t(".total_cost") %></p>
</div>
<p class="text-2xl font-semibold text-primary">$<%= sprintf("%.2f", @statistics[:total_cost]) %></p>
</div>
<div class="bg-container-inset rounded-lg p-4">
<div class="flex items-center gap-2 mb-2">
<%= icon "trending-up", class: "w-5 h-5 text-secondary" %>
<p class="text-xs font-medium text-secondary uppercase"><%= t(".avg_cost_per_request") %></p>
</div>
<p class="text-2xl font-semibold text-primary">
$<%= sprintf("%.4f", @statistics[:avg_cost]) %>
</p>
<% if @statistics[:requests_with_cost] < @statistics[:total_requests] %>
<p class="text-xs text-secondary mt-1">
<%= t(".based_on_requests", with_cost: number_with_delimiter(@statistics[:requests_with_cost]), total: number_with_delimiter(@statistics[:total_requests])) %>
</p>
<% end %>
</div>
</div>
<!-- Cost by Operation -->
<% if @statistics[:by_operation].any? %>
<div class="mb-6">
<h2 class="text-lg font-semibold text-primary mb-3"><%= t(".cost_by_operation") %></h2>
<div class="bg-container-inset rounded-lg p-4">
<div class="space-y-2">
<% @statistics[:by_operation].each do |operation, cost| %>
<div class="flex justify-between items-center">
<span class="text-sm text-primary"><%= operation.humanize %></span>
<span class="text-sm font-medium text-primary">$<%= sprintf("%.4f", cost) %></span>
</div>
<% end %>
</div>
</div>
</div>
<% end %>
<!-- Cost by Model -->
<% if @statistics[:by_model].any? %>
<div class="mb-6">
<h2 class="text-lg font-semibold text-primary mb-3"><%= t(".cost_by_model") %></h2>
<div class="bg-container-inset rounded-lg p-4">
<div class="space-y-2">
<% @statistics[:by_model].each do |model, cost| %>
<div class="flex justify-between items-center">
<span class="text-sm text-primary font-mono"><%= model %></span>
<span class="text-sm font-medium text-primary">$<%= sprintf("%.4f", cost) %></span>
</div>
<% end %>
</div>
</div>
</div>
<% end %>
<!-- Recent Usage Table -->
<div>
<h2 class="text-lg font-semibold text-primary mb-3"><%= t(".recent_usage") %></h2>
<div class="bg-container-inset rounded-lg overflow-hidden">
<% if @llm_usages.any? %>
<table class="w-full">
<thead class="bg-surface-default border-b border-primary">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-secondary uppercase"><%= t(".col_date") %></th>
<th class="px-4 py-3 text-left text-xs font-medium text-secondary uppercase"><%= t(".col_operation") %></th>
<th class="px-4 py-3 text-left text-xs font-medium text-secondary uppercase"><%= t(".col_model") %></th>
<th class="px-4 py-3 text-right text-xs font-medium text-secondary uppercase"><%= t(".col_tokens") %></th>
<th class="px-4 py-3 text-right text-xs font-medium text-secondary uppercase"><%= t(".col_cost") %></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<% @llm_usages.each do |usage| %>
<tr class="<%= "bg-red-50 theme-dark:bg-red-950/30" if usage.failed? %>">
<td class="px-4 py-3 text-sm text-primary whitespace-nowrap">
<%= usage.created_at.strftime("%b %d, %Y %I:%M %p") %>
</td>
<td class="px-4 py-3 text-sm text-primary">
<%= usage.operation.humanize %>
</td>
<td class="px-4 py-3 text-sm text-primary font-mono">
<%= usage.model %>
</td>
<td class="px-4 py-3 text-sm text-primary text-right whitespace-nowrap">
<% if usage.failed? %>
<div data-controller="tooltip" class="inline-flex justify-end">
<div class="inline-flex items-center gap-1 cursor-help">
<%= icon "alert-circle", class: "w-4 h-4 text-red-600 theme-dark:text-red-400" %>
<span class="text-red-600 theme-dark:text-red-400 font-medium"><%= t(".failed") %></span>
</div>
<div role="tooltip" data-tooltip-target="tooltip" class="tooltip bg-inverse text-sm p-3 rounded w-72 text-left break-words whitespace-normal shadow-lg hidden">
<div class="text-inverse">
<% if usage.http_status_code.present? %>
<p class="text-xs mt-1 text-inverse opacity-70">HTTP Status: <%= usage.http_status_code %></p>
<% end %>
<p class="text-xs"><%= usage.error_message %></p>
</div>
</div>
</div>
<% else %>
<%= number_with_delimiter(usage.total_tokens) %>
<span class="text-xs text-secondary">
(<%= number_with_delimiter(usage.prompt_tokens) %>/<%= number_with_delimiter(usage.completion_tokens) %>)
</span>
<% end %>
</td>
<td class="px-4 py-3 text-sm font-medium text-primary text-right whitespace-nowrap">
<%= usage.formatted_cost %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<div class="p-8 text-center">
<p class="text-secondary"><%= t(".no_usage_data") %></p>
</div>
<% end %>
</div>
</div>
<div class="mt-6">
<%= render DS::Alert.new(
title: t(".cost_estimates_title"),
message: t(".cost_estimates_description"),
variant: :info
) %>
</div>
</div>