Skip to content

feat(workflows): expose failover controls in execution plans#186

Merged
SantiagoDePolonia merged 11 commits intomainfrom
feature/workflows-hardening
Mar 30, 2026
Merged

feat(workflows): expose failover controls in execution plans#186
SantiagoDePolonia merged 11 commits intomainfrom
feature/workflows-hardening

Conversation

@SantiagoDePolonia
Copy link
Copy Markdown
Contributor

@SantiagoDePolonia SantiagoDePolonia commented Mar 29, 2026

Summary

  • add an allowlisted admin dashboard config endpoint that exposes FEATURE_FALLBACK_MODE
  • surface the execution-plan failover toggle in the dashboard only when the global runtime flag enables it and preserve explicit workflow fallback state
  • refine the settings timezone helper toggle with the compact rotating ? control and simplified helper copy styling

Test Plan

  • go test ./internal/app ./internal/admin ./internal/server ./internal/executionplans -v
  • node --test internal/admin/dashboard/static/js/modules/execution-plans.test.js internal/admin/dashboard/static/js/modules/execution-plans-layout.test.js internal/admin/dashboard/static/js/modules/dashboard-layout.test.js internal/admin/dashboard/static/js/modules/timezone-layout.test.js

Summary by CodeRabbit

  • New Features

    • Workflow editor: “New Workflow” CTA, Failover checkbox, live Preview panel, updated submit button with icons.
    • Shared UI components: auth-banner, pagination, helper-disclosure, reusable execution-pipeline chart; Audit Logs show model and pipeline previews; request/response audit panes.
  • Behavior

    • Runtime-driven feature visibility for cache/usage/audit/guardrails and conditional preview/submission of failover.
  • Style

    • Redesigned inline help toggle, updated pipeline node colors and submit UI sizing.
  • Documentation

    • Removed token-usage tracking plan document.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 29, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a dashboard runtime-config endpoint and wiring (FEATURE_FALLBACK_MODE), admin endpoints to fetch execution-plan views, frontend execution-plan UI/preview and templates, persists and propagates audit log cache type (exact/semantic) and marks audit entries on cache hits, removes a token-usage design doc, and expands tests across frontend and backend.

Changes

Cohort / File(s) Summary
Docs removal
docs/plans/token-usage-tracking.md
Removed the token-usage-tracking design document.
Admin handler & API routes
internal/admin/handler.go, internal/server/http.go
Added DashboardConfig endpoint, WithDashboardRuntimeConfig option, GetExecutionPlan handler, and route registrations for /admin/api/v1/dashboard/config and /admin/api/v1/execution-plans/:id.
App init & config derivation
internal/app/app.go, internal/app/app_test.go
Compute and pass normalized dashboard runtime config (fallback mode and feature flags) into admin handler; added helpers and tests for normalization.
Execution plans service/API
internal/executionplans/service.go
Added GetView(ctx, id) to load an execution-plan view by version ID and consistent nil-service handling.
Frontend execution-plan logic
internal/admin/dashboard/static/js/modules/execution-plans.js, ...execution-plans.test.js
Fetch runtime config, version caching/prefetch, scope hydration, features.fallback handling, guardrail parsing, runtime normalization (statusCode/responseSuccess/aiSuccess), chart helpers, and extensive tests.
Dashboard templates & UI
internal/admin/dashboard/templates/index.html, templates/*
Introduced reusable templates (auth-banner, pagination, helper-disclosure, execution-plan-chart, audit-pane), added preview/failover UI, submit button icon/label changes, and audit entry model display.
Dashboard CSS & layout
internal/admin/dashboard/static/css/dashboard.css
Updated inline-help toggle, icon rotation animation, execution-plan success/semantic styles, async layout tweaks, audit pill styling, and submit button styles.
Frontend layout/tests & CSS assertions
internal/admin/dashboard/static/js/modules/execution-plans-layout.test.js, timezone-layout.test.js, dashboard-layout.test.js
Tests updated to assert new templates, CSS rules, helper-disclosure usage, async layout exact offsets, and execution-plan chart wiring.
Dashboard static JS (audit fetch)
internal/admin/dashboard/static/js/modules/audit-list.js, internal/admin/dashboard/static/js/modules/audit-list.test.js
fetchAuditLog ensures entries array, conditionally prefetches execution-plan versions (errors logged), and added auditRequestPane/auditResponsePane helpers with tests.
Dashboard template parsing
internal/admin/dashboard/dashboard.go
Now parses all embedded templates via templates/*.html.
Auditlog model & enrichment
internal/auditlog/auditlog.go, internal/auditlog/middleware.go, internal/auditlog/stream_wrapper.go
Added CacheType field and constants (exact,semantic), normalizeCacheType, and EnrichEntryWithCacheType; stream wrapper copies ExecutionPlanVersionID and CacheType.
Auditlog readers & stores
internal/auditlog/reader_*.go, internal/auditlog/store_*.go
Persist/load cache_type across Mongo/Postgres/SQLite readers and stores; DB schema/migration and batched-insert logic updated.
Auditlog tests & store tests
internal/auditlog/*_test.go
Added/updated tests to validate cache_type preservation, JSON/BSON decoding, and SQL/SQLite insert/reader behavior.
Response cache integration & tests
internal/responsecache/simple.go, internal/responsecache/semantic.go, internal/responsecache/*.test.go
On exact/semantic cache hits, call EnrichEntryWithCacheType to mark audit entries; added tests verifying X-Cache headers and audit entry mutation.
Server & admin tests
internal/server/http_test.go, internal/admin/handler_test.go, internal/admin/handler_executionplans_test.go
Added tests asserting new /admin/api/v1/dashboard/config registration and payload, allowlist behavior, GetExecutionPlan behavior, and features.fallback presence/values in plan payloads.
Response cache tests
internal/responsecache/handle_request_test.go, semantic_test.go
New tests verify exact/semantic hits set X-Cache and mutate audit entry CacheType.

Sequence Diagram(s)

sequenceDiagram
    participant App as App Init
    participant Server as HTTP Server
    participant Admin as Admin Handler
    participant Frontend as Dashboard Frontend
    participant ResponseCache as Response Cache
    participant Audit as Audit subsystem

    App-->>Admin: WithDashboardRuntimeConfig(map of allowlisted flags)
    Server->>Admin: register GET /admin/api/v1/dashboard/config and /execution-plans/:id
    Frontend->>Server: GET /admin/api/v1/dashboard/config
    Server->>Admin: handle DashboardConfig request
    Admin-->>Server: return allowlisted runtime config JSON
    Server-->>Frontend: runtime config JSON (e.g., FEATURE_FALLBACK_MODE)
    Frontend->>Frontend: update UI (failover visibility, preview behavior)
    Frontend->>Server: user triggers execution/preview or requests plan version
    Server->>Audit: create LogEntry and attach to request context
    ResponseCache->>Audit: EnrichEntryWithCacheType("exact"/"semantic") on cache hit
    Server-->>Frontend: response (may include X-Cache header)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement

Poem

🐇 I nibbled through flags and templates bright,
Failover hops in when plans take flight,
Cache types whisper "exact" or "semantic" true,
Templates reused, tests growing too,
A rabbit cheers — code clean and light! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: exposing failover controls in execution plans. It accurately reflects the main feature addition documented across the changeset.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/workflows-hardening

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.

@SantiagoDePolonia SantiagoDePolonia marked this pull request as ready for review March 29, 2026 21:47
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: 2

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/js/modules/execution-plans.js (1)

20-31: ⚠️ Potential issue | 🟠 Major

Don't default hidden failover state to true for new workflows.

When FEATURE_FALLBACK_MODE is off (or the config fetch fails), the toggle is hidden, but new forms still initialize features.fallback to true and buildExecutionPlanRequest() persists that hidden value. That means workflows created while failover is unavailable can silently opt into failover later when the global mode is turned on. Only hydrated existing plans should carry hidden fallback values forward.

🩹 Suggested fix
             executionPlanForm: {
                 scope_provider: '',
                 scope_model: '',
                 name: '',
                 description: '',
                 features: {
                     cache: true,
                     audit: true,
                     usage: true,
                     guardrails: false,
-                    fallback: true
+                    fallback: false
                 },
                 guardrails: []
             },
 
             defaultExecutionPlanForm() {
+                const fallbackVisible = this.executionPlanFailoverVisible();
                 return {
                     scope_provider: '',
                     scope_model: '',
                     name: '',
                     description: '',
                     features: {
                         cache: true,
                         audit: true,
                         usage: true,
                         guardrails: false,
-                        fallback: true
+                        fallback: fallbackVisible
                     },
                     guardrails: []
                 };
             },

Also applies to: 35-49, 326-339

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

In `@internal/admin/dashboard/static/js/modules/execution-plans.js` around lines
20 - 31, The form is initializing features.fallback to true for all new
execution plans so hidden fallback toggles get persisted even when
FEATURE_FALLBACK_MODE is off; change the initialization in executionPlanForm
(and the other form defaults around the indicated blocks) to set
features.fallback = false for new plans, and update buildExecutionPlanRequest to
only carry a persisted fallback value when the form was hydrated from an
existing plan (e.g., detect hydration via a flag like isHydrated or by checking
if an existing plan object was loaded) so hidden fallback values are not
implicitly enabled for newly created workflows.
🤖 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 2238-2243: The 16×16 control (the rule with width: 16px; height:
16px; border-radius: 4px; border: 1px solid color-mix(...); background:
transparent) has a too-small hit area on touch — keep the 16px visual but add a
larger, invisible hit target with a pseudo-element (e.g., ::before or ::after)
that is position: absolute, centered with negative offsets or increased inset to
expand the clickable area, has transparent background, and pointer-events: auto;
ensure the original element remains visually 16×16 and retains its
border/background while the pseudo-element increases the interactive size.

In `@internal/app/app.go`:
- Around line 730-739: dashboardFallbackModeValue currently returns any
non-empty string from cfg.Fallback.DefaultMode, which can expose unsupported
modes in the UI; change it to validate the normalized mode against the allowed
values ("auto" and "manual") and return config.FallbackModeOff for any other
value. Update dashboardFallbackModeValue to normalize the mode (as it already
does), then if mode == "auto" return config.FallbackModeAuto, else if mode ==
"manual" return config.FallbackModeManual, otherwise return
config.FallbackModeOff so that it matches the behavior of
fallbackFeatureEnabledGlobally().

---

Outside diff comments:
In `@internal/admin/dashboard/static/js/modules/execution-plans.js`:
- Around line 20-31: The form is initializing features.fallback to true for all
new execution plans so hidden fallback toggles get persisted even when
FEATURE_FALLBACK_MODE is off; change the initialization in executionPlanForm
(and the other form defaults around the indicated blocks) to set
features.fallback = false for new plans, and update buildExecutionPlanRequest to
only carry a persisted fallback value when the form was hydrated from an
existing plan (e.g., detect hydration via a flag like isHydrated or by checking
if an existing plan object was loaded) so hidden fallback values are not
implicitly enabled for newly created workflows.
🪄 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: 5f2915da-3d0e-4e4e-9c73-a08c503c3442

📥 Commits

Reviewing files that changed from the base of the PR and between 3740a6c and 6161664.

📒 Files selected for processing (14)
  • docs/plans/token-usage-tracking.md
  • internal/admin/dashboard/static/css/dashboard.css
  • internal/admin/dashboard/static/js/modules/execution-plans-layout.test.js
  • internal/admin/dashboard/static/js/modules/execution-plans.js
  • internal/admin/dashboard/static/js/modules/execution-plans.test.js
  • internal/admin/dashboard/static/js/modules/timezone-layout.test.js
  • internal/admin/dashboard/templates/index.html
  • internal/admin/handler.go
  • internal/admin/handler_executionplans_test.go
  • internal/admin/handler_test.go
  • internal/app/app.go
  • internal/app/app_test.go
  • internal/server/http.go
  • internal/server/http_test.go
💤 Files with no reviewable changes (1)
  • docs/plans/token-usage-tracking.md

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: 4

🤖 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/audit-list.js`:
- Around line 36-38: The current flow awaits
this.prefetchAuditExecutionPlans(this.auditLog.entries) inside the outer
try/catch so any rejection clears this.auditLog; instead, split the prefetch
into its own try/catch: after successfully setting this.auditLog (and after
parse), check typeof this.prefetchAuditExecutionPlans === 'function' and then
call and await it inside a nested try/catch that catches and logs the error (or
handles it) without rethrowing, so failures in prefetchAuditExecutionPlans do
not reset or modify this.auditLog or affect the successful fetch/parse path.

In `@internal/admin/dashboard/static/js/modules/execution-plans.js`:
- Around line 201-239: executionPlanPreview currently calls
executionPlanSourceGuardrails which coerces blank/whitespace step strings into
0, causing previews to disagree with submit-time validation in
buildExecutionPlanRequest/validateExecutionPlanRequest; fix by reusing the same
submit-time parsing logic (or at minimum trim the step strings before Number()
conversion) inside executionPlanPreview (and the parallel hydration code paths)
so blank or whitespace-only step values remain invalid and preview/hydration
match buildExecutionPlanRequest/validateExecutionPlanRequest behavior.
- Around line 437-456: fetchExecutionPlanRuntimeConfig never uses the request
timeout/AbortController used by fetchExecutionPlansPage, so a hung
/admin/api/v1/dashboard/config fetch can stall refreshes; update
fetchExecutionPlanRuntimeConfig (and the similar block around lines 573-577) to
create an AbortController, pass its signal into fetch, start a timeout (matching
the workflow list timeout used by fetchExecutionPlansPage) that calls
controller.abort(), and clear the timeout on success/failure so the fetch will
fail fast and not block execution plan updates.
- Around line 855-883: The responseSuccess calculation in epRuntimeFromEntry
incorrectly treats only 200 as success; update responseSuccess (and consequently
aiSuccess) to consider any 2xx HTTP status as successful by checking that
statusCode is a finite number and 200 <= statusCode < 300 (ensure null/undefined
statusCode remains treated as non-success). Modify the expression that defines
responseSuccess inside epRuntimeFromEntry to use that range check so
201/204/etc. are classified as successes.
🪄 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: f343ad0b-8600-4a3b-b93b-a26132edec42

📥 Commits

Reviewing files that changed from the base of the PR and between 6161664 and 9bf2b4e.

📒 Files selected for processing (29)
  • internal/admin/dashboard/static/css/dashboard.css
  • internal/admin/dashboard/static/js/modules/audit-list.js
  • internal/admin/dashboard/static/js/modules/execution-plans-layout.test.js
  • internal/admin/dashboard/static/js/modules/execution-plans.js
  • internal/admin/dashboard/static/js/modules/execution-plans.test.js
  • internal/admin/dashboard/static/js/modules/timezone-layout.test.js
  • internal/admin/dashboard/templates/index.html
  • internal/admin/handler.go
  • internal/admin/handler_executionplans_test.go
  • internal/app/app.go
  • internal/app/app_test.go
  • internal/auditlog/auditlog.go
  • internal/auditlog/auditlog_test.go
  • internal/auditlog/middleware.go
  • internal/auditlog/reader_mongodb.go
  • internal/auditlog/reader_mongodb_test.go
  • internal/auditlog/reader_postgresql.go
  • internal/auditlog/reader_sqlite.go
  • internal/auditlog/store_postgresql.go
  • internal/auditlog/store_postgresql_test.go
  • internal/auditlog/store_sqlite.go
  • internal/auditlog/store_sqlite_test.go
  • internal/auditlog/stream_wrapper.go
  • internal/executionplans/service.go
  • internal/responsecache/handle_request_test.go
  • internal/responsecache/semantic.go
  • internal/responsecache/semantic_test.go
  • internal/responsecache/simple.go
  • internal/server/http.go

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: 3

Caution

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

⚠️ Outside diff range comments (2)
internal/admin/dashboard/static/js/modules/execution-plans.js (2)

267-279: ⚠️ Potential issue | 🟡 Minor

Keep preview and hydration aligned with integer-only guardrail steps.

executionPlanSourceGuardrails() now fixes blank values, but it still passes -1 and 1.5 because the filter only checks Number.isFinite. executionPlanPreview() and openExecutionPlanCreate() both reuse this helper, so the UI can still render guardrail steps that validateExecutionPlanRequest() rejects on submit.

Suggested fix
                 return raw
                     .map((step) => ({
                         ref: String(step && step.ref || '').trim(),
                         step: this.parseExecutionPlanGuardrailStep(step && step.step)
                     }))
-                    .filter((step) => Number.isFinite(step.step));
+                    .filter((step) => Number.isInteger(step.step) && step.step >= 0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/static/js/modules/execution-plans.js` around lines
267 - 279, The helper executionPlanSourceGuardrails currently filters guardrails
with Number.isFinite, which still allows non-integer and negative values (e.g.,
-1, 1.5); change the final filter to require integer-only, positive steps (e.g.,
replace .filter((step) => Number.isFinite(step.step)) with .filter((step) =>
Number.isInteger(step.step) && step.step > 0)) so executionPlanPreview and
openExecutionPlanCreate (which reuse this helper) will only render guardrails
that match validateExecutionPlanRequest’s integer-only constraints.

402-439: ⚠️ Potential issue | 🟠 Major

Don't preserve a hidden fallback bit after the user retargets the workflow scope.

includeFallback is currently keyed off executionPlanFormHydrated, so editing an existing workflow and then changing scope_provider/scope_model while FEATURE_FALLBACK_MODE is off will still POST the old hidden features.fallback value. Because the toggle and preview hint are both hidden in that mode, operators can unintentionally stamp inherited failover state onto a brand-new scope.

Suggested fix
                 const features = form.features || {};
-                const includeFallback = this.executionPlanFailoverVisible() || !!this.executionPlanFormHydrated;
+                const preserveHydratedFallback = !!this.executionPlanFormHydrated && this.executionPlanScopeMatches(
+                    { scope: this.executionPlanHydratedScope },
+                    { scope_provider: provider, scope_model: model }
+                );
+                const includeFallback = this.executionPlanFailoverVisible() || preserveHydratedFallback;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/static/js/modules/execution-plans.js` around lines
402 - 439, The current includeFallback logic preserves a hidden fallback flag
when an existing plan is edited and the scope is retargeted; update
buildExecutionPlanRequest so includeFallback is true only if the failover
controls are visible (executionPlanFailoverVisible()) OR the current form
explicitly contains a user-set features.fallback and the user has not retargeted
the scope (compare the current provider/model to the original
this.executionPlanForm.scope_provider/scope_model to detect retargeting);
reference buildExecutionPlanRequest, executionPlanFormHydrated,
executionPlanFailoverVisible, and features.fallback when making this change so
hidden fallback bits are not carried to a new scope.
🤖 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/execution-plans.js`:
- Around line 814-843: executionPlanAuditChart can be called with source ===
null which causes executionPlanChartModel to suppress audit/async nodes; modify
executionPlanChartModel (or callsite) so the audit path is forced for
audit-chart rendering: update executionPlanChartModel(source, runtime, options)
to respect an explicit options.forceAudit (and options.forceAsync if needed) or
have executionPlanAuditChart call executionPlanChartModel with { forceCache:
true, forceAudit: true, forceAsync: true }; ensure the computed properties
showAudit and showAsync use options.forceAudit/options.forceAsync or fall back
to epHasAudit(source)/epHasAsync(source) so the Audit Log node is always shown
for executionPlanAuditChart.

In `@internal/admin/dashboard/templates/audit-pane.html`:
- Line 5: The "Copy Body" button lacks an explicit button type which can cause
it to act as a submit inside forms; update the button element that calls
copyAuditJSON (the button with class "pagination-btn" and
`@click.prevent`="copyAuditJSON({{.}}.copyBody, $event)") to include type="button"
so it does not trigger form submission.

In `@internal/admin/dashboard/templates/pagination.html`:
- Around line 5-6: The pagination buttons currently lack an explicit type and
will default to type="submit"; update the two button elements (the ones with
class "pagination-btn" and click handlers `@click`="{{.}}PrevPage()" and
`@click`="{{.}}NextPage()") to include type="button" so they never trigger form
submission when rendered inside a <form>, keeping existing :disabled bindings
({{.}}.offset, {{.}}.limit, {{.}}.total) and click handler names unchanged.

---

Outside diff comments:
In `@internal/admin/dashboard/static/js/modules/execution-plans.js`:
- Around line 267-279: The helper executionPlanSourceGuardrails currently
filters guardrails with Number.isFinite, which still allows non-integer and
negative values (e.g., -1, 1.5); change the final filter to require
integer-only, positive steps (e.g., replace .filter((step) =>
Number.isFinite(step.step)) with .filter((step) => Number.isInteger(step.step)
&& step.step > 0)) so executionPlanPreview and openExecutionPlanCreate (which
reuse this helper) will only render guardrails that match
validateExecutionPlanRequest’s integer-only constraints.
- Around line 402-439: The current includeFallback logic preserves a hidden
fallback flag when an existing plan is edited and the scope is retargeted;
update buildExecutionPlanRequest so includeFallback is true only if the failover
controls are visible (executionPlanFailoverVisible()) OR the current form
explicitly contains a user-set features.fallback and the user has not retargeted
the scope (compare the current provider/model to the original
this.executionPlanForm.scope_provider/scope_model to detect retargeting);
reference buildExecutionPlanRequest, executionPlanFormHydrated,
executionPlanFailoverVisible, and features.fallback when making this change so
hidden fallback bits are not carried to a new scope.
🪄 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: 4514d7e6-6345-47ef-83e7-986b5b3d1366

📥 Commits

Reviewing files that changed from the base of the PR and between 9bf2b4e and 86075d6.

📒 Files selected for processing (15)
  • internal/admin/dashboard/dashboard.go
  • internal/admin/dashboard/static/css/dashboard.css
  • internal/admin/dashboard/static/js/modules/audit-list.js
  • internal/admin/dashboard/static/js/modules/audit-list.test.js
  • internal/admin/dashboard/static/js/modules/dashboard-layout.test.js
  • internal/admin/dashboard/static/js/modules/execution-plans-layout.test.js
  • internal/admin/dashboard/static/js/modules/execution-plans.js
  • internal/admin/dashboard/static/js/modules/execution-plans.test.js
  • internal/admin/dashboard/static/js/modules/timezone-layout.test.js
  • internal/admin/dashboard/templates/audit-pane.html
  • internal/admin/dashboard/templates/auth-banner.html
  • internal/admin/dashboard/templates/execution-plan-chart.html
  • internal/admin/dashboard/templates/helper-disclosure.html
  • internal/admin/dashboard/templates/index.html
  • internal/admin/dashboard/templates/pagination.html

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.

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/js/modules/execution-plans.js (1)

407-445: ⚠️ Potential issue | 🟠 Major

Preserve hidden fallback from the matched active workflow, not only hydrated forms.

executionPlanSubmitMode() already treats a fresh form as a save when the current scope matches an active workflow, but buildExecutionPlanRequest() only carries plan_payload.features.fallback across when executionPlanFormHydrated is true. With FEATURE_FALLBACK_MODE hidden, saving over an existing scope from a non-hydrated form drops fallback; on reload executionPlanSourceFeatures() interprets that omission as true, so an explicitly disabled fallback is silently re-enabled.

Suggested fix
                 const features = form.features || {};
-                const hydratedScope = this.executionPlanHydratedScope || {
-                    scope_provider: '',
-                    scope_model: ''
-                };
-                const sameHydratedScope = String(hydratedScope.scope_provider || '').trim() === provider
-                    && String(hydratedScope.scope_model || '').trim() === model;
+                const currentScope = {
+                    scope_provider: provider,
+                    scope_model: model
+                };
+                const activeScopePlan = (Array.isArray(this.executionPlans) ? this.executionPlans : [])
+                    .find((plan) => this.executionPlanScopeMatches(plan, currentScope)) || null;
                 const includeFallback = this.executionPlanFailoverVisible()
-                    || (!!this.executionPlanFormHydrated
-                        && sameHydratedScope
-                        && Object.prototype.hasOwnProperty.call(features, 'fallback'));
+                    || !!activeScopePlan;
@@
                 if (includeFallback) {
-                    payload.plan_payload.features.fallback = !!features.fallback;
+                    payload.plan_payload.features.fallback = this.executionPlanFailoverVisible()
+                        ? !!features.fallback
+                        : this.executionPlanSourceFeatures(activeScopePlan).fallback;
                 }

Please add a regression test for the non-hydrated save path too.

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

In `@internal/admin/dashboard/static/js/modules/execution-plans.js` around lines
407 - 445, buildExecutionPlanRequest currently only preserves
plan_payload.features.fallback when this.executionPlanFormHydrated is true,
which drops an explicitly disabled fallback when saving a fresh (non-hydrated)
form that matches the active workflow; update the includeFallback logic to also
preserve the existing fallback when the current scope matches
executionPlanHydratedScope (use the sameHydratedScope boolean and check
Object.prototype.hasOwnProperty.call(features, 'fallback')) even if
executionPlanFormHydrated is false, so payload.plan_payload.features.fallback is
set whenever the active workflow has a defined fallback; add a regression test
covering the non-hydrated save path (exercise executionPlanSubmitMode +
buildExecutionPlanRequest for a fresh form whose scope equals
executionPlanHydratedScope to assert fallback is preserved).
♻️ Duplicate comments (1)
internal/admin/dashboard/static/js/modules/execution-plans.js (1)

664-719: ⚠️ Potential issue | 🟠 Major

This repeats the earlier hung-fetch pattern on historical version lookups.

executionPlanVersionRequests[normalizedID] holds the in-flight promise until it settles. Without an abort timeout here, one stalled /admin/api/v1/execution-plans/:id request leaves that cache slot pinned forever, so every later prefetch for the same version ID reuses the same never-resolving promise and audit charts never recover.

Suggested fix
                 const request = (async () => {
+                    const controller = typeof AbortController === 'function' ? new AbortController() : null;
+                    const timeoutID = controller && typeof setTimeout === 'function'
+                        ? setTimeout(() => controller.abort(), 10000)
+                        : null;
                     try {
-                        const res = await fetch('/admin/api/v1/execution-plans/' + encodeURIComponent(normalizedID), {
-                            headers: this.headers()
-                        });
+                        const options = { headers: this.headers() };
+                        if (controller) {
+                            options.signal = controller.signal;
+                        }
+                        const res = await fetch('/admin/api/v1/execution-plans/' + encodeURIComponent(normalizedID), options);
@@
                     } catch (e) {
                         console.error('Failed to fetch workflow version:', e);
                         return null;
                     } finally {
+                        if (timeoutID !== null && typeof clearTimeout === 'function') {
+                            clearTimeout(timeoutID);
+                        }
                         if (this.executionPlanVersionRequests) {
                             delete this.executionPlanVersionRequests[normalizedID];
                         }
                     }
                 })();

Please mirror the existing abort/clear-timeout coverage here as well.

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

In `@internal/admin/dashboard/static/js/modules/execution-plans.js` around lines
664 - 719, The fetchExecutionPlanVersion function leaves
executionPlanVersionRequests[normalizedID] pinned if a fetch stalls; mirror the
abort/clear-timeout pattern used elsewhere by creating an AbortController, pass
its signal into fetch('/admin/api/v1/execution-plans/' +
encodeURIComponent(normalizedID), { headers: this.headers(), signal }), start a
timeout that calls controller.abort() and deletes
executionPlanVersionRequests[normalizedID], clear that timeout when the request
completes successfully or errors, and ensure the existing finally block still
deletes executionPlanVersionRequests[normalizedID]; keep using
cacheExecutionPlanVersion, cacheMissingExecutionPlanVersion and
handleFetchResponse exactly as currently used for success/404/401/error
branches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@internal/admin/dashboard/static/js/modules/execution-plans.js`:
- Around line 407-445: buildExecutionPlanRequest currently only preserves
plan_payload.features.fallback when this.executionPlanFormHydrated is true,
which drops an explicitly disabled fallback when saving a fresh (non-hydrated)
form that matches the active workflow; update the includeFallback logic to also
preserve the existing fallback when the current scope matches
executionPlanHydratedScope (use the sameHydratedScope boolean and check
Object.prototype.hasOwnProperty.call(features, 'fallback')) even if
executionPlanFormHydrated is false, so payload.plan_payload.features.fallback is
set whenever the active workflow has a defined fallback; add a regression test
covering the non-hydrated save path (exercise executionPlanSubmitMode +
buildExecutionPlanRequest for a fresh form whose scope equals
executionPlanHydratedScope to assert fallback is preserved).

---

Duplicate comments:
In `@internal/admin/dashboard/static/js/modules/execution-plans.js`:
- Around line 664-719: The fetchExecutionPlanVersion function leaves
executionPlanVersionRequests[normalizedID] pinned if a fetch stalls; mirror the
abort/clear-timeout pattern used elsewhere by creating an AbortController, pass
its signal into fetch('/admin/api/v1/execution-plans/' +
encodeURIComponent(normalizedID), { headers: this.headers(), signal }), start a
timeout that calls controller.abort() and deletes
executionPlanVersionRequests[normalizedID], clear that timeout when the request
completes successfully or errors, and ensure the existing finally block still
deletes executionPlanVersionRequests[normalizedID]; keep using
cacheExecutionPlanVersion, cacheMissingExecutionPlanVersion and
handleFetchResponse exactly as currently used for success/404/401/error
branches.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: da6970df-b94c-4f12-adac-7c42b1bac6d7

📥 Commits

Reviewing files that changed from the base of the PR and between 86075d6 and d4c5343.

📒 Files selected for processing (5)
  • internal/admin/dashboard/static/js/modules/dashboard-layout.test.js
  • internal/admin/dashboard/static/js/modules/execution-plans.js
  • internal/admin/dashboard/static/js/modules/execution-plans.test.js
  • internal/admin/dashboard/templates/audit-pane.html
  • internal/admin/dashboard/templates/pagination.html

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

🤖 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/execution-plans.js`:
- Around line 764-807: fetchExecutionPlanVersion is missing an
AbortController/timeout and can hang; modify the async request block in
fetchExecutionPlanVersion to create an AbortController, pass its signal into
fetch, and set a 10_000 ms setTimeout that calls controller.abort(); clear the
timeout when fetch completes (in try/catch/finally) to avoid leaks; ensure the
abort is handled similarly to other fetches (call handleFetchResponse for
non-ok/401/404 as before or treat abort as null) and keep the existing cleanup
that deletes this.executionPlanVersionRequests[normalizedID].

In `@internal/admin/handler.go`:
- Around line 27-33: The handler currently exposes runtimeConfig as
map[string]string on the handler struct which makes the /dashboard/config
response shape open-ended; replace that with a named, strongly-typed response
struct (e.g., DashboardConfigResponse with the six explicit fields) and update
the handler method that serves /dashboard/config to populate and return that
struct, keeping any internal allowlist/filtering logic inside the handler (do
not expose the map). Update other usages of runtimeConfig in the file to
construct or read the typed struct as needed and apply the same change where
runtimeConfig is used in the other referenced blocks (the other handler methods
around the same file) so serialization, docs, and type safety are preserved.

In `@internal/app/app_test.go`:
- Around line 79-85: The test builds a config where ResponseCacheConfig.Simple
is assigned a value but that field is declared as a pointer
(*config.SimpleCacheConfig); change the composite literal to provide a pointer
by using &config.SimpleCacheConfig{...} for the Simple field (inside the Cache:
config.CacheConfig block), keeping the nested Redis:
&config.RedisResponseConfig{ URL: "redis://localhost:6379" } as-is so the types
match and the test compiles.

In `@internal/app/app.go`:
- Around line 724-727: dashboardFallbackModeValue currently reads only
cfg.Fallback.DefaultMode so the dashboard can show "off" even when per-model
overrides enable fallback; update
dashboardRuntimeConfig/dashboardFallbackModeValue to compute visibility using
the same logic as fallbackFeatureEnabledGlobally (i.e., treat any per-model
override of manual/auto as enabling fallback) rather than only DefaultMode, and
apply the same change to the other uses of dashboardFallbackModeValue in the
surrounding block (the code noted around dashboardRuntimeConfig and the related
742-755 region) so the dashboard visibility matches the compiled execution plan
behavior.
- Around line 789-790: The function simpleResponseCacheConfiguredFromResponse
dereferences cfg.Simple without guarding against nil; update it to first check
cfg.Simple != nil (and still check cfg.Simple.Redis != nil) before accessing
Redis.URL so deployments that omit the simple-cache block won't panic—i.e.,
change the conditional in simpleResponseCacheConfiguredFromResponse to ensure
cfg.Simple and cfg.Simple.Redis are non-nil and that Redis.URL is non-empty (use
strings.TrimSpace) so responseCacheConfigured() can safely call it.
🪄 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: ef23a81e-d04a-4597-9e16-4f8e9afb1d4f

📥 Commits

Reviewing files that changed from the base of the PR and between d4c5343 and 7543799.

📒 Files selected for processing (8)
  • internal/admin/dashboard/static/js/modules/execution-plans-layout.test.js
  • internal/admin/dashboard/static/js/modules/execution-plans.js
  • internal/admin/dashboard/static/js/modules/execution-plans.test.js
  • internal/admin/dashboard/templates/index.html
  • internal/admin/handler.go
  • internal/admin/handler_test.go
  • internal/app/app.go
  • internal/app/app_test.go

Comment on lines +79 to +85
Cache: config.CacheConfig{
Response: config.ResponseCacheConfig{
Simple: config.SimpleCacheConfig{
Redis: &config.RedisResponseConfig{
URL: "redis://localhost:6379",
},
},
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 | 🔴 Critical

Use a pointer for ResponseCacheConfig.Simple in this test.

config.ResponseCacheConfig.Simple is defined as *config.SimpleCacheConfig, so the composite literal at Lines 81-85 will not compile as a value.

🛠️ Proposed fix
-				Simple: config.SimpleCacheConfig{
+				Simple: &config.SimpleCacheConfig{
 					Redis: &config.RedisResponseConfig{
 						URL: "redis://localhost:6379",
 					},
 				},
📝 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
Cache: config.CacheConfig{
Response: config.ResponseCacheConfig{
Simple: config.SimpleCacheConfig{
Redis: &config.RedisResponseConfig{
URL: "redis://localhost:6379",
},
},
Cache: config.CacheConfig{
Response: config.ResponseCacheConfig{
Simple: &config.SimpleCacheConfig{
Redis: &config.RedisResponseConfig{
URL: "redis://localhost:6379",
},
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/app/app_test.go` around lines 79 - 85, The test builds a config
where ResponseCacheConfig.Simple is assigned a value but that field is declared
as a pointer (*config.SimpleCacheConfig); change the composite literal to
provide a pointer by using &config.SimpleCacheConfig{...} for the Simple field
(inside the Cache: config.CacheConfig block), keeping the nested Redis:
&config.RedisResponseConfig{ URL: "redis://localhost:6379" } as-is so the types
match and the test compiles.

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