Skip to content

refactor(dashboard): polish guardrails admin forms#247

Merged
SantiagoDePolonia merged 6 commits intomainfrom
feat/guardrails-polishing
Apr 21, 2026
Merged

refactor(dashboard): polish guardrails admin forms#247
SantiagoDePolonia merged 6 commits intomainfrom
feat/guardrails-polishing

Conversation

@SantiagoDePolonia
Copy link
Copy Markdown
Contributor

@SantiagoDePolonia SantiagoDePolonia commented Apr 20, 2026

Summary

  • log admin/dashboard request failures with structured context
  • split the large dashboard template into page partials and remove the one-page Settings subnav
  • move admin forms into modal overlays, keep mobile close buttons inline, and surface HTTP form errors in the active form
  • move guardrail field help text into inline question-mark helpers

Tests

  • node --test internal/admin/dashboard/static/js/modules/dashboard-layout.test.js internal/admin/dashboard/static/js/modules/timezone-layout.test.js internal/admin/dashboard/static/js/modules/workflows-layout.test.js internal/admin/dashboard/static/js/modules/guardrails.test.js internal/admin/dashboard/static/js/modules/aliases.test.js internal/admin/dashboard/static/js/modules/auth-keys.test.js internal/admin/dashboard/static/js/modules/workflows.test.js
  • go test ./internal/admin/...

Summary by CodeRabbit

  • New Features

    • Added Audit Logs viewer and distinct Overview/Usage/Models/Workflows/Guardrails/Auth Keys pages, plus an inline-help toggle and modal editor shell for unified dialogs.
  • Bug Fixes

    • Improved error handling with structured admin logging and safer template/render-write error reporting.
    • Replaced scroll-into-view with robust deferred focus for modal editors and forms; refined toolbar and responsive layout behavior.
  • Tests

    • Expanded tests for layout, focus behavior, submissions, stale-auth handling, and error logging.
  • Style

    • Updated dashboard CSS for modal backdrops, editor shells, inputs, and responsive toolbar/grid.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 20, 2026

📝 Walkthrough

Walkthrough

Reorganized the admin dashboard into page-specific templates, replaced scroll-into-view editor behavior with deferred focus handlers, added structured slog logging for render/write/handled errors, updated modal/form CSS and JS routing to compute overlay state, and expanded tests to reflect these behaviors.

Changes

Cohort / File(s) Summary
Backend logging
internal/admin/handler.go, internal/admin/handler_test.go, internal/admin/dashboard/dashboard.go
Added structured log/slog logging for handled admin errors and template/render/write failures; request ID included; tests assert warning vs error levels and JSON log fields.
Template split & pages
internal/admin/dashboard/templates/index.html, internal/admin/dashboard/templates/layout.html, internal/admin/dashboard/templates/page-*.html, internal/admin/dashboard/templates/inline-help-toggle.html
Replaced monolithic index with multiple dashboard-page-* templates, added inline-help-toggle, updated layout.html to bind dashboard-modal-open to overlay state.
New audit UI
internal/admin/dashboard/templates/page-audit-logs.html
Added dashboard-page-audit-logs with audit list, expandable entries, pagination, and an interactions/conversation drawer.
Models / Guardrails / Workflows pages
internal/admin/dashboard/templates/page-models.html, .../page-guardrails.html, .../page-workflows.html, .../page-usage.html, .../page-overview.html, .../page-auth-keys.html, .../page-settings.html
Added dedicated page templates for models, guardrails, workflows, usage, overview, auth-keys, settings with modal editor shells and page-specific bindings.
CSS: modal & form renames
internal/admin/dashboard/static/css/dashboard.css
Introduced modal backdrop/shell styles, body.dashboard-modal-open scroll-lock, renamed editor/form hooks, generalized input/textarea rules, added .form-error, and rewrote toolbar/grid responsive rules.
JS: focus-first editor behavior & fetch handling
internal/admin/dashboard/static/js/modules/aliases.js, .../guardrails.js, .../workflows.js, .../auth-keys.js
Removed scrollIntoView helpers; added focusEditorField/focusGuardrailForm/focusWorkflowForm deferred focus helpers; added optional requestOptions + handleFetchResponse hooks and console.error logging on non-OK responses.
Dashboard routing/state
internal/admin/dashboard/static/js/dashboard.js
Removed settings-subpage routing and navigateSettings; added overlayDialogOpen() to compute modal/dialog-open state from per-page flags.
Tests: layout, focus & error handling
internal/admin/dashboard/static/js/modules/*.test.js, internal/admin/dashboard/static/js/modules/*layout.test.js, internal/admin/dashboard/static/js/modules/*workflow*.test.js, internal/admin/dashboard/static/js/modules/*guardrails.test.js, internal/admin/dashboard/static/js/modules/auth-keys.test.js, internal/admin/dashboard/static/js/modules/aliases.test.js
Updated many tests to expect focus calls (deferred) instead of scrolls, added tests asserting console.error on failed fetches, normalized template fixture helpers (e.g., readDashboardTemplateSource()), and expanded layout/template assertions to match new page templates.
Removed helper template
internal/admin/dashboard/templates/helper-disclosure.html
Deleted helper-disclosure template; replaced by inline-help-toggle across pages.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

release:internal

Poem

🐰
I hopped through templates, rugs unrolled,
Focus found fields where scrolls once rolled.
Modals dim the busy night,
Logs now glow with steadier light.
A gentle hop — the dashboard's whole.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: refactoring and polishing guardrails admin forms within the dashboard, which aligns with the primary objectives of moving forms into modals and improving form UX.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/guardrails-polishing

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 20, 2026

Greptile Summary

This PR polishes the guardrails admin UI as part of a broader dashboard refactor: admin forms are lifted out of inline sections and into modal overlays, HTTP error messages from the server are now surfaced inside the active form, a new shared inline-help-toggle partial replaces the removed helper-disclosure.html, and the large index template is split into per-page partials. On the Go side, logHandledAdminError adds structured context (method, path, request ID, provider, code) to every admin request failure log.

Key changes:

  • handler.go — new logHandledAdminError + requestIDFromAdminContextOrHeader helpers provide consistent, structured slog output for all admin 4xx/5xx responses.
  • guardrails.js — adds openGuardrailCreate, openGuardrailEdit, closeGuardrailForm, focusGuardrailForm, and guardrailResponseMessage; the submit/delete paths now surface API error messages directly inside the modal form.
  • page-guardrails.html — new backdrop-dismiss modal with Escape key handler, form-level error display (role="alert" aria-live="assertive"), and inline ? help toggles for user-path and all dynamic type fields.
  • inline-help-toggle.html — new shared partial (replaces helper-disclosure.html); correctly used in all call sites with required x-data scope (open, copyId, showLabel, hideLabel, text).
  • dashboard.jsoverlayDialogOpen() updated to include guardrailFormOpen and authKeyFormOpen so the dashboard-modal-open body class is applied correctly for scroll-lock.
  • Two issues noted: (1) autofocus selector in focusGuardrailForm matches disabled elements, so focus is silently lost when opening the edit form; (2) guardrail write operations bypass the requestOptions/stale-auth detection used by the same module's read operations (consistent with auth-keys.js, but diverges from the more defensive aliases.js pattern).

Confidence Score: 4/5

Safe to merge — all critical paths work correctly; the two flagged issues are non-blocking UX and defensive-programming concerns.

The refactoring is well-structured: Go-side logging is clean, template splitting is correct, the modal pattern is consistent across guardrails and auth-keys pages, and the helper-disclosure rename is fully applied. The two P2 issues (autofocus no-op on disabled input in edit mode; write ops not using stale-auth guard) don't break functionality — the edit form is still usable and 401s are handled explicitly. No security or data-loss concerns.

guardrails.js — autofocus selector and stale-auth handling in write operations.

Important Files Changed

Filename Overview
internal/admin/handler.go Adds structured error logging (logHandledAdminError) with method, path, request_id context and a requestIDFromAdminContextOrHeader helper; logic is clean and no issues found.
internal/admin/dashboard/static/js/modules/guardrails.js New modal form (open/close/focus), inline error surfacing, and guardrailResponseMessage helper; two issues: autofocus no-ops on disabled name input in edit mode, and write operations bypass the stale-auth detection present in read operations.
internal/admin/dashboard/templates/page-guardrails.html New modal overlay form with backdrop-dismiss, keyboard Escape handling, inline help toggles for user-path and dynamic fields, and form-level error display; data-modal-autofocus on the (potentially disabled) name input causes the autofocus bug noted in guardrails.js.
internal/admin/dashboard/templates/inline-help-toggle.html New shared partial replacing the removed helper-disclosure.html; renders a ? toggle button with proper ARIA attributes; all call sites supply the required open, copyId, showLabel, and hideLabel variables in their enclosing x-data.
internal/admin/dashboard/static/js/modules/auth-keys.js Adds modal open/close helpers and moves form into a modal overlay; write ops use this.headers() directly (consistent pre-existing pattern); no new issues introduced.
internal/admin/dashboard/templates/page-auth-keys.html Auth key creation form moved to modal overlay; inline help toggle for user-path field has correct x-data context; no issues found.
internal/admin/dashboard/static/js/modules/aliases.js Adds focusEditorField helper and openProviderOverrideEdit convenience wrapper; no new issues; write operations consistently use adminRequestOptions + handleFetchResponse.
internal/admin/dashboard/dashboard.go No functional changes; template glob pattern unchanged and correctly picks up the new page partial files.
internal/admin/handler_test.go Test scaffolding only; no changes to existing tests; provides mocks for the Go-side changes.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User clicks Create / Edit Guardrail] --> B{guardrailFormMode}
    B -- create --> C[openGuardrailCreate\nresets form + sets defaults]
    B -- edit --> D[openGuardrailEdit\nloads existing data]
    C --> E[guardrailFormOpen = true]
    D --> E
    E --> F[focusGuardrailForm\nquerySelector autofocus field]
    F --> G[Modal overlay rendered\neditor-modal-shell]
    G --> H{User action}
    H -- types + submits --> I[submitGuardrailForm\nPUT /admin/api/v1/guardrails/:name]
    H -- clicks backdrop or Escape --> J[closeGuardrailForm]
    H -- clicks Cancel --> J
    I --> K{HTTP status}
    K -- 200 --> L[fetchGuardrails\nguardrailNotice set\ncloseGuardrailForm]
    K -- 400/422 --> M[guardrailResponseMessage\nreads error.message\nguardrailError shown in form]
    K -- 401 --> N[authError = true\nneedsAuth = true]
    K -- 503 --> O[guardrailsAvailable = false]
    H -- clicks Delete --> P[deleteGuardrail\nDELETE /admin/api/v1/guardrails/:name]
    P --> Q{HTTP status}
    Q -- 204 --> R[fetchGuardrails\nguardrailNotice set]
    Q -- 400/404 --> S[guardrailResponseMessage\nguardrailError shown outside form]
Loading

Comments Outside Diff (1)

  1. internal/admin/dashboard/static/js/modules/guardrails.js, line 377-412 (link)

    P2 Write operations bypass stale-auth detection

    fetchGuardrailTypes() and fetchGuardrails() both use requestOptions() (which stamps authGeneration onto the request) and then call isStaleAuthFetchResult to prevent stale 401 responses from re-triggering the auth error flow. submitGuardrailForm() and deleteGuardrail() skip this entirely and call this.headers() directly, then hard-code:

    if (res.status === 401) {
        this.authError = true;
        this.needsAuth = true;
        ...
    }

    If a user changes their API key via the auth dialog while a guardrail save/delete is still in-flight, the old 401 response will incorrectly flip authError and needsAuth back to true, even though the user just successfully re-authenticated. The same inconsistency exists in auth-keys.js write operations; aliases.js handles this correctly by routing all mutations through handleFetchResponse + isStaleAuthFetchResult.

    Consider aligning with the aliases.js pattern so the stale-auth guard covers writes as well.

Reviews (1): Last reviewed commit: "fix(dashboard): focus modal forms and lo..." | Re-trigger Greptile

Comment on lines +196 to +222
const focus = () => {
const refs = this.$refs || {};
const editor = refs.guardrailEditor || null;
if (!editor || typeof editor.querySelector !== 'function') {
return;
}
const field = editor.querySelector('[data-modal-autofocus], input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled]), button:not([disabled])');
if (!field || typeof field.focus !== 'function') {
return;
}
field.focus({ preventScroll: true });
};

const focusAfterPaint = () => {
if (typeof global.requestAnimationFrame === 'function') {
global.requestAnimationFrame(focus);
return;
}
focus();
};

if (typeof this.$nextTick === 'function') {
this.$nextTick(focusAfterPaint);
return;
}
focusAfterPaint();
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Autofocus fails silently in edit mode

focusGuardrailForm uses the selector [data-modal-autofocus] to find the first focusable field. In edit mode the name <input> is disabled but still carries the data-modal-autofocus attribute (see page-guardrails.html line 74), so querySelector('[data-modal-autofocus], ...]') returns the disabled element first — and focus() on a disabled element is a browser no-op. The user opens an edit form and no field receives focus, forcing them to click or Tab before they can start typing.

To fix this, exclude disabled elements from the [data-modal-autofocus] match:

Suggested change
const focus = () => {
const refs = this.$refs || {};
const editor = refs.guardrailEditor || null;
if (!editor || typeof editor.querySelector !== 'function') {
return;
}
const field = editor.querySelector('[data-modal-autofocus], input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled]), button:not([disabled])');
if (!field || typeof field.focus !== 'function') {
return;
}
field.focus({ preventScroll: true });
};
const focusAfterPaint = () => {
if (typeof global.requestAnimationFrame === 'function') {
global.requestAnimationFrame(focus);
return;
}
focus();
};
if (typeof this.$nextTick === 'function') {
this.$nextTick(focusAfterPaint);
return;
}
focusAfterPaint();
},
const field = editor.querySelector('[data-modal-autofocus]:not([disabled]), input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled]), button:not([disabled])');

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/admin/dashboard/static/css/dashboard.css (1)

295-332: 🧹 Nitpick | 🔵 Trivial

Duplicate rules: fold editor-modal into auth-dialog shells.

.editor-modal-backdrop (295–300) is byte-for-byte identical to .auth-dialog-backdrop (288–293), and .editor-modal-shell (311–319) duplicates .auth-dialog-shell (302–309) apart from overflow-y: auto. The mobile override at 3565–3568 likewise repeats 3561–3564. Combine the selectors to prevent drift between the two modal families:

♻️ Proposed consolidation
-.auth-dialog-backdrop {
-  position: fixed;
-  inset: 0;
-  background: rgba(0, 0, 0, 0.48);
-  z-index: 80;
-}
-
-.editor-modal-backdrop {
+.auth-dialog-backdrop,
+.editor-modal-backdrop {
   position: fixed;
   inset: 0;
   background: rgba(0, 0, 0, 0.48);
   z-index: 80;
 }

-.auth-dialog-shell {
+.auth-dialog-shell,
+.editor-modal-shell {
   position: fixed;
   inset: 0;
   z-index: 90;
   display: grid;
   place-items: center;
   padding: 20px;
 }

-.editor-modal-shell {
-  position: fixed;
-  inset: 0;
-  z-index: 90;
-  display: grid;
-  place-items: center;
-  padding: 20px;
+.editor-modal-shell {
   overflow-y: auto;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/static/css/dashboard.css` around lines 295 - 332,
The CSS duplicates for modal backdrops and shells should be consolidated:
replace the two identical backdrop rules (.editor-modal-backdrop and
.auth-dialog-backdrop) with a single combined selector (.editor-modal-backdrop,
.auth-dialog-backdrop) and collapse the shared shell properties into one
combined selector (.editor-modal-shell, .auth-dialog-shell) keeping only the
common declarations; then add the editor-specific overflow override separately
(e.g., .editor-modal-shell { ...common... } and .editor-modal-shell {
overflow-y: auto; } or a combined selector with a later rule for
.editor-modal-shell) and do the same consolidation for the mobile override rules
to avoid repeating the same declarations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/admin/dashboard/static/css/dashboard.css`:
- Around line 1688-1697: The CSS uses an overly generic selector set (.form,
.form-kicker, .form-hint etc.) that risks collisions; rename these selectors to
a more specific editor-scoped prefix (e.g., change .form → .editor-form and
likewise .form-kicker → .editor-form-kicker, .form-hint → .editor-form-hint) and
update all related variants mentioned (e.g., .form-grid, .form-field,
.form-error, .form-actions) to the same .editor-form-* pattern to match existing
.editor-modal-* / .editor-header naming; ensure you update every occurrence in
the stylesheet so class names remain consistent with the editor modal
components.
- Around line 1359-1373: The bare selector input:is([type="text"],
[type="date"], [type="number"]) is applying width:100% and other form styles
globally and also omits other common input types; restrict the rule to the
intended scope (for example replace the bare selector with a scoped one like
.form input:is([type="text"],[type="date"],[type="number"]) or
.editor-modal-shell input:is(...) so it only affects inputs inside form/editor
containers) and/or expand the type list to include
email/password/search/url/tel/time if a broader default look is desired; update
the matching focus rule input:is(...):focus the same way to keep styles
consistent.

In `@internal/admin/dashboard/static/js/modules/aliases.js`:
- Around line 478-489: In focusEditorField(refName) the single querySelector
with multiple comma-separated selectors can match the close button before the
intended [data-modal-autofocus] field; change the logic to first attempt a
focused element lookup using editor.querySelector('[data-modal-autofocus]') and,
only if that returns null, perform a second query with the fallback selector
(input:not([type="hidden"]):not([disabled]), textarea:not([disabled]),
select:not([disabled]), button:not([disabled]')); keep the existing guards
(editor existence and typeof field.focus === 'function') and call field.focus({
preventScroll: true }) on the chosen element so the modal reliably focuses the
intended field.

In `@internal/admin/dashboard/static/js/modules/guardrails.js`:
- Around line 195-222: The current focusGuardrailForm() uses a single
querySelector list that lets the close button match before the intended field;
update the logic in focusGuardrailForm() so it first attempts
editor.querySelector('[data-modal-autofocus]') and, if that returns null or is
not focusable, then falls back to
editor.querySelector('input:not([type="hidden"]):not([disabled]),
textarea:not([disabled]), select:not([disabled])') — do not include generic
button selectors in the fallback. Keep the existing focus(), focusAfterPaint(),
and $nextTick handling but change the selector checks and focusability tests
accordingly; apply the same selector-change fix to the analogous functions in
workflows.js and aliases.js (the functions that mirror focusGuardrailForm's
behavior).

In `@internal/admin/dashboard/static/js/modules/workflows.js`:
- Around line 483-508: The focusWorkflowForm method duplicates shared logic
already implemented in focusEditorField(refName); replace the body of
focusWorkflowForm by delegating to the shared helper (call
this.focusEditorField('workflowEditor')) and remove any redundant local
implementation; do the same for other duplicated helpers like focusGuardrailForm
and focusAliasForm so they simply call focusEditorField with the appropriate ref
name, ensuring existing usage of this.$nextTick/requestAnimationFrame continues
to be handled by the centralized focusEditorField implementation.

In `@internal/admin/dashboard/templates/page-auth-keys.html`:
- Around line 47-49: This close button embeds the raw "✕" glyph instead of using
the shared template; replace the glyph inside the button in the template
fragment that contains the alias-close-btn and the `@click`="closeAuthKeyForm()"
with the shared Handlebars template invocation {{template "x-icon"}} so the
button uses the same icon SVG/styling as other modals (referenced elsewhere in
page-workflows.html and page-guardrails.html) and preserves sizing, stroke
weight and theming consistency.

In `@internal/admin/dashboard/templates/page-guardrails.html`:
- Around line 58-70: The guardrail editor uses a <div class="form"> which
prevents native Enter-to-submit and browser validation; change that container to
a real <form> and attach the submit handler via
`@submit.prevent`="submitGuardrailForm()" (keep the submit button as type="submit"
and remove its `@click`), ensure the close button (closeGuardrailForm) remains
type="button" so it doesn't trigger submit, and leave existing bindings like
guardrailFormMode and guardrailError unchanged so validation messages and header
logic continue to work.
- Around line 72-84: The modal autofocus currently targets the name input via
the data-modal-autofocus attribute while the input is disabled when
guardrailFormMode === 'edit', so move or make the autofocus conditional: either
place data-modal-autofocus on a non-disabled field such as the
guardrail-description or the first type-specific field, or change the attribute
to be conditional (e.g., bind it based on guardrailFormMode) so focusEditorField
doesn't call .focus() on a disabled element; ensure the same change is applied
to the guardrail-type <select> (which uses x-ref="guardrailTypeSelect" and is
tied to guardrailForm.type, guardrailTypes, syncGuardrailTypeSelectValue, and
onGuardrailTypeChange) if it could be targeted for autofocus.

In `@internal/admin/dashboard/templates/page-overview.html`:
- Around line 8-13: The interval-picker buttons (elements with class
"interval-btn" that call setInterval('daily'|'weekly'|'monthly'|'yearly') and
bind to interval) lack an explicit type attribute; add type="button" to each of
those four <button> elements so they don't default to "submit" if moved into a
form, matching the rest of the template pattern and avoiding future regressions.

In `@internal/admin/dashboard/templates/page-settings.html`:
- Around line 24-33: Add the missing aria-describedby attributes to wire the
inline help copy to the controls: update the select with id
"timezone-override-select" (x-ref "timezoneOverrideSelect", x-model
"timezoneOverride", using timezoneOptions and syncTimezoneOverrideSelectValue)
to include aria-describedby="timezone-help-copy", and likewise add
aria-describedby="runtime-refresh-help-copy" to the runtime-refresh button
referenced near the runtime refresh logic so screen readers announce the
corresponding help regions.

In `@internal/admin/dashboard/templates/page-usage.html`:
- Around line 131-156: The search input bound to usageLogSearch in the template
currently only uses a placeholder (input with x-model="usageLogSearch") which is
not an accessible name; add an explicit aria-label (e.g., aria-label="Search by
request ID, model, provider") to that input to match the pattern used by the
usageLogUserPath control and ensure screen readers can identify the field,
keeping the existing x-model, `@input.debounce.300ms`="fetchUsageLog(true)", and
class attributes unchanged.

In `@internal/admin/handler_test.go`:
- Around line 1934-2010: Add a new unit test that exercises the X-Request-ID
fallback by calling handleError with an HTTP request that does NOT have a
context request ID but DOES have the "X-Request-ID" header set; mirror the
existing
TestHandleError_LogsClientErrorsAtWarnLevel/TestHandleError_LogsServerErrorsAtErrorLevel
pattern (create buf, set slog.Default to JSON handler, build echo.Context via
e.NewContext), set req.Header.Set("X-Request-ID","admin-header-req-789"), call
handleError (e.g., pass core.NewInvalidRequestError or core.NewProviderError),
assert response code and verify the JSON log contains the header value
`"request_id":"admin-header-req-789"` along with the expected
level/msg/path/provider/message fields.

---

Outside diff comments:
In `@internal/admin/dashboard/static/css/dashboard.css`:
- Around line 295-332: The CSS duplicates for modal backdrops and shells should
be consolidated: replace the two identical backdrop rules
(.editor-modal-backdrop and .auth-dialog-backdrop) with a single combined
selector (.editor-modal-backdrop, .auth-dialog-backdrop) and collapse the shared
shell properties into one combined selector (.editor-modal-shell,
.auth-dialog-shell) keeping only the common declarations; then add the
editor-specific overflow override separately (e.g., .editor-modal-shell {
...common... } and .editor-modal-shell { overflow-y: auto; } or a combined
selector with a later rule for .editor-modal-shell) and do the same
consolidation for the mobile override rules to avoid repeating the same
declarations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e203e7cf-5f81-4541-9368-e3f5903c0b21

📥 Commits

Reviewing files that changed from the base of the PR and between 44256f7 and 14cf2da.

📒 Files selected for processing (28)
  • internal/admin/dashboard/dashboard.go
  • internal/admin/dashboard/static/css/dashboard.css
  • internal/admin/dashboard/static/js/dashboard.js
  • internal/admin/dashboard/static/js/modules/aliases.js
  • internal/admin/dashboard/static/js/modules/aliases.test.js
  • internal/admin/dashboard/static/js/modules/auth-keys.js
  • internal/admin/dashboard/static/js/modules/auth-keys.test.js
  • internal/admin/dashboard/static/js/modules/dashboard-layout.test.js
  • internal/admin/dashboard/static/js/modules/guardrails.js
  • internal/admin/dashboard/static/js/modules/guardrails.test.js
  • internal/admin/dashboard/static/js/modules/timezone-layout.test.js
  • internal/admin/dashboard/static/js/modules/workflows-layout.test.js
  • internal/admin/dashboard/static/js/modules/workflows.js
  • internal/admin/dashboard/static/js/modules/workflows.test.js
  • internal/admin/dashboard/templates/helper-disclosure.html
  • internal/admin/dashboard/templates/index.html
  • internal/admin/dashboard/templates/inline-help-toggle.html
  • internal/admin/dashboard/templates/layout.html
  • internal/admin/dashboard/templates/page-audit-logs.html
  • internal/admin/dashboard/templates/page-auth-keys.html
  • internal/admin/dashboard/templates/page-guardrails.html
  • internal/admin/dashboard/templates/page-models.html
  • internal/admin/dashboard/templates/page-overview.html
  • internal/admin/dashboard/templates/page-settings.html
  • internal/admin/dashboard/templates/page-usage.html
  • internal/admin/dashboard/templates/page-workflows.html
  • internal/admin/handler.go
  • internal/admin/handler_test.go
💤 Files with no reviewable changes (1)
  • internal/admin/dashboard/templates/helper-disclosure.html

Comment on lines +1359 to +1373
input:is([type="text"], [type="date"], [type="number"]) {
width: 100%;
padding: 8px 12px;
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
font-size: 13px;
font-family: inherit;
outline: none;
}

input:is([type="text"], [type="date"], [type="number"]):focus {
border-color: var(--accent);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Bare input type selector is too broad and narrow at the same time.

Switching from a class-scoped rule to the bare selector input:is([type="text"], [type="date"], [type="number"]) forces width: 100% (plus padding/border/background) on every matching input on the page — including any inline/filter/date-picker inputs that previously relied on intrinsic sizing or other class rules. This can silently regress layouts outside the editor modals.

It is also inconsistent across input types: email, password, search, url, tel, and time inputs (which are likely to appear in admin forms) won't pick up the shared look, producing visual drift.

Consider scoping this to form containers (e.g., .form input[type=...], .editor-modal-shell input[type=...]) or widening the type list, rather than applying a body-wide default.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/static/css/dashboard.css` around lines 1359 - 1373,
The bare selector input:is([type="text"], [type="date"], [type="number"]) is
applying width:100% and other form styles globally and also omits other common
input types; restrict the rule to the intended scope (for example replace the
bare selector with a scoped one like .form
input:is([type="text"],[type="date"],[type="number"]) or .editor-modal-shell
input:is(...) so it only affects inputs inside form/editor containers) and/or
expand the type list to include email/password/search/url/tel/time if a broader
default look is desired; update the matching focus rule input:is(...):focus the
same way to keep styles consistent.

Comment on lines +1688 to 1697
.form h3 {
font-size: 20px;
font-weight: 700;
}

.alias-form-kicker,
.alias-form-hint {
.form-kicker,
.form-hint {
color: var(--text-muted);
font-size: 13px;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

.form is an overly generic class name.

A top-level .form selector (and .form-grid, .form-field, .form-error, .form-actions, …) is extremely likely to collide with future markup or third-party snippets embedded in the dashboard. Given these rules are tightly coupled to the editor modals, consider a more specific prefix such as .editor-form / .editor-form-grid / .editor-form-error to match the new .editor-modal-* / .editor-header naming introduced in this PR.

Also applies to: 1784-1788

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/static/css/dashboard.css` around lines 1688 - 1697,
The CSS uses an overly generic selector set (.form, .form-kicker, .form-hint
etc.) that risks collisions; rename these selectors to a more specific
editor-scoped prefix (e.g., change .form → .editor-form and likewise
.form-kicker → .editor-form-kicker, .form-hint → .editor-form-hint) and update
all related variants mentioned (e.g., .form-grid, .form-field, .form-error,
.form-actions) to the same .editor-form-* pattern to match existing
.editor-modal-* / .editor-header naming; ensure you update every occurrence in
the stylesheet so class names remain consistent with the editor modal
components.

Comment on lines +478 to +489
focusEditorField(refName) {
const focus = () => {
const refs = this.$refs || {};
const editor = refs[refName] || null;
if (!editor || typeof editor.querySelector !== 'function') {
return;
}
const field = editor.querySelector('[data-modal-autofocus], input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled]), button:not([disabled])');
if (!field || typeof field.focus !== 'function') {
return;
}
field.focus({ preventScroll: true });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

MDN querySelector selector list first element document order

💡 Result:

Yes, document.querySelector with a selector list (comma-separated selectors, e.g., "p, div, pre") returns the first matching element in document order. It performs a depth-first pre-order traversal starting from the first element in the document's markup, returning the first match found across all selectors, regardless of selector order in the list.

Citations:


🏁 Script executed:

# Check if the file exists and get the line count
wc -l internal/admin/dashboard/static/js/modules/aliases.js

Repository: ENTERPILOT/GoModel

Length of output: 120


🏁 Script executed:

# Read the specific lines mentioned in the review (478-489)
sed -n '475,495p' internal/admin/dashboard/static/js/modules/aliases.js

Repository: ENTERPILOT/GoModel

Length of output: 1075


🏁 Script executed:

# Search for calls to focusEditorField to understand the impact
rg "focusEditorField" internal/admin/dashboard/static/js/modules/aliases.js -A 2 -B 2

Repository: ENTERPILOT/GoModel

Length of output: 811


🏁 Script executed:

# Search for data-modal-autofocus in templates to see if it exists
fd -e vue -e html -e js | xargs rg "data-modal-autofocus" || echo "No data-modal-autofocus found"

Repository: ENTERPILOT/GoModel

Length of output: 2114


🏁 Script executed:

# Find the template that contains aliasEditor ref
rg "x-ref=\"aliasEditor\"" internal/admin/dashboard/templates/ -B 5 -A 15

Repository: ENTERPILOT/GoModel

Length of output: 2364


🏁 Script executed:

# Find the template that contains modelOverrideEditor ref
rg "x-ref=\"modelOverrideEditor\"" internal/admin/dashboard/templates/ -B 5 -A 15

Repository: ENTERPILOT/GoModel

Length of output: 2495


Split autofocus and fallback lookup so the modal focuses the intended field.

The current implementation on line 485 does not prioritize [data-modal-autofocus] fields. A selector list in querySelector returns the first match in document order regardless of selector position. In the actual templates, the close button appears before the data-modal-autofocus input, so querySelector('[data-modal-autofocus], input:not([type="hidden"]):not([disabled]), ..., button:not([disabled])') will match and focus the close button instead of the form field, breaking keyboard navigation and accessibility.

Separate the autofocus lookup from fallback selectors to ensure the intended field receives focus.

♿ Proposed fix
             focusEditorField(refName) {
                 const focus = () => {
                     const refs = this.$refs || {};
                     const editor = refs[refName] || null;
                     if (!editor || typeof editor.querySelector !== 'function') {
                         return;
                     }
-                    const field = editor.querySelector('[data-modal-autofocus], input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled]), button:not([disabled])');
+                    const isFocusable = (candidate) => {
+                        if (!candidate || candidate.disabled || candidate.hidden) {
+                            return false;
+                        }
+                        if (candidate.getAttribute && candidate.getAttribute('aria-hidden') === 'true') {
+                            return false;
+                        }
+                        if (candidate.type === 'hidden') {
+                            return false;
+                        }
+                        if (typeof global.getComputedStyle === 'function') {
+                            const style = global.getComputedStyle(candidate);
+                            if (style.display === 'none' || style.visibility === 'hidden') {
+                                return false;
+                            }
+                        }
+                        return typeof candidate.focus === 'function';
+                    };
+                    const firstFocusable = (selector) => Array.from(editor.querySelectorAll(selector)).find(isFocusable);
+                    const field = firstFocusable('[data-modal-autofocus]')
+                        || firstFocusable('input:not([type="hidden"]), textarea, select, button');
                     if (!field || typeof field.focus !== 'function') {
                         return;
                     }
                     field.focus({ preventScroll: true });
                 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/static/js/modules/aliases.js` around lines 478 -
489, In focusEditorField(refName) the single querySelector with multiple
comma-separated selectors can match the close button before the intended
[data-modal-autofocus] field; change the logic to first attempt a focused
element lookup using editor.querySelector('[data-modal-autofocus]') and, only if
that returns null, perform a second query with the fallback selector
(input:not([type="hidden"]):not([disabled]), textarea:not([disabled]),
select:not([disabled]), button:not([disabled]')); keep the existing guards
(editor existence and typeof field.focus === 'function') and call field.focus({
preventScroll: true }) on the chosen element so the modal reliably focuses the
intended field.

Comment on lines +195 to +222
focusGuardrailForm() {
const focus = () => {
const refs = this.$refs || {};
const editor = refs.guardrailEditor || null;
if (!editor || typeof editor.querySelector !== 'function') {
return;
}
const field = editor.querySelector('[data-modal-autofocus], input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled]), button:not([disabled])');
if (!field || typeof field.focus !== 'function') {
return;
}
field.focus({ preventScroll: true });
};

const focusAfterPaint = () => {
if (typeof global.requestAnimationFrame === 'function') {
global.requestAnimationFrame(focus);
return;
}
focus();
};

if (typeof this.$nextTick === 'function') {
this.$nextTick(focusAfterPaint);
return;
}
focusAfterPaint();
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In the DOM, when Element.querySelector is called with a selector list, is the returned element selected by document order across all selectors, or by which selector in the list matches first?

💡 Result:

The returned element is selected by document order across all selectors, not by which selector in the list matches first. Element.querySelector (and Document.querySelector) performs a depth-first pre-order traversal of the DOM tree, starting from the first element in the document's markup. During this traversal, it checks each element against the entire selector list (comma-separated selectors treated as a group). The first element encountered that matches any selector in the list is returned. The order of selectors in the string does not affect the result; reversing the selector order yields the same element if the same matches exist in the DOM.

Citations:


🏁 Script executed:

# Find the guardrails template file
fd -e html -e vue . internal/admin/dashboard/static | grep -i guardrail

Repository: ENTERPILOT/GoModel

Length of output: 44


🏁 Script executed:

# Find aliases.js and workflows.js to check for similar patterns
fd -e js . internal/admin/dashboard/static | grep -E "(aliases|workflows)\.js"

Repository: ENTERPILOT/GoModel

Length of output: 171


🏁 Script executed:

# Search for the focusGuardrailForm method and related template structure
rg "data-modal-autofocus|guardrailEditor|focusGuardrailForm" internal/admin/dashboard/static --type js --type html

Repository: ENTERPILOT/GoModel

Length of output: 1379


🏁 Script executed:

# Find the page-guardrails.html template file
fd -e html . internal/admin/dashboard | grep -i guardrail

Repository: ENTERPILOT/GoModel

Length of output: 117


🏁 Script executed:

# Search for template files more broadly
fd -e html . internal/admin/dashboard/templates

Repository: ENTERPILOT/GoModel

Length of output: 1159


🏁 Script executed:

# Look for guardrails template file with different naming patterns
find internal/admin/dashboard -name "*guardrail*" -type f

Repository: ENTERPILOT/GoModel

Length of output: 236


🏁 Script executed:

# Inspect the guardrails template to see editor structure and button/input order
cat internal/admin/dashboard/templates/page-guardrails.html

Repository: ENTERPILOT/GoModel

Length of output: 15946


🏁 Script executed:

# Search for the focus functions in aliases and workflows modules
rg "focusAliasForm|focusWorkflowForm" internal/admin/dashboard/static/js/modules/ -A 20

Repository: ENTERPILOT/GoModel

Length of output: 12310


🏁 Script executed:

# Check the templates for aliases and workflows
cat internal/admin/dashboard/templates/page-workflows.html | grep -A 30 "editor-header"

Repository: ENTERPILOT/GoModel

Length of output: 2383


🏁 Script executed:

# Check aliases template
cat internal/admin/dashboard/templates/page-aliases.html 2>/dev/null | grep -A 30 "editor-header" || echo "File may not exist or pattern not found"

Repository: ENTERPILOT/GoModel

Length of output: 101


🏁 Script executed:

# Find aliases template file
fd . internal/admin/dashboard/templates | grep -i alias

Repository: ENTERPILOT/GoModel

Length of output: 44


🏁 Script executed:

# Search for focusAliasForm in aliases.js
rg "focusAliasForm" internal/admin/dashboard/static/js/modules/aliases.js -A 20

Repository: ENTERPILOT/GoModel

Length of output: 44


🏁 Script executed:

# Search for alias editors and templates more broadly
fd . internal/admin/dashboard/templates | head -20

Repository: ENTERPILOT/GoModel

Length of output: 1112


🏁 Script executed:

# Search for the focus function calls and patterns in aliases.js
rg "querySelector.*data-modal-autofocus" internal/admin/dashboard/static/js/modules/aliases.js -B 5 -A 5

Repository: ENTERPILOT/GoModel

Length of output: 707


🏁 Script executed:

# Check what template the aliases.js module is using
rg "refName|Editor" internal/admin/dashboard/static/js/modules/aliases.js | head -20

Repository: ENTERPILOT/GoModel

Length of output: 391


🏁 Script executed:

# Look for page-models.html to see if it has similar editor structure
grep -A 30 "editor-header" internal/admin/dashboard/templates/page-models.html | head -40

Repository: ENTERPILOT/GoModel

Length of output: 2377


Autofocus lands on the close button, not the first form field.

Element.querySelector() with a selector list returns the first element in document order that matches any selector in the list — not the first selector that has a match. In page-guardrails.html, the editor-header close <button> (not disabled) is rendered before the name input that carries data-modal-autofocus, so button:not([disabled]) wins and the close button receives focus on both create and edit opens. In edit mode this is worse: the name input is :disabled="guardrailFormMode === 'edit'", so it wouldn't be focusable anyway, and the close button is still the first match.

Resolve the intent explicitly — try the marker first, then fall back to form controls without including button:

🛠️ Proposed fix
             focusGuardrailForm() {
                 const focus = () => {
                     const refs = this.$refs || {};
                     const editor = refs.guardrailEditor || null;
                     if (!editor || typeof editor.querySelector !== 'function') {
                         return;
                     }
-                    const field = editor.querySelector('[data-modal-autofocus], input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled]), button:not([disabled])');
+                    const field = editor.querySelector('[data-modal-autofocus]:not([disabled])')
+                        || editor.querySelector('input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled])');
                     if (!field || typeof field.focus !== 'function') {
                         return;
                     }
                     field.focus({ preventScroll: true });
                 };

The same pattern exists in workflows.js and aliases.js — their templates also place close buttons before form fields with data-modal-autofocus, causing the same bug. Tests only stub querySelector to return a focus spy, so they don't catch which element was actually selected.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
focusGuardrailForm() {
const focus = () => {
const refs = this.$refs || {};
const editor = refs.guardrailEditor || null;
if (!editor || typeof editor.querySelector !== 'function') {
return;
}
const field = editor.querySelector('[data-modal-autofocus], input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled]), button:not([disabled])');
if (!field || typeof field.focus !== 'function') {
return;
}
field.focus({ preventScroll: true });
};
const focusAfterPaint = () => {
if (typeof global.requestAnimationFrame === 'function') {
global.requestAnimationFrame(focus);
return;
}
focus();
};
if (typeof this.$nextTick === 'function') {
this.$nextTick(focusAfterPaint);
return;
}
focusAfterPaint();
},
focusGuardrailForm() {
const focus = () => {
const refs = this.$refs || {};
const editor = refs.guardrailEditor || null;
if (!editor || typeof editor.querySelector !== 'function') {
return;
}
const field = editor.querySelector('[data-modal-autofocus]:not([disabled])')
|| editor.querySelector('input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled])');
if (!field || typeof field.focus !== 'function') {
return;
}
field.focus({ preventScroll: true });
};
const focusAfterPaint = () => {
if (typeof global.requestAnimationFrame === 'function') {
global.requestAnimationFrame(focus);
return;
}
focus();
};
if (typeof this.$nextTick === 'function') {
this.$nextTick(focusAfterPaint);
return;
}
focusAfterPaint();
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/static/js/modules/guardrails.js` around lines 195 -
222, The current focusGuardrailForm() uses a single querySelector list that lets
the close button match before the intended field; update the logic in
focusGuardrailForm() so it first attempts
editor.querySelector('[data-modal-autofocus]') and, if that returns null or is
not focusable, then falls back to
editor.querySelector('input:not([type="hidden"]):not([disabled]),
textarea:not([disabled]), select:not([disabled])') — do not include generic
button selectors in the fallback. Keep the existing focus(), focusAfterPaint(),
and $nextTick handling but change the selector checks and focusability tests
accordingly; apply the same selector-change fix to the analogous functions in
workflows.js and aliases.js (the functions that mirror focusGuardrailForm's
behavior).

Comment on lines +483 to 508
focusWorkflowForm() {
const focus = () => {
const refs = this.$refs || {};
const editor = refs.workflowEditor || null;
if (!editor || typeof editor.querySelector !== 'function') {
return;
}
const field = editor.querySelector('[data-modal-autofocus], input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled]), button:not([disabled])');
if (!field || typeof field.focus !== 'function') {
return;
}
field.focus({ preventScroll: true });
};
const focusAfterPaint = () => {
if (typeof global.requestAnimationFrame === 'function') {
global.requestAnimationFrame(focus);
return;
}
focus();
};
if (typeof global.requestAnimationFrame === 'function') {
global.requestAnimationFrame(scroll);
if (typeof this.$nextTick === 'function') {
this.$nextTick(focusAfterPaint);
return;
}
scroll();
focusAfterPaint();
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Duplicated focus helper — consolidate with focusEditorField(refName).

Per the relevant snippet, aliases.js already defines a parameterized focusEditorField(refName) (line 478–506) that implements the exact same logic. Re-implementing it per-module duplicates non-trivial focus/$nextTick/requestAnimationFrame logic and drifts easily. Prefer calling the shared helper (e.g. this.focusEditorField('workflowEditor')) and delete focusWorkflowForm(). Same advice applies wherever focusGuardrailForm/focusAliasForm/etc. were introduced.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/static/js/modules/workflows.js` around lines 483 -
508, The focusWorkflowForm method duplicates shared logic already implemented in
focusEditorField(refName); replace the body of focusWorkflowForm by delegating
to the shared helper (call this.focusEditorField('workflowEditor')) and remove
any redundant local implementation; do the same for other duplicated helpers
like focusGuardrailForm and focusAliasForm so they simply call focusEditorField
with the appropriate ref name, ensuring existing usage of
this.$nextTick/requestAnimationFrame continues to be handled by the centralized
focusEditorField implementation.

Comment on lines +72 to +84
<div class="form-field">
<label class="form-field-label" for="guardrail-name">Name</label>
<input id="guardrail-name" type="text" placeholder="safety-system-prompt" x-model="guardrailForm.name" :disabled="guardrailFormMode === 'edit'" data-modal-autofocus>
</div>

<div class="form-field">
<label class="form-field-label" for="guardrail-type">Type</label>
<select id="guardrail-type" class="usage-log-select settings-select" x-ref="guardrailTypeSelect" x-model="guardrailForm.type" x-effect="guardrailTypes.length; guardrailForm.type; $nextTick(() => syncGuardrailTypeSelectValue())" :disabled="guardrailFormMode === 'edit'" @change="onGuardrailTypeChange()">
<template x-for="typeDef in guardrailTypes" :key="typeDef.type">
<option :value="typeDef.type" x-text="typeDef.label"></option>
</template>
</select>
</div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

data-modal-autofocus targets a field that is :disabled in edit mode.

The shared focusEditorField selector matches [data-modal-autofocus] unconditionally, but the name input here is disabled when guardrailFormMode === 'edit'. Calling .focus() on a disabled element is a no-op in every major browser, so the edit modal opens with no focused field and keyboard users land nowhere useful. Options:

  • Put data-modal-autofocus on guardrail-description (or the first type-specific field) instead; or
  • Make the attribute conditional: :data-modal-autofocus="guardrailFormMode === 'edit' ? null : true".

The same concern applies to the type <select> at Line 79 if it ever becomes the preferred autofocus target in edit mode.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/templates/page-guardrails.html` around lines 72 -
84, The modal autofocus currently targets the name input via the
data-modal-autofocus attribute while the input is disabled when
guardrailFormMode === 'edit', so move or make the autofocus conditional: either
place data-modal-autofocus on a non-disabled field such as the
guardrail-description or the first type-specific field, or change the attribute
to be conditional (e.g., bind it based on guardrailFormMode) so focusEditorField
doesn't call .focus() on a disabled element; ensure the same change is applied
to the guardrail-type <select> (which uses x-ref="guardrailTypeSelect" and is
tied to guardrailForm.type, guardrailTypes, syncGuardrailTypeSelectValue, and
onGuardrailTypeChange) if it could be targeted for autofocus.

Comment thread internal/admin/dashboard/templates/page-overview.html
Comment thread internal/admin/dashboard/templates/page-settings.html
Comment thread internal/admin/dashboard/templates/page-usage.html
Comment on lines +1934 to +2010
func TestHandleError_LogsClientErrorsAtWarnLevel(t *testing.T) {
var buf bytes.Buffer
original := slog.Default()
slog.SetDefault(slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})))
t.Cleanup(func() {
slog.SetDefault(original)
})

e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/admin/api/v1/workflows", nil)
req = req.WithContext(core.WithRequestID(req.Context(), "admin-warn-req-123"))
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

if err := handleError(c, core.NewInvalidRequestError("unknown provider name: missing", nil)); err != nil {
t.Fatalf("handleError() error = %v", err)
}

if rec.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
}

logOutput := buf.String()
if !strings.Contains(logOutput, `"level":"WARN"`) {
t.Fatalf("expected WARN log, got %q", logOutput)
}
if !strings.Contains(logOutput, `"msg":"admin request failed"`) {
t.Fatalf("expected admin request failed log, got %q", logOutput)
}
if !strings.Contains(logOutput, `"path":"/admin/api/v1/workflows"`) {
t.Fatalf("expected admin path in log, got %q", logOutput)
}
if !strings.Contains(logOutput, `"request_id":"admin-warn-req-123"`) {
t.Fatalf("expected request_id in log, got %q", logOutput)
}
}

func TestHandleError_LogsServerErrorsAtErrorLevel(t *testing.T) {
var buf bytes.Buffer
original := slog.Default()
slog.SetDefault(slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})))
t.Cleanup(func() {
slog.SetDefault(original)
})

e := echo.New()
req := httptest.NewRequest(http.MethodPut, "/admin/api/v1/guardrails/privacy", nil)
req = req.WithContext(core.WithRequestID(req.Context(), "admin-error-req-456"))
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

upstreamErr := errors.New("storage unavailable")
if err := handleError(c, core.NewProviderError("guardrails", http.StatusInternalServerError, "failed to refresh guardrails", upstreamErr)); err != nil {
t.Fatalf("handleError() error = %v", err)
}

if rec.Code != http.StatusInternalServerError {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusInternalServerError)
}

logOutput := buf.String()
if !strings.Contains(logOutput, `"level":"ERROR"`) {
t.Fatalf("expected ERROR log, got %q", logOutput)
}
if !strings.Contains(logOutput, `"msg":"admin request failed"`) {
t.Fatalf("expected admin request failed log, got %q", logOutput)
}
if !strings.Contains(logOutput, `"provider":"guardrails"`) {
t.Fatalf("expected provider in log, got %q", logOutput)
}
if !strings.Contains(logOutput, `"request_id":"admin-error-req-456"`) {
t.Fatalf("expected request_id in log, got %q", logOutput)
}
if !strings.Contains(logOutput, `"message":"failed to refresh guardrails"`) {
t.Fatalf("expected error message in log, got %q", logOutput)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Add coverage for the X-Request-ID fallback branch.

The production helper now falls back to the request header when the context has no request ID, but the new tests only exercise the context path.

Suggested test coverage
+func TestHandleError_LogsRequestIDFromHeaderWhenContextMissing(t *testing.T) {
+	var buf bytes.Buffer
+	original := slog.Default()
+	slog.SetDefault(slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})))
+	t.Cleanup(func() {
+		slog.SetDefault(original)
+	})
+
+	e := echo.New()
+	req := httptest.NewRequest(http.MethodGet, "/admin/api/v1/aliases", nil)
+	req.Header.Set("X-Request-ID", "admin-header-req-789")
+	rec := httptest.NewRecorder()
+	c := e.NewContext(req, rec)
+
+	if err := handleError(c, core.NewNotFoundError("alias not found: missing")); err != nil {
+		t.Fatalf("handleError() error = %v", err)
+	}
+
+	if rec.Code != http.StatusNotFound {
+		t.Fatalf("status = %d, want %d", rec.Code, http.StatusNotFound)
+	}
+	if logOutput := buf.String(); !strings.Contains(logOutput, `"request_id":"admin-header-req-789"`) {
+		t.Fatalf("expected header request_id in log, got %q", logOutput)
+	}
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/handler_test.go` around lines 1934 - 2010, Add a new unit test
that exercises the X-Request-ID fallback by calling handleError with an HTTP
request that does NOT have a context request ID but DOES have the "X-Request-ID"
header set; mirror the existing
TestHandleError_LogsClientErrorsAtWarnLevel/TestHandleError_LogsServerErrorsAtErrorLevel
pattern (create buf, set slog.Default to JSON handler, build echo.Context via
e.NewContext), set req.Header.Set("X-Request-ID","admin-header-req-789"), call
handleError (e.g., pass core.NewInvalidRequestError or core.NewProviderError),
assert response code and verify the JSON log contains the header value
`"request_id":"admin-header-req-789"` along with the expected
level/msg/path/provider/message fields.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (1)
internal/admin/dashboard/static/js/modules/guardrails.js (1)

195-207: ⚠️ Potential issue | 🟠 Major

Autofocus still lands on the close button first.

querySelector() does not prioritize the first selector in the list; it returns the first matching element in DOM order. In page-guardrails.html, the header close button appears before the data-modal-autofocus input, and button:not([disabled]) matches it first. Try the explicit autofocus marker first, then fall back to non-button form fields.

🐛 Proposed fix
-                    const field = editor.querySelector('[data-modal-autofocus]:not([disabled]), input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled]), button:not([disabled])');
+                    const field = editor.querySelector('[data-modal-autofocus]:not([disabled])')
+                        || editor.querySelector('input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled])');

Verify the DOM-order condition and any analogous selector-list implementations:

#!/bin/bash
set -euo pipefail

python - <<'PY'
from pathlib import Path

js = Path("internal/admin/dashboard/static/js/modules/guardrails.js").read_text()
tpl = Path("internal/admin/dashboard/templates/page-guardrails.html").read_text()

selector = "button:not([disabled])"
if selector not in js:
    raise SystemExit("Expected button fallback selector was not found in focusGuardrailForm")

close_index = tpl.index("alias-close-btn")
autofocus_index = tpl.index("data-modal-autofocus")
print(f"close button index: {close_index}")
print(f"autofocus field index: {autofocus_index}")

if close_index < autofocus_index:
    print("Close button is before the intended autofocus field; a single querySelector selector list can select it first.")
else:
    raise SystemExit("Template order no longer reproduces the focus issue")
PY

rg -n -C 3 "querySelector\\('\\[data-modal-autofocus][^']*button:not\\(\\[disabled\\]\\)" internal/admin/dashboard/static/js/modules
Does Element.querySelector with a comma-separated selector list return the first matching element in document order rather than prioritizing the first selector?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/static/js/modules/guardrails.js` around lines 195 -
207, The focusGuardrailForm function currently uses a single comma-separated
querySelector which returns the first matching element in DOM order (causing the
header close button to get focused); update focus logic in focusGuardrailForm to
explicitly try editor.querySelector('[data-modal-autofocus]:not([disabled])')
first, then fall back to
editor.querySelector('input:not([type="hidden"]):not([disabled]),
textarea:not([disabled]), select:not([disabled])') and only as a last resort
querySelector('button:not([disabled])'); ensure you keep the existing guards
(check editor and field and typeof field.focus) and call field.focus({
preventScroll: true }) when a field is found.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/admin/dashboard/static/js/modules/workflows-layout.test.js`:
- Around line 31-37: The readCSSRule helper currently requires an unindented
newline before the closing brace which breaks valid CSS with CRLF or indented
braces; update the RegExp in readCSSRule (the function named readCSSRule) to be
more tolerant by allowing optional CR/LF and any whitespace before the `}` (e.g.
replace `\\n\\}` with `\\s*\\}` and/or support `\\r?\\n`), so the pattern
becomes something like `${escapedSelector}\\s*\\{([\\s\\S]*?)\\s*\\}` with the
"m" flag to match rules regardless of closing-brace indentation or line endings.

In `@internal/admin/dashboard/templates/page-models.html`:
- Line 25: The category tab button element (class "category-tab") is missing an
explicit type attribute; update the <button class="category-tab" :class="{
active: activeCategory === cat.category }"
`@click`="selectCategory(cat.category)"> element to include type="button" so it
matches other buttons in the template and avoids HTMLHint warnings and future
form-related regressions.

In `@internal/admin/dashboard/templates/page-usage.html`:
- Around line 9-10: The two usage-mode buttons (class "usage-mode-btn", bound to
usageMode and calling toggleUsageMode('tokens') / toggleUsageMode('costs')) lack
an explicit type attribute; add type="button" to both button elements to match
other buttons (e.g., chart-view-btn) and prevent default form-submit behavior if
moved into a form in the future. Locate the buttons by the class
"usage-mode-btn" and update their markup to include type="button" for both the
Tokens and Costs buttons.

In `@internal/admin/dashboard/templates/page-workflows.html`:
- Line 169: Replace the stray tab characters at the start of the affected
template lines with spaces so indentation matches the rest of the file;
specifically remove the leading TAB before the "<div
class=\"workflow-guardrail-editor\" x-show=\"workflowForm.features.guardrails &&
workflowGuardrailsVisible()\">" line (and the other occurrence around line 236)
so both divs use the same space-based indentation as the surrounding template.
- Around line 37-46: The workflow modal backdrop and shell (elements using
x-cloak x-show="workflowFormOpen") currently lack the fade transition used by
other editors; add the same Alpine transition modifier
x-transition.opacity.duration.160ms to both the backdrop div and the shell div
(the elements tied to workflowFormOpen and the shell that has
`@click`="closeWorkflowForm()") so the workflow editor (x-ref="workflowEditor")
fades in/out consistently with the alias and model-access modals.

---

Duplicate comments:
In `@internal/admin/dashboard/static/js/modules/guardrails.js`:
- Around line 195-207: The focusGuardrailForm function currently uses a single
comma-separated querySelector which returns the first matching element in DOM
order (causing the header close button to get focused); update focus logic in
focusGuardrailForm to explicitly try
editor.querySelector('[data-modal-autofocus]:not([disabled])') first, then fall
back to editor.querySelector('input:not([type="hidden"]):not([disabled]),
textarea:not([disabled]), select:not([disabled])') and only as a last resort
querySelector('button:not([disabled])'); ensure you keep the existing guards
(check editor and field and typeof field.focus) and call field.focus({
preventScroll: true }) when a field is found.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e0485495-fa83-4d45-995f-0bc4c982a442

📥 Commits

Reviewing files that changed from the base of the PR and between 14cf2da and 7fee26e.

📒 Files selected for processing (14)
  • internal/admin/dashboard/static/js/modules/auth-keys.js
  • internal/admin/dashboard/static/js/modules/auth-keys.test.js
  • internal/admin/dashboard/static/js/modules/dashboard-layout.test.js
  • internal/admin/dashboard/static/js/modules/guardrails.js
  • internal/admin/dashboard/static/js/modules/guardrails.test.js
  • internal/admin/dashboard/static/js/modules/timezone-layout.test.js
  • internal/admin/dashboard/static/js/modules/workflows-layout.test.js
  • internal/admin/dashboard/templates/page-auth-keys.html
  • internal/admin/dashboard/templates/page-guardrails.html
  • internal/admin/dashboard/templates/page-models.html
  • internal/admin/dashboard/templates/page-overview.html
  • internal/admin/dashboard/templates/page-settings.html
  • internal/admin/dashboard/templates/page-usage.html
  • internal/admin/dashboard/templates/page-workflows.html

Comment on lines 31 to +37
function readCSSRule(source, selector) {
const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const match = source.match(new RegExp(`${escapedSelector}\\s*\\{([\\s\\S]*?)\\n\\}`, 'm'));
assert.ok(match, `Expected CSS rule for ${selector}`);
return match[1];
const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const match = source.match(
new RegExp(`${escapedSelector}\\s*\\{([\\s\\S]*?)\\n\\}`, "m"),
);
assert.ok(match, `Expected CSS rule for ${selector}`);
return match[1];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Keep readCSSRule tolerant of valid CSS formatting.

This copy still requires an unindented } after \n, so CRLF or indented closing braces can break workflow layout tests even though the CSS is valid. Align it with the more robust helper used in dashboard-layout.test.js.

♻️ Proposed fix
 function readCSSRule(source, selector) {
   const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
   const match = source.match(
-    new RegExp(`${escapedSelector}\\s*\\{([\\s\\S]*?)\\n\\}`, "m"),
+    new RegExp(`${escapedSelector}\\s*\\{([\\s\\S]*?)\\s*\\}`, "m"),
   );
   assert.ok(match, `Expected CSS rule for ${selector}`);
   return match[1];
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/static/js/modules/workflows-layout.test.js` around
lines 31 - 37, The readCSSRule helper currently requires an unindented newline
before the closing brace which breaks valid CSS with CRLF or indented braces;
update the RegExp in readCSSRule (the function named readCSSRule) to be more
tolerant by allowing optional CR/LF and any whitespace before the `}` (e.g.
replace `\\n\\}` with `\\s*\\}` and/or support `\\r?\\n`), so the pattern
becomes something like `${escapedSelector}\\s*\\{([\\s\\S]*?)\\s*\\}` with the
"m" flag to match rules regardless of closing-brace indentation or line endings.

Comment thread internal/admin/dashboard/templates/page-models.html Outdated
Comment thread internal/admin/dashboard/templates/page-usage.html Outdated
Comment thread internal/admin/dashboard/templates/page-workflows.html
</article>
</div>

<div class="workflow-guardrail-editor" x-show="workflowForm.features.guardrails && workflowGuardrailsVisible()">
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Stray tab characters break indentation.

Lines 169 and 236 start with a literal TAB followed by spaces (\t <div ...>), while the surrounding template uses only spaces. This trips up editors configured with different tab widths and shows up oddly in diffs/grep. Replace the tabs with spaces to match the rest of the file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/templates/page-workflows.html` at line 169, Replace
the stray tab characters at the start of the affected template lines with spaces
so indentation matches the rest of the file; specifically remove the leading TAB
before the "<div class=\"workflow-guardrail-editor\"
x-show=\"workflowForm.features.guardrails && workflowGuardrailsVisible()\">"
line (and the other occurrence around line 236) so both divs use the same
space-based indentation as the surrounding template.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (5)
internal/admin/dashboard/templates/page-workflows.html (1)

171-171: 🧹 Nitpick | 🔵 Trivial

Remove the stray tab indentation.

Lines 171 and 238 still start with a literal tab before spaces; normalize to spaces to match the surrounding template.

Also applies to: 238-238

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/templates/page-workflows.html` at line 171, Remove
the stray tab characters at the start of the affected template lines so
indentation uses spaces consistently; specifically, edit the lines containing
the div with class "workflow-guardrail-editor" (the element with
x-show="workflowForm.features.guardrails && workflowGuardrailsVisible()") and
the similar line around the other occurrence (line with the same class/attribute
near the second mention) to replace the leading tab with the project's standard
spaces so the template's indentation is normalized.
internal/admin/dashboard/static/css/dashboard.css (2)

1700-1709: 🧹 Nitpick | 🔵 Trivial

Still using generic .form* selectors for modal-specific forms.

These selectors remain broad enough to collide with future dashboard markup. Prefer an editor-scoped prefix such as .editor-form, .editor-form-field, .editor-form-error, and .editor-form-actions.

Also applies to: 1786-1889

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/static/css/dashboard.css` around lines 1700 - 1709,
Replace the broad form selectors with editor-scoped ones to avoid global
collisions: change occurrences of the selectors .form h3, .form-kicker,
.form-hint (and the other .form* selectors in the block covering lines
referenced 1786-1889) to namespaced versions such as .editor-form h3,
.editor-form-kicker, .editor-form-hint (and .editor-form-field,
.editor-form-error, .editor-form-actions where applicable), then update any
corresponding markup using these selectors to match the new class names so
styles remain scoped to the editor modal.

1371-1385: 🧹 Nitpick | 🔵 Trivial

Still broad-scoping base input styles.

This global input:is(...) rule can still affect unrelated dashboard controls and omits common form types like email, password, search, url, tel, and time. Scope it to the intended form/editor containers or broaden the supported input types consistently.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/static/css/dashboard.css` around lines 1371 - 1385,
The global selector input:is([type="text"], [type="date"], [type="number"]) is
too broad and misses common types; narrow it to the intended container (e.g.,
prefix with the editor/form container class such as .dashboard-editor or
.form-container so only inputs inside that component are styled) and extend the
type list to include email, password, search, url, tel, and time (update both
the base selector and the :focus rule for consistency). Ensure you replace
input:is(...) with the scoped selector (e.g., .dashboard-editor
input:is([type="text"], [type="date"], [type="number"], [type="email"],
[type="password"], [type="search"], [type="url"], [type="tel"], [type="time"]))
so unrelated dashboard controls aren’t affected and focus styling remains
consistent.
internal/admin/dashboard/static/js/modules/workflows-layout.test.js (1)

31-37: 🧹 Nitpick | 🔵 Trivial

Keep readCSSRule tolerant of valid CSS formatting.

This helper still requires an unindented LF before }, so valid CRLF or indented closing braces can break these tests. Match the more robust helper already used in dashboard-layout.test.js.

♻️ Proposed fix
 function readCSSRule(source, selector) {
   const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
   const match = source.match(
-    new RegExp(`${escapedSelector}\\s*\\{([\\s\\S]*?)\\n\\}`, "m"),
+    new RegExp(`${escapedSelector}\\s*\\{([\\s\\S]*?)\\s*\\}`, "m"),
   );
   assert.ok(match, `Expected CSS rule for ${selector}`);
   return match[1];
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/static/js/modules/workflows-layout.test.js` around
lines 31 - 37, The readCSSRule helper's regex requires an unindented LF before
the closing brace and fails on CRLF or indented braces; update the regexp in
function readCSSRule to accept optional CR and any whitespace before the closing
"}" (e.g., allow \r?\n or any whitespace characters) and/or use a non-greedy
capture that terminates at a closing brace preceded by optional whitespace
instead of requiring "\n\}"; keep the escapedSelector handling and assertion
(readCSSRule, escapedSelector, match) otherwise unchanged.
internal/admin/dashboard/templates/page-guardrails.html (1)

72-88: ⚠️ Potential issue | 🟡 Minor

Don’t autofocus a disabled field in edit mode.

Line 74 keeps data-modal-autofocus on the name input even when edit mode disables it, so the edit modal can open without useful keyboard focus.

♿ Proposed fix
-                        <input id="guardrail-name" type="text" placeholder="safety-system-prompt" x-model="guardrailForm.name" :disabled="guardrailFormMode === 'edit'" data-modal-autofocus>
+                        <input id="guardrail-name" type="text" placeholder="safety-system-prompt" x-model="guardrailForm.name" :disabled="guardrailFormMode === 'edit'" :data-modal-autofocus="guardrailFormMode === 'create' ? true : null">
@@
-                        <input id="guardrail-description" type="text" placeholder="What this guardrail is meant to do" x-model="guardrailForm.description">
+                        <input id="guardrail-description" type="text" placeholder="What this guardrail is meant to do" x-model="guardrailForm.description" :data-modal-autofocus="guardrailFormMode === 'edit' ? true : null">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/templates/page-guardrails.html` around lines 72 -
88, The name input currently always has data-modal-autofocus even when
guardrailFormMode === 'edit', causing focus to land on a disabled field; update
the guardrail-name input to only include the autofocus behavior when not in edit
mode by conditionally applying or removing data-modal-autofocus based on
guardrailFormMode (use the existing guardrailFormMode and guardrailForm.name
identifiers to locate the element and ensure the autofocus attribute is not
present when guardrailFormMode === 'edit').
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/admin/dashboard/templates/page-audit-logs.html`:
- Around line 121-174: Add x-cloak to the overlay div and the <aside
class="conversation-drawer"> and add :inert="!conversationOpen" to the same
aside so the drawer and its focusable descendants (e.g., the close button
x-ref="conversationCloseBtn" and details toggles in the conversation thread) are
removed from keyboard navigation when conversationOpen is false; keep bindings
that reference conversationOpen, x-ref="conversationDialog", and aria-hidden
as-is to preserve existing behavior.

In `@internal/admin/dashboard/templates/page-auth-keys.html`:
- Around line 83-85: The Name input (id="auth-key-name",
x-model="authKeyForm.name", x-ref="authKeyNameInput") is marked required in the
label but lacks native validation—add the required attribute (and optionally
minlength or pattern) to the input so the browser enforces it, and update the
form submission flow to call the form's checkValidity()/reportValidity() (or
return early if invalid) before sending (ensure the submit handler that reads
authKeyForm runs this check). This ensures the visible create form prevents
empty names client-side using native HTML validation tied to authKeyNameInput.

In `@internal/admin/dashboard/templates/page-guardrails.html`:
- Around line 43-44: The dynamic guardrail alert DIVs lack ARIA live-region
semantics so screen readers won't announce the Alpine-driven changes; update the
two elements rendered in page-guardrails.html (the divs using x-show with
guardrailError and guardrailNotice) to include accessibility attributes such as
role="status", aria-live="polite" and aria-atomic="true" (and optionally
tabindex="-1" if focusable announcement is desired) so assistive tech will
announce the messages when the Alpine state (guardrailError / guardrailNotice)
changes.
- Around line 130-142: Checkbox checkbox fields always show the help text
instead of using the inline question-mark helper like other field types; update
the template under the block where field.input === 'checkboxes' to use the same
inline helper pattern as the other type-specific fields. Replace the
always-visible <small class="form-hint" ...> help with the question-mark/inline
helper markup used elsewhere (keep the same aria-describedby id pattern
'guardrail-field-help-' + field.key and only render the helper when field.help
exists), wire the helper's toggle/tooltip to the same accessibility attributes
so guardrailArrayFieldSelected and toggleGuardrailArrayFieldValue behavior and
focus/aria semantics remain unchanged. Ensure the helper reads field.help and
preserves keyboard accessibility for the checkbox inputs.

In `@internal/admin/dashboard/templates/page-usage.html`:
- Around line 139-149: The model/provider <select> controls (bound to
usageLogModel and usageLogProvider, class usage-log-select) lack accessible
names; add aria-label attributes to each select (e.g., aria-label="Model filter"
on the usageLogModel select and aria-label="Provider filter" on the
usageLogProvider select) so screen readers announce the control purpose
consistently after a value is chosen.

---

Duplicate comments:
In `@internal/admin/dashboard/static/css/dashboard.css`:
- Around line 1700-1709: Replace the broad form selectors with editor-scoped
ones to avoid global collisions: change occurrences of the selectors .form h3,
.form-kicker, .form-hint (and the other .form* selectors in the block covering
lines referenced 1786-1889) to namespaced versions such as .editor-form h3,
.editor-form-kicker, .editor-form-hint (and .editor-form-field,
.editor-form-error, .editor-form-actions where applicable), then update any
corresponding markup using these selectors to match the new class names so
styles remain scoped to the editor modal.
- Around line 1371-1385: The global selector input:is([type="text"],
[type="date"], [type="number"]) is too broad and misses common types; narrow it
to the intended container (e.g., prefix with the editor/form container class
such as .dashboard-editor or .form-container so only inputs inside that
component are styled) and extend the type list to include email, password,
search, url, tel, and time (update both the base selector and the :focus rule
for consistency). Ensure you replace input:is(...) with the scoped selector
(e.g., .dashboard-editor input:is([type="text"], [type="date"], [type="number"],
[type="email"], [type="password"], [type="search"], [type="url"], [type="tel"],
[type="time"])) so unrelated dashboard controls aren’t affected and focus
styling remains consistent.

In `@internal/admin/dashboard/static/js/modules/workflows-layout.test.js`:
- Around line 31-37: The readCSSRule helper's regex requires an unindented LF
before the closing brace and fails on CRLF or indented braces; update the regexp
in function readCSSRule to accept optional CR and any whitespace before the
closing "}" (e.g., allow \r?\n or any whitespace characters) and/or use a
non-greedy capture that terminates at a closing brace preceded by optional
whitespace instead of requiring "\n\}"; keep the escapedSelector handling and
assertion (readCSSRule, escapedSelector, match) otherwise unchanged.

In `@internal/admin/dashboard/templates/page-guardrails.html`:
- Around line 72-88: The name input currently always has data-modal-autofocus
even when guardrailFormMode === 'edit', causing focus to land on a disabled
field; update the guardrail-name input to only include the autofocus behavior
when not in edit mode by conditionally applying or removing data-modal-autofocus
based on guardrailFormMode (use the existing guardrailFormMode and
guardrailForm.name identifiers to locate the element and ensure the autofocus
attribute is not present when guardrailFormMode === 'edit').

In `@internal/admin/dashboard/templates/page-workflows.html`:
- Line 171: Remove the stray tab characters at the start of the affected
template lines so indentation uses spaces consistently; specifically, edit the
lines containing the div with class "workflow-guardrail-editor" (the element
with x-show="workflowForm.features.guardrails && workflowGuardrailsVisible()")
and the similar line around the other occurrence (line with the same
class/attribute near the second mention) to replace the leading tab with the
project's standard spaces so the template's indentation is normalized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b1e2037b-1445-4c7b-81b0-e48ab6e99d10

📥 Commits

Reviewing files that changed from the base of the PR and between 7fee26e and e000213.

📒 Files selected for processing (10)
  • internal/admin/dashboard/static/css/dashboard.css
  • internal/admin/dashboard/static/js/modules/dashboard-layout.test.js
  • internal/admin/dashboard/static/js/modules/workflows-layout.test.js
  • internal/admin/dashboard/templates/layout.html
  • internal/admin/dashboard/templates/page-audit-logs.html
  • internal/admin/dashboard/templates/page-auth-keys.html
  • internal/admin/dashboard/templates/page-guardrails.html
  • internal/admin/dashboard/templates/page-models.html
  • internal/admin/dashboard/templates/page-usage.html
  • internal/admin/dashboard/templates/page-workflows.html

Comment on lines +121 to +174
<div class="conversation-overlay" x-show="conversationOpen" x-transition.opacity.duration.160ms aria-hidden="true" @click="closeConversation()"></div>
<aside class="conversation-drawer"
:class="{ open: conversationOpen }"
x-ref="conversationDialog"
tabindex="-1"
role="dialog"
aria-modal="true"
:aria-hidden="(!conversationOpen).toString()"
aria-labelledby="interactions-drawer-title"
aria-describedby="interactions-drawer-content"
@keydown.escape.window="conversationOpen && closeConversation()">
<div class="conversation-drawer-header">
<h3 id="interactions-drawer-title">Interactions</h3>
<button type="button" class="dialog-close-btn" x-ref="conversationCloseBtn" aria-label="Close interactions" @click="closeConversation()">
{{template "x-icon"}}
</button>
</div>

<div id="interactions-drawer-content">
<div class="alert alert-warning" x-show="conversationError" x-text="conversationError"></div>
<p class="empty-state" x-show="conversationLoading">Loading interactions...</p>
<p class="empty-state" x-show="!conversationLoading && !conversationError && conversationMessages.length === 0">No interaction data available for this entry.</p>

<div class="conversation-thread" x-show="conversationMessages.length > 0">
<template x-for="msg in conversationMessages" :key="msg.uid">
<article :class="[msg.role === 'function_call' || msg.role === 'function_result' ? 'chat-function-note' : 'chat-message', msg.roleClass, msg.isAnchor ? 'is-anchor' : '']">
<details class="chat-function-note-details" x-show="msg.role === 'function_call' || msg.role === 'function_result'">
<summary class="chat-function-note-inner">
<span class="chat-function-label" x-text="msg.roleLabel"></span>
<span class="chat-function-detail" x-text="msg.role === 'function_call' ? (msg.toolCalls || []).map(tc => tc.name + '()').join(', ') : (msg.functionName ? msg.functionName + ': ' : '') + msg.text"></span>
</summary>
<pre class="chat-function-expanded" x-text="functionExpandedContent(msg)"></pre>
</details>
<header class="chat-message-meta" x-show="msg.role !== 'function_call' && msg.role !== 'function_result'">
<span class="chat-role" x-text="msg.roleLabel"></span>
<span class="mono chat-time" x-text="formatTimestamp(msg.timestamp)"></span>
</header>
<pre class="chat-content" x-show="msg.text && msg.role !== 'function_call' && msg.role !== 'function_result'" x-text="msg.text"></pre>
<footer class="chat-tool-calls" x-show="msg.toolCalls && msg.role !== 'function_call'">
<template x-for="(tc, tcIdx) in (msg.toolCalls || [])" :key="tc.name + '-' + tcIdx">
<div class="chat-tool-call">
<span class="chat-tool-call-name" x-text="tc.name + '()'"></span>
</div>
</template>
</footer>
</article>
</template>
</div>
</div>

<div class="conversation-drawer-footer" x-show="conversationAnchorID">
<p class="conversation-meta" x-text="'Opened from log: ' + conversationAnchorID"></p>
</div>
</aside>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the file and verify it exists
find . -name "page-audit-logs.html" -type f

Repository: ENTERPILOT/GoModel

Length of output: 119


🏁 Script executed:

# Check the content around lines 121-174
head -n 180 internal/admin/dashboard/templates/page-audit-logs.html | tail -n 65

Repository: ENTERPILOT/GoModel

Length of output: 4077


🌐 Web query:

Alpine.js :inert binding support and x-cloak behavior

💡 Result:

Alpine.js supports the inert attribute via x-bind binding (e.g., :inert="true"), as 'inert' is recognized as a boolean attribute in recent versions (confirmed in Alpine.js PR #4325 merged July 2024, adding 'inert' to the list of boolean attributes handled by x-bind). There is no dedicated x-inert directive; use x-bind or its shorthand ':' for dynamic control. Note that browser support for the native inert attribute is good in modern browsers (Chrome 102+, Firefox 118+, Safari 15.4+ as of 2026). x-cloak hides elements with the x-cloak attribute until Alpine initializes, preventing FOUC (Flash of Unstyled Content). It requires CSS: [x-cloak] { display: none !important; }. Alpine removes x-cloak attributes post-initialization, revealing the content. Commonly used with x-show, x-text, etc., on elements that might briefly show raw templates before reactivity kicks in. Alternative: for similar effect without global CSS.

Citations:


🏁 Script executed:

# Search for Alpine.js version and configuration
find . -name "package.json" -o -name "go.mod" -o -name "*.lock" | head -5 | xargs grep -l "alpine\|Alpine" 2>/dev/null

Repository: ENTERPILOT/GoModel

Length of output: 46


🏁 Script executed:

# Look for x-cloak CSS definition in the codebase
rg "x-cloak" --type html --type css -A 3

Repository: ENTERPILOT/GoModel

Length of output: 4510


Add x-cloak and :inert binding to prevent closed drawer from remaining focusable.

The drawer uses CSS class binding only and leaves all focusable descendants (close button, expandable content) in the tab order when conversationOpen is false. This creates an accessibility issue where keyboard users can focus hidden controls.

Add x-cloak on both the overlay and drawer (consistent with all other modals in the codebase like page-guardrails.html, page-workflows.html, and page-models.html) to prevent initial paint flicker. Add :inert="!conversationOpen" to the drawer to properly remove it from keyboard navigation when closed.

Proposed fix
-    <div class="conversation-overlay" x-show="conversationOpen" x-transition.opacity.duration.160ms aria-hidden="true" `@click`="closeConversation()"></div>
+    <div class="conversation-overlay" x-cloak x-show="conversationOpen" x-transition.opacity.duration.160ms aria-hidden="true" `@click`="closeConversation()"></div>
     <aside class="conversation-drawer"
+           x-cloak
            :class="{ open: conversationOpen }"
+           :inert="!conversationOpen"
            x-ref="conversationDialog"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/templates/page-audit-logs.html` around lines 121 -
174, Add x-cloak to the overlay div and the <aside class="conversation-drawer">
and add :inert="!conversationOpen" to the same aside so the drawer and its
focusable descendants (e.g., the close button x-ref="conversationCloseBtn" and
details toggles in the conversation thread) are removed from keyboard navigation
when conversationOpen is false; keep bindings that reference conversationOpen,
x-ref="conversationDialog", and aria-hidden as-is to preserve existing behavior.

Comment on lines +83 to +85
<label class="form-field-label" for="auth-key-name">Name <span class="form-hint">(required)</span></label>
<input id="auth-key-name" type="text" placeholder="e.g. ci-deploy"
x-model="authKeyForm.name" autocomplete="off" x-ref="authKeyNameInput" data-modal-autofocus>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make the required API key name enforceable.

The label marks this field required, but the input can still submit blank and rely on a server error. Add native validation for the visible create form.

🐛 Proposed fix
-                            <input id="auth-key-name" type="text" placeholder="e.g. ci-deploy"
-                                x-model="authKeyForm.name" autocomplete="off" x-ref="authKeyNameInput" data-modal-autofocus>
+                            <input id="auth-key-name" type="text" placeholder="e.g. ci-deploy"
+                                x-model="authKeyForm.name" autocomplete="off" x-ref="authKeyNameInput"
+                                data-modal-autofocus :required="!authKeyIssuedValue">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<label class="form-field-label" for="auth-key-name">Name <span class="form-hint">(required)</span></label>
<input id="auth-key-name" type="text" placeholder="e.g. ci-deploy"
x-model="authKeyForm.name" autocomplete="off" x-ref="authKeyNameInput" data-modal-autofocus>
<input id="auth-key-name" type="text" placeholder="e.g. ci-deploy"
x-model="authKeyForm.name" autocomplete="off" x-ref="authKeyNameInput"
data-modal-autofocus :required="!authKeyIssuedValue">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/templates/page-auth-keys.html` around lines 83 - 85,
The Name input (id="auth-key-name", x-model="authKeyForm.name",
x-ref="authKeyNameInput") is marked required in the label but lacks native
validation—add the required attribute (and optionally minlength or pattern) to
the input so the browser enforces it, and update the form submission flow to
call the form's checkValidity()/reportValidity() (or return early if invalid)
before sending (ensure the submit handler that reads authKeyForm runs this
check). This ensures the visible create form prevents empty names client-side
using native HTML validation tied to authKeyNameInput.

Comment on lines +43 to +44
<div class="alert alert-warning" x-show="!authError && guardrailError && !guardrailFormOpen" x-text="guardrailError"></div>
<div class="alert alert-success" x-show="!authError && guardrailNotice && !guardrailError" x-text="guardrailNotice"></div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Announce dynamic guardrail messages to assistive tech.

These messages appear via Alpine state changes, but without live-region semantics screen readers may not announce them.

♿ Proposed fix
-    <div class="alert alert-warning" x-show="!authError && guardrailError && !guardrailFormOpen" x-text="guardrailError"></div>
-    <div class="alert alert-success" x-show="!authError && guardrailNotice && !guardrailError" x-text="guardrailNotice"></div>
+    <div class="alert alert-warning" x-show="!authError && guardrailError && !guardrailFormOpen" x-text="guardrailError" role="alert" aria-live="assertive"></div>
+    <div class="alert alert-success" x-show="!authError && guardrailNotice && !guardrailError" x-text="guardrailNotice" role="status" aria-live="polite"></div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="alert alert-warning" x-show="!authError && guardrailError && !guardrailFormOpen" x-text="guardrailError"></div>
<div class="alert alert-success" x-show="!authError && guardrailNotice && !guardrailError" x-text="guardrailNotice"></div>
<div class="alert alert-warning" x-show="!authError && guardrailError && !guardrailFormOpen" x-text="guardrailError" role="alert" aria-live="assertive"></div>
<div class="alert alert-success" x-show="!authError && guardrailNotice && !guardrailError" x-text="guardrailNotice" role="status" aria-live="polite"></div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/templates/page-guardrails.html` around lines 43 -
44, The dynamic guardrail alert DIVs lack ARIA live-region semantics so screen
readers won't announce the Alpine-driven changes; update the two elements
rendered in page-guardrails.html (the divs using x-show with guardrailError and
guardrailNotice) to include accessibility attributes such as role="status",
aria-live="polite" and aria-atomic="true" (and optionally tabindex="-1" if
focusable announcement is desired) so assistive tech will announce the messages
when the Alpine state (guardrailError / guardrailNotice) changes.

Comment on lines +130 to +142
<template x-if="field.input === 'checkboxes'">
<fieldset class="form-field form-field-wide form-field-fieldset" :aria-describedby="field.help ? 'guardrail-field-help-' + field.key : null">
<legend class="form-field-legend" x-text="field.label"></legend>
<div class="workflow-feature-toggles">
<template x-for="option in field.options" :key="field.key + '-' + option.value">
<label class="workflow-feature-toggle">
<input type="checkbox" :checked="guardrailArrayFieldSelected(field, option.value)" @change="toggleGuardrailArrayFieldValue(field, option.value, $event.target.checked)">
<span x-text="option.label"></span>
</label>
</template>
</div>
<small class="form-hint" :id="'guardrail-field-help-' + field.key" x-show="field.help" x-text="field.help"></small>
</fieldset>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Use the inline helper pattern for checkbox fields too.

Checkbox guardrail fields still render always-visible help text, unlike the other type-specific fields that use the question-mark helper.

♻️ Proposed refactor
-                                <fieldset class="form-field form-field-wide form-field-fieldset" :aria-describedby="field.help ? 'guardrail-field-help-' + field.key : null">
-                                    <legend class="form-field-legend" x-text="field.label"></legend>
+                                <fieldset class="form-field form-field-wide form-field-fieldset" x-data="{ open: false, get copyId() { return 'guardrail-field-help-' + field.key }, get showLabel() { return 'Show ' + field.label + ' help' }, get hideLabel() { return 'Hide ' + field.label + ' help' }, get text() { return field.help || '' } }" :aria-describedby="field.help ? copyId : null">
+                                    <legend class="form-field-legend inline-help-title-row">
+                                        <span x-text="field.label"></span>
+                                        <template x-if="field.help">
+                                            {{template "inline-help-toggle" .}}
+                                        </template>
+                                    </legend>
+                                    <p :id="copyId" class="inline-help-copy" x-show="open && text" x-transition.opacity.duration.200ms x-text="text"></p>
                                     <div class="workflow-feature-toggles">
                                         <template x-for="option in field.options" :key="field.key + '-' + option.value">
                                             <label class="workflow-feature-toggle">
                                                 <input type="checkbox" :checked="guardrailArrayFieldSelected(field, option.value)" `@change`="toggleGuardrailArrayFieldValue(field, option.value, $event.target.checked)">
                                                 <span x-text="option.label"></span>
@@
-                                    <small class="form-hint" :id="'guardrail-field-help-' + field.key" x-show="field.help" x-text="field.help"></small>
                                 </fieldset>
🧰 Tools
🪛 HTMLHint (1.9.2)

[warning] 136-136: No matching [ label ] tag found.

(input-requires-label)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/templates/page-guardrails.html` around lines 130 -
142, Checkbox checkbox fields always show the help text instead of using the
inline question-mark helper like other field types; update the template under
the block where field.input === 'checkboxes' to use the same inline helper
pattern as the other type-specific fields. Replace the always-visible <small
class="form-hint" ...> help with the question-mark/inline helper markup used
elsewhere (keep the same aria-describedby id pattern 'guardrail-field-help-' +
field.key and only render the helper when field.help exists), wire the helper's
toggle/tooltip to the same accessibility attributes so
guardrailArrayFieldSelected and toggleGuardrailArrayFieldValue behavior and
focus/aria semantics remain unchanged. Ensure the helper reads field.help and
preserves keyboard accessibility for the checkbox inputs.

Comment on lines +139 to +149
<select x-model="usageLogModel" @change="fetchUsageLog(true)" class="usage-log-select">
<option value="">All Models</option>
<template x-for="m in usageLogModelOptions()" :key="m">
<option :value="m" x-text="m"></option>
</template>
</select>
<select x-model="usageLogProvider" @change="fetchUsageLog(true)" class="usage-log-select">
<option value="">All Providers</option>
<template x-for="p in usageLogProviderOptions()" :key="p">
<option :value="p" x-text="p"></option>
</template>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add accessible names to the model/provider filters.

The selects rely on option text only, so screen-reader users do not get a stable control purpose after a value is selected. Add aria-label values like the adjacent text inputs.

♿ Proposed fix
-                <select x-model="usageLogModel" `@change`="fetchUsageLog(true)" class="usage-log-select">
+                <select x-model="usageLogModel" `@change`="fetchUsageLog(true)" class="usage-log-select" aria-label="Model filter">
@@
-                <select x-model="usageLogProvider" `@change`="fetchUsageLog(true)" class="usage-log-select">
+                <select x-model="usageLogProvider" `@change`="fetchUsageLog(true)" class="usage-log-select" aria-label="Provider filter">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<select x-model="usageLogModel" @change="fetchUsageLog(true)" class="usage-log-select">
<option value="">All Models</option>
<template x-for="m in usageLogModelOptions()" :key="m">
<option :value="m" x-text="m"></option>
</template>
</select>
<select x-model="usageLogProvider" @change="fetchUsageLog(true)" class="usage-log-select">
<option value="">All Providers</option>
<template x-for="p in usageLogProviderOptions()" :key="p">
<option :value="p" x-text="p"></option>
</template>
<select x-model="usageLogModel" `@change`="fetchUsageLog(true)" class="usage-log-select" aria-label="Model filter">
<option value="">All Models</option>
<template x-for="m in usageLogModelOptions()" :key="m">
<option :value="m" x-text="m"></option>
</template>
</select>
<select x-model="usageLogProvider" `@change`="fetchUsageLog(true)" class="usage-log-select" aria-label="Provider filter">
<option value="">All Providers</option>
<template x-for="p in usageLogProviderOptions()" :key="p">
<option :value="p" x-text="p"></option>
</template>
</select>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/templates/page-usage.html` around lines 139 - 149,
The model/provider <select> controls (bound to usageLogModel and
usageLogProvider, class usage-log-select) lack accessible names; add aria-label
attributes to each select (e.g., aria-label="Model filter" on the usageLogModel
select and aria-label="Provider filter" on the usageLogProvider select) so
screen readers announce the control purpose consistently after a value is
chosen.

@SantiagoDePolonia SantiagoDePolonia merged commit 0553801 into main Apr 21, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant