Skip to content

[codex] Polish dashboard UI and local dev flow - #204

Open
andrewm4894 wants to merge 4 commits into
mainfrom
codex/dashboard-ui-polish
Open

[codex] Polish dashboard UI and local dev flow#204
andrewm4894 wants to merge 4 commits into
mainfrom
codex/dashboard-ui-polish

Conversation

@andrewm4894

@andrewm4894 andrewm4894 commented Apr 18, 2026

Copy link
Copy Markdown
Owner

What changed

  • refreshed the dashboard homepage, batch controls, batch cards, and shared presentation helpers to give the FastHTML/MonsterUI UI a more intentional visual system
  • modernized the chart layer with cleaner Plotly styling, better anomaly markers, improved compact chart defaults, and more polished expanded chart surfaces
  • rebuilt the anomaly list into a responsive card layout with roomier sparklines, clearer review actions, and better pagination/context copy
  • fixed direct-navigation and HTMX interaction issues across batch and anomaly routes, including the Home button regression from direct page loads
  • updated the local dashboard dev workflow and dependency floors for current FastHTML and MonsterUI usage, including the dashboard kill target fix

Why

The dashboard was working, but it still felt cramped and overly utilitarian in several key flows:

  • the homepage and batch views felt visually uneven
  • charts still read like basic embedded Plotly output
  • the anomaly page was too tight and table-like for a review workflow
  • direct batch/anomaly URLs broke toolbar navigation because the HTMX target container was missing on full-page responses

Impact

  • the dashboard feels more cohesive and easier to scan across homepage, batch, and anomaly views
  • anomaly review is more usable on both direct links and in normal in-app navigation
  • local dashboard development is easier to run and stop reliably

Root cause

The main functional regression came from mixing HTMX fragment responses with full-page responses on batch/anomaly routes. Direct page loads were not consistently rendering #main-content, so toolbar actions targeting that container failed with htmx:targetError.

The UI issues were largely structural: dense table layouts, cramped chart shells, and default Plotly chrome dominated the presentation.

Validation

  • venv/bin/python -m compileall dashboard
  • curl -sSf http://127.0.0.1:5003/health
  • curl -sSf http://127.0.0.1:5003/
  • curl -sSf http://127.0.0.1:5003/batch/currency
  • curl -sSf http://127.0.0.1:5003/batch/netdata/anomalies
  • browser smoke checks for homepage, batch pages, anomaly list, expanded charts, and Home navigation from a direct anomaly URL

Summary by CodeRabbit

Release Notes

  • New Features

    • Added overview cards displaying summary metrics across batches
    • Enhanced chart interactions with double-click reset and unified hover tooltips
    • Improved empty state messaging for unconfigured batches
    • Refined search and filtering UI with clearer intent
  • Style

    • Redesigned dashboard layout with improved spacing and responsive behavior
    • Updated chart styling with better marker visibility and visual hierarchy
    • Refreshed settings labels for clarity
    • Enhanced dark mode support across all dashboard elements
  • Chores

    • Updated framework and UI library dependencies

@coderabbitai

coderabbitai Bot commented Apr 18, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@andrewm4894 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 11 minutes and 18 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 11 minutes and 18 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fff91282-d2ac-447d-afc8-1cef0310043d

📥 Commits

Reviewing files that changed from the base of the PR and between 823ba11 and 8815d73.

📒 Files selected for processing (12)
  • dashboard/app.py
  • dashboard/charts.py
  • dashboard/components/batch.py
  • dashboard/components/common.py
  • dashboard/components/header.py
  • dashboard/components/settings.py
  • dashboard/components/toolbar.py
  • dashboard/routes/index.py
  • dashboard/routes/toggles.py
  • dashboard/static/styles.css
  • requirements-dashboard.txt
  • requirements.txt
📝 Walkthrough

Walkthrough

The PR refactors the Anomstack dashboard with enhanced chart rendering (line smoothing, area fills, spike lines, dynamic markers), reorganized component layouts featuring pill-based summaries and stat blocks, new presentation helper functions for metric filtering and formatting, redesigned anomaly list using card-based layout instead of tables, extended CSS styling with responsive behavior, and tightened dependency version constraints.

Changes

Cohort / File(s) Summary
Build & Dependencies
Makefile, requirements.txt, requirements-dashboard.txt
Updated Makefile to add seed command targets to .PHONY and rewrote kill-dashboardd to discover/deduplicate PIDs via pgrep and port lookups with conditional killing. Tightened python-fasthtml and MonsterUI version constraints to >=0.13.4 and >=1.0.46 respectively.
Theme & App Configuration
dashboard/app.py
Updated fast_app header/theme from Theme.blue.headers() to explicit theme config with mode="auto", radii, shadows, and font settings. Changed stylesheet path to absolute URL and added title="Anomstack" and body class configuration.
Chart Rendering Engine
dashboard/charts.py
Major overhaul: added doubleClick: "reset" to Plotly config, introduced dynamic line shaping/smoothing via ChartStyle.get_line_shape(), added conditional area fill (fill="tozeroy") when metric values are non-negative, implemented spike lines with themed hover labels, redesigned condition markers as diamond/star shapes with themed styling, added "latest value" halo markers, and updated color palette with new keys (background_solid, muted_text, border, primary_fill, primary_glow).
UI Components
dashboard/components/batch.py, dashboard/components/common.py, dashboard/components/toolbar.py
Refactored batch card to use stat-block layout with formatted metric counts and integer coercion; restructured create_controls to feature header section and summary pills via new create_controls_summary(); enhanced toolbar buttons with responsive text labels and aria_label attributes.
Search & Settings Components
dashboard/components/search.py, dashboard/components/settings.py
Updated search placeholder to "Filter metrics by name", removed inline console logging, and adjusted input CSS classes; revised settings dropdown labels to sentence-case phrasing (e.g., "Use dark mode" instead of "dark mode").
Presentation Helpers
dashboard/presentation.py
Added new module with utilities: format_batch_name() (title-case conversion), format_metric_count() (metric pluralization), get_filtered_metric_stats() (case-insensitive metric filtering), and summarize_batches() (aggregated overview stats).
Batch View & Routes
dashboard/routes/batch.py
Updated route signatures to accept Request parameter for HTMX detection; integrated get_filtered_metric_stats() for metric filtering; replaced table-based anomaly layout with card-based list design; added responsive grid class computation; introduced compact anomaly formatting helpers; adjusted chart placeholders and grid styling.
Index & Overview
dashboard/routes/index.py
Split batch categorization into active and empty batches; added create_overview_cards() for top-level summary grid; refactored dark-mode script to toggle documentElement.dark and body.dark-mode classes; updated /refresh-all return behavior.
Search & Toggle Routes
dashboard/routes/search.py, dashboard/routes/toggles.py
Replaced regex-based filtering with get_filtered_metric_stats() calls; consolidated empty-state rendering with controls summary; added _controls_summary() helper with hx_swap_oob; updated all toggle routes to accept Request parameter and reworked theme toggle to persist mode in localStorage under __FRANKEN__.
State & Styling
dashboard/state.py, dashboard/static/styles.css
Changed show_markers default from True to False; added 578 lines of CSS including dashboard shell padding, hero/card layouts, pill components, batch card styling with hover transitions, chart container/figure positioning (Plotly modebar tweaks), anomaly list UI with typed badges, and responsive media queries (≤1024px, ≤768px) with dark-mode overrides.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Route as Batch Route<br/>(get_batch_view)
    participant Presenter as Presentation<br/>(get_filtered_metric_stats)
    participant ChartMgr as Chart Manager<br/>(plot_time_series)
    participant AppState as App State
    
    Client->>Route: Request batch view<br/>(batch_name, request)
    Route->>AppState: Read search_term,<br/>stats_cache
    Route->>Presenter: get_filtered_metric_stats<br/>(batch_name)
    Presenter->>Presenter: Filter metrics by<br/>search_term (case-insensitive)
    Presenter-->>Route: Return filtered metrics<br/>with original indices
    Route->>AppState: Read chart config<br/>(small_charts, line_width, etc.)
    Route->>ChartMgr: plot_time_series<br/>(metric_df, config)
    ChartMgr->>ChartMgr: Apply dynamic line shape<br/>& smoothing
    ChartMgr->>ChartMgr: Add area fill<br/>(if min >= 0)
    ChartMgr->>ChartMgr: Add spike lines<br/>& themed markers
    ChartMgr->>ChartMgr: Render latest value<br/>halo marker
    ChartMgr-->>Route: Return styled chart
    Route->>Route: Render responsive grid<br/>+ stat blocks
    Route-->>Client: Return batch view<br/>(HTMX or full-page)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 Hop, hop, the charts now gleam,
With flowing lines and fills so keen,
Filtered metrics dance with grace,
Pills and cards in their rightful place,
Dark modes toggle, sparklines shine—
A dashboard redesign so fine!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[codex] Polish dashboard UI and local dev flow' clearly and concisely summarizes the main changes: UI/UX improvements to the dashboard and enhancements to the local development workflow.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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 codex/dashboard-ui-polish

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.

@andrewm4894
andrewm4894 marked this pull request as ready for review April 18, 2026 21:32

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 8

Caution

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

⚠️ Outside diff range comments (2)
dashboard/routes/batch.py (2)

405-412: ⚠️ Potential issue | 🟠 Major

URL-encode anomaly path parameters before building HTMX URLs.

metric_name and timestamp are interpolated directly into path segments. Metrics containing /, ?, #, %, or similar reserved characters can break feedback/expanded-chart routes. Precompute encoded values and use them in all anomaly action URLs; wrapping the long attributes also keeps these changed Python lines within Ruff’s 100-character limit. As per coding guidelines, **/*.py: Use Ruff for linting with a maximum line length of 100 characters for Python code.

🐛 Proposed fix
+from urllib.parse import quote
+
 from fasthtml.common import H4, Div, P, Request, Safe, Script, Title
         feedback_key = f"{batch_name}-{safe_metric}-{safe_timestamp}"
+        url_metric = quote(str(metric_name), safe="")
+        url_timestamp = quote(str(timestamp), safe="")
@@
-                                    hx_post=f"/batch/{batch_name}/anomaly/{metric_name}/{timestamp}/thumbs-up",
+                                    hx_post=(
+                                        f"/batch/{batch_name}/anomaly/"
+                                        f"{url_metric}/{url_timestamp}/thumbs-up"
+                                    ),
@@
-                                    hx_post=f"/batch/{batch_name}/anomaly/{metric_name}/{timestamp}/thumbs-down",
+                                    hx_post=(
+                                        f"/batch/{batch_name}/anomaly/"
+                                        f"{url_metric}/{url_timestamp}/thumbs-down"
+                                    ),
@@
-                        hx_get=f"/batch/{batch_name}/anomaly/{metric_name}/{timestamp}/expanded",
+                        hx_get=(
+                            f"/batch/{batch_name}/anomaly/"
+                            f"{url_metric}/{url_timestamp}/expanded"
+                        ),

Apply the same url_metric / url_timestamp substitution to the modal feedback buttons in this loop as well.

Also applies to: 479-498, 535-551, 571-575

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

In `@dashboard/routes/batch.py` around lines 405 - 412, The code builds
feedback_key and HTMX URLs by manually replacing a few characters
(safe_metric/safe_timestamp) which doesn't properly handle reserved URL
characters and can break routes; replace that logic by percent-URL-encoding
metric_name and timestamp into url_metric and url_timestamp (use these encoded
values when composing feedback_key and every anomaly action/expanded-chart/modal
feedback URL), update all occurrences noted (around safe_metric/safe_timestamp,
feedback_key, and the modal feedback buttons) to use url_metric/url_timestamp,
and wrap long attribute strings to respect Ruff's 100-character line-length
limit.

607-633: ⚠️ Potential issue | 🟠 Major

Add hx_swap="outerHTML" to pagination buttons to prevent nesting duplicate #anomaly-list containers.

The pagination buttons target #anomaly-list without an explicit swap directive. HTMX defaults to innerHTML, inserting the response content inside the target element. Since the handler returns a wrapper with id="anomaly-list", each pagination click nests a duplicate container. Use outerHTML to replace the entire target element instead.

Fix
             Button(
                 UkIcon("chevron-left"),
                 hx_get=f"/batch/{batch_name}/anomalies?page={page-1}",
                 hx_target="#anomaly-list",
+                hx_swap="outerHTML",
                 cls=ButtonT.secondary,
                 disabled=page <= 1,
             ),
@@
             Button(
                 UkIcon("chevron-right"),
                 hx_get=f"/batch/{batch_name}/anomalies?page={page+1}",
                 hx_target="#anomaly-list",
+                hx_swap="outerHTML",
                 cls=ButtonT.secondary,
                 disabled=page >= total_pages,
             ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dashboard/routes/batch.py` around lines 607 - 633, The pagination Button
elements that build the previous/next controls (the Button with
hx_get=f"/batch/{batch_name}/anomalies?page={page-1}" and the Button with
hx_get=f"/batch/{batch_name}/anomalies?page={page+1}") currently rely on HTMX's
default innerHTML swap and cause nested duplicate `#anomaly-list` containers; add
hx_swap="outerHTML" to both Button instances so the returned wrapper with
id="anomaly-list" replaces the entire target element instead of being inserted
inside it.
🧹 Nitpick comments (3)
dashboard/components/settings.py (2)

69-69: Line-width toggle label logic.

line_width != 2 → "Use normal lines", else "Use narrow lines". This flips correctly only if the two width states are "2" and "not-2" (e.g., narrow=1 or 3). If there are ever more than two width presets, the label will be misleading. Fine for the current binary toggle; just flagging for future extension.

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

In `@dashboard/components/settings.py` at line 69, The label logic for the
line-width toggle currently uses a loose check (app.state.line_width != 2) which
assumes only two presets; update it to check explicitly against the narrow
preset (e.g., compare app.state.line_width == NARROW_LINE_WIDTH or an
enum/constant) or replace with a small mapping/dictionary from known line_width
values to labels so the label remains correct if more presets are added; locate
the label generation referencing app.state.line_width and change the conditional
to an explicit equality against the narrow constant or use a mapping of
width→label.

47-73: Label readability: action vs. state wording is inconsistent.

Most toggles phrase the label as the action the click will perform (e.g., when small_charts=True, label says "Use spacious charts"), but the markers/legend entries phrase the label as the current state after the action (when show_markers=False, label says "Show markers" — reads like the current state, although it is also the action). That's fine, but the columns toggle reads awkwardly: when two_columns=True, label is "Use one column"; when False, "Use two columns" — consistent with action-phrasing. Consider unifying wording across all toggles (all action-phrased) for a cleaner UX. Minor nit.

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

In `@dashboard/components/settings.py` around lines 47 - 73, The labels in the
buttons list should be consistently action-oriented; update the entries in the
buttons array so each label clearly states the action the click will perform
(use verbs), e.g. keep "Use spacious charts"/"Use compact charts" for
app.state.small_charts, keep "Hide markers"/"Show markers" for
app.state.show_markers and "Hide legend"/"Show legend" for
app.state.show_legend, but change the columns entry (app.state.two_columns,
toggle-columns) to a consistent verb form such as "Switch to two columns" when
two_columns is False and "Switch to one column" when True (and similarly ensure
the line width entry for app.state.line_width uses a matching action-phrase).
dashboard/static/styles.css (1)

435-447: Respect reduced-motion preferences for hover animations.

These new transform/transition effects are polished, but users with reduced-motion enabled will still get card/button movement. Consider disabling the new motion in a prefers-reduced-motion media query.

♿ Proposed reduced-motion guard
 .anomaly-row-card:hover {
     transform: translateY(-1px);
     border-color: rgba(37, 99, 235, 0.24);
     box-shadow: 0 16px 32px rgba(15, 23, 42, 0.06);
 }
+
+@media (prefers-reduced-motion: reduce) {
+    .batch-card,
+    .chart-expand-btn,
+    .anomaly-row-card {
+        transition: none;
+    }
+
+    .batch-card:hover,
+    .chart-expand-btn:hover,
+    .anomaly-row-card:hover {
+        transform: none;
+    }
+}

Also applies to: 591-619, 635-647

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

In `@dashboard/static/styles.css` around lines 435 - 447, The hover motion on
.batch-card (and its related selectors) ignores users' reduced-motion
preference; wrap the motion styles in a prefers-reduced-motion media query and
disable animations there by removing transitions and transforms for .batch-card
and .batch-card:hover (e.g., set transition: none and reset
transform/box-shadow/border-color) so users with prefers-reduced-motion: reduce
do not see card movement; apply the same change to the other mentioned blocks
(lines referencing .batch-card hover variants at the other ranges).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@dashboard/charts.py`:
- Line 648: The "grid" dict entry currently uses a redundant ternary that
returns the same color for both branches ("grid": "rgba(148,163,184,0.12)" if
dark_mode else "rgba(148,163,184,0.12)"); replace this with a single direct
assignment ("grid": "rgba(148,163,184,0.12)") and remove the unnecessary
conditional expression (locate the "grid" key in the chart/theme dict where
dark_mode is referenced).

In `@dashboard/routes/batch.py`:
- Around line 320-325: Clamp the per_page parameter to a safe range before any
pagination math in get_anomaly_list: validate and coerce per_page to an integer
>= 1 (and optionally capped to a sensible max like 100) immediately on entry,
then use the clamped value for division and slice calculations to avoid
division-by-zero and invalid slices; apply the same change to the other
pagination logic in this module that performs page/per_page math so all
pagination sites use the validated/clamped per_page value.

In `@dashboard/routes/index.py`:
- Around line 172-182: The external link A component that wraps
DivLAligned/UkIcon/P and uses target="_blank" should also include rel="noopener
noreferrer" to prevent window.opener access; update the A(...) invocation (the
component instance using ButtonT.secondary, uk_tooltip and target="_blank") to
add rel="noopener noreferrer".
- Line 13: The long import line and the long list/comprehension in the
empty-batch section exceed Ruff's 100-character limit; split the import from
monsterui.all across multiple lines (one symbol per line or grouped logically)
and wrap the long string/list/comprehension around the empty-batch section so no
line exceeds 100 chars; update the import that includes Button, ButtonT, Card,
Container, DivFullySpaced, DivLAligned, Grid, H3, Section, Subtitle,
TextPresets, UkIcon and reformat the batch construction at/around the previous
line ~130 to reuse the existing batch formatter style used elsewhere in the
file.
- Around line 166-168: The HTMX callers (the toolbar button using
hx_post="/refresh-all" and the index route invoked from toolbar.py) are swapping
innerHTML into an element that already contains a Div named main_content,
causing nested duplicate id="main-content" elements; fix by either changing the
caller to use hx_swap="outerHTML" or (preferred) modify the refresh_all and
index handlers so when the request is an HTMX request (check HX-Request header)
they return only the inner content instead of the wrapper Div with
id="main-content" — update the functions referenced as refresh_all and index to
detect request.headers.get("HX-Request") and return the unwrapped content (e.g.,
the inner children or template fragment) while keeping the full Div
(main_content) for normal non-HTMX requests; alternatively, if you choose the
caller-side fix, add hx_swap="outerHTML" to the toolbar/button that posts to
/refresh-all.

In `@dashboard/routes/toggles.py`:
- Around line 63-71: The inline Script that parses localStorage["__FRANKEN__"]
can throw and prevent the theme toggle; update the JS payload constructed in
Script(...) to guard JSON.parse with a try/catch (e.g., attempt parse, on
failure set franken = {}), then always set franken.mode and call
localStorage.setItem and the document.documentElement/document.body class
toggles (the existing document.documentElement.classList.toggle and
document.body.classList.toggle lines) so the DOM classes are applied even if
localStorage is corrupt; keep references to "__FRANKEN__" and the use of
app.state.dark_mode unchanged.

In `@requirements-dashboard.txt`:
- Line 11: Replace the incorrect package constraint "MonsterUI>=1.0.46" with
"MonsterUI>=1.0.45" (i.e., change MonsterUI>=1.0.46 → MonsterUI>=1.0.45) in the
current requirements file and make the identical edit in the other requirements
file that contains the same entry so both files use MonsterUI>=1.0.45.

In `@requirements.txt`:
- Line 19: The requirements.txt lists non-existent PyPI versions; update the two
package specifiers to published versions or ensure those versions are published
before merging: change MonsterUI>=1.0.46 to a published constraint such as
MonsterUI==1.0.45 (or another valid published version) and change
python-fasthtml>=0.13.4 to python-fasthtml==0.13.3 (or another valid published
version); alternatively, if the newer versions are required, publish MonsterUI
1.0.46 and python-fasthtml 0.13.4 to PyPI and then keep the current range
constraints.

---

Outside diff comments:
In `@dashboard/routes/batch.py`:
- Around line 405-412: The code builds feedback_key and HTMX URLs by manually
replacing a few characters (safe_metric/safe_timestamp) which doesn't properly
handle reserved URL characters and can break routes; replace that logic by
percent-URL-encoding metric_name and timestamp into url_metric and url_timestamp
(use these encoded values when composing feedback_key and every anomaly
action/expanded-chart/modal feedback URL), update all occurrences noted (around
safe_metric/safe_timestamp, feedback_key, and the modal feedback buttons) to use
url_metric/url_timestamp, and wrap long attribute strings to respect Ruff's
100-character line-length limit.
- Around line 607-633: The pagination Button elements that build the
previous/next controls (the Button with
hx_get=f"/batch/{batch_name}/anomalies?page={page-1}" and the Button with
hx_get=f"/batch/{batch_name}/anomalies?page={page+1}") currently rely on HTMX's
default innerHTML swap and cause nested duplicate `#anomaly-list` containers; add
hx_swap="outerHTML" to both Button instances so the returned wrapper with
id="anomaly-list" replaces the entire target element instead of being inserted
inside it.

---

Nitpick comments:
In `@dashboard/components/settings.py`:
- Line 69: The label logic for the line-width toggle currently uses a loose
check (app.state.line_width != 2) which assumes only two presets; update it to
check explicitly against the narrow preset (e.g., compare app.state.line_width
== NARROW_LINE_WIDTH or an enum/constant) or replace with a small
mapping/dictionary from known line_width values to labels so the label remains
correct if more presets are added; locate the label generation referencing
app.state.line_width and change the conditional to an explicit equality against
the narrow constant or use a mapping of width→label.
- Around line 47-73: The labels in the buttons list should be consistently
action-oriented; update the entries in the buttons array so each label clearly
states the action the click will perform (use verbs), e.g. keep "Use spacious
charts"/"Use compact charts" for app.state.small_charts, keep "Hide
markers"/"Show markers" for app.state.show_markers and "Hide legend"/"Show
legend" for app.state.show_legend, but change the columns entry
(app.state.two_columns, toggle-columns) to a consistent verb form such as
"Switch to two columns" when two_columns is False and "Switch to one column"
when True (and similarly ensure the line width entry for app.state.line_width
uses a matching action-phrase).

In `@dashboard/static/styles.css`:
- Around line 435-447: The hover motion on .batch-card (and its related
selectors) ignores users' reduced-motion preference; wrap the motion styles in a
prefers-reduced-motion media query and disable animations there by removing
transitions and transforms for .batch-card and .batch-card:hover (e.g., set
transition: none and reset transform/box-shadow/border-color) so users with
prefers-reduced-motion: reduce do not see card movement; apply the same change
to the other mentioned blocks (lines referencing .batch-card hover variants at
the other ranges).
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 74253d30-be0a-40c8-9ea8-15212661a052

📥 Commits

Reviewing files that changed from the base of the PR and between 08d38ff and 823ba11.

📒 Files selected for processing (17)
  • Makefile
  • dashboard/app.py
  • dashboard/charts.py
  • dashboard/components/batch.py
  • dashboard/components/common.py
  • dashboard/components/search.py
  • dashboard/components/settings.py
  • dashboard/components/toolbar.py
  • dashboard/presentation.py
  • dashboard/routes/batch.py
  • dashboard/routes/index.py
  • dashboard/routes/search.py
  • dashboard/routes/toggles.py
  • dashboard/state.py
  • dashboard/static/styles.css
  • requirements-dashboard.txt
  • requirements.txt

Comment thread dashboard/charts.py
"background_solid": "#111827" if dark_mode else "#ffffff",
"text": "#dbe4f0" if dark_mode else "#334155",
"muted_text": "#94a3b8" if dark_mode else "#64748b",
"grid": "rgba(148,163,184,0.12)" if dark_mode else "rgba(148,163,184,0.12)",

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

Collapse the redundant color conditional.

Ruff flags this as RUF034 because both branches return the same grid color.

🧹 Proposed Ruff fix
-            "grid": "rgba(148,163,184,0.12)" if dark_mode else "rgba(148,163,184,0.12)",
+            "grid": "rgba(148,163,184,0.12)",

As per coding guidelines, **/*.py: Use Ruff for linting with a maximum line length of 100 characters for Python code.

🧰 Tools
🪛 Ruff (0.15.10)

[warning] 648-648: Useless if-else condition

(RUF034)

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

In `@dashboard/charts.py` at line 648, The "grid" dict entry currently uses a
redundant ternary that returns the same color for both branches ("grid":
"rgba(148,163,184,0.12)" if dark_mode else "rgba(148,163,184,0.12)"); replace
this with a single direct assignment ("grid": "rgba(148,163,184,0.12)") and
remove the unnecessary conditional expression (locate the "grid" key in the
chart/theme dict where dark_mode is referenced).

Comment thread dashboard/routes/batch.py
Comment on lines +320 to +325
def get_anomaly_list(
batch_name: str,
request: Request,
page: int = 1,
per_page: int = 50,
):

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

Clamp per_page before pagination math.

A direct request such as ?per_page=0 will hit division by zero; negative values also produce invalid pagination slices.

🛡️ Proposed fix
-    total_pages = (total_anomalies + per_page - 1) // per_page
+    per_page = max(1, min(per_page, 200))
+    total_pages = (total_anomalies + per_page - 1) // per_page

Also applies to: 384-391

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

In `@dashboard/routes/batch.py` around lines 320 - 325, Clamp the per_page
parameter to a safe range before any pagination math in get_anomaly_list:
validate and coerce per_page to an integer >= 1 (and optionally capped to a
sensible max like 100) immediately on entry, then use the clamped value for
division and slice calculations to avoid division-by-zero and invalid slices;
apply the same change to the other pagination logic in this module that performs
page/per_page math so all pagination sites use the validated/clamped per_page
value.

Comment thread dashboard/routes/index.py

from fasthtml.common import H2, A, Div, P, Request, Safe, Script, Title
from monsterui.all import Button, ButtonT, Card, DivLAligned, TextPresets, UkIcon
from monsterui.all import Button, ButtonT, Card, Container, DivFullySpaced, DivLAligned, Grid, H3, Section, Subtitle, TextPresets, UkIcon

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

Wrap the changed long lines for Ruff’s 100-character limit.

Line 13 and the empty-batch section contain long changed lines. This will be easier to keep Ruff-clean if the import/string/list comprehension are wrapped, and line 130 can reuse the existing batch formatter.

🧹 Proposed Ruff-friendly cleanup
-from monsterui.all import Button, ButtonT, Card, Container, DivFullySpaced, DivLAligned, Grid, H3, Section, Subtitle, TextPresets, UkIcon
+from monsterui.all import (
+    Button,
+    ButtonT,
+    Card,
+    Container,
+    DivFullySpaced,
+    DivLAligned,
+    Grid,
+    H3,
+    Section,
+    Subtitle,
+    TextPresets,
+    UkIcon,
+)
@@
-from dashboard.presentation import summarize_batches
+from dashboard.presentation import format_batch_name, summarize_batches
@@
                 H3("Configured but waiting for data"),
                 Subtitle(
-                    "These metric batches are enabled in config but do not have recent rows in the current data source."
+                    "These metric batches are enabled in config but do not have recent rows "
+                    "in the current data source."
                 ),
@@
         ),
         Div(
-            *[P(name.replace('_', ' ').replace('-', ' ').title(), cls="dashboard-pill") for name in empty_batch_names],
+            *[
+                P(format_batch_name(name), cls="dashboard-pill")
+                for name in empty_batch_names
+            ],
             cls="no-data-pills",
         ),

As per coding guidelines, **/*.py: Use Ruff for linting with a maximum line length of 100 characters for Python code.

Also applies to: 122-130

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

In `@dashboard/routes/index.py` at line 13, The long import line and the long
list/comprehension in the empty-batch section exceed Ruff's 100-character limit;
split the import from monsterui.all across multiple lines (one symbol per line
or grouped logically) and wrap the long string/list/comprehension around the
empty-batch section so no line exceeds 100 chars; update the import that
includes Button, ButtonT, Card, Container, DivFullySpaced, DivLAligned, Grid,
H3, Section, Subtitle, TextPresets, UkIcon and reformat the batch construction
at/around the previous line ~130 to reuse the existing batch formatter style
used elsewhere in the file.

Comment thread dashboard/routes/index.py
Comment on lines +166 to +168
hx_post="/refresh-all",
hx_target="#main-content",
hx_indicator="#loading",

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:

#!/bin/bash
# Description: Find HTMX callers targeting `#main-content` and nearby hx_swap usage.
# Expectation: callers that receive a response rooted at id="main-content" should use hx_swap="outerHTML",
# or the route should return only inner content.

rg -n -C3 'hx_target="#main-content"|id="main-content"|hx-target="#main-content"' --type py

Repository: andrewm4894/anomstack

Length of output: 6056


🌐 Web query:

What is the default htmx swap style when hx-swap is omitted?

💡 Result:

The default htmx swap style when hx-swap is omitted is innerHTML.

Citations:


🏁 Script executed:

#!/bin/bash
# Check what /refresh-all endpoint returns
grep -n "refresh-all\|refresh_all" dashboard/routes/index.py -A 15 | head -40

Repository: andrewm4894/anomstack

Length of output: 1466


🏁 Script executed:

#!/bin/bash
# Read lines 283-284 from dashboard/routes/index.py
sed -n '280,290p' dashboard/routes/index.py

Repository: andrewm4894/anomstack

Length of output: 421


🏁 Script executed:

#!/bin/bash
# Check if any hx_target="#main-content" callers have explicit hx_swap attributes
rg -n 'hx_target="#main-content"' -A 3 --type py | grep -E '(hx_target|hx_swap)'

Repository: andrewm4894/anomstack

Length of output: 680


🏁 Script executed:

#!/bin/bash
# Find create_main_content function definition
rg -n "def create_main_content" dashboard/routes/index.py -A 20

Repository: andrewm4894/anomstack

Length of output: 930


🏁 Script executed:

#!/bin/bash
# Also check what the index() function structure is for the full picture
grep -n "^def index" dashboard/routes/index.py -A 5

Repository: andrewm4894/anomstack

Length of output: 214


🏁 Script executed:

#!/bin/bash
# Get the full return structure from index() function for non-HTMX case
sed -n '280,300p' dashboard/routes/index.py

Repository: andrewm4894/anomstack

Length of output: 523


🏁 Script executed:

#!/bin/bash
# Verify full create_main_content return - check if Div closes and where
sed -n '137,240p' dashboard/routes/index.py | tail -50

Repository: andrewm4894/anomstack

Length of output: 1914


🏁 Script executed:

#!/bin/bash
# Check batch.py routes that return Div with id="main-content"
rg -n "@rt.*batch" dashboard/routes/batch.py | head -10

Repository: andrewm4894/anomstack

Length of output: 547


🏁 Script executed:

#!/bin/bash
# Find the endpoints in batch.py and their return statements
grep -n "^@rt\|^def " dashboard/routes/batch.py | head -20

Repository: andrewm4894/anomstack

Length of output: 1247


🏁 Script executed:

#!/bin/bash
# Check lines 283-284 context from index.py more carefully
sed -n '275,290p' dashboard/routes/index.py

Repository: andrewm4894/anomstack

Length of output: 516


🏁 Script executed:

#!/bin/bash
# Check refresh_batch endpoint (line 281) return statement
sed -n '281,305p' dashboard/routes/batch.py

Repository: andrewm4894/anomstack

Length of output: 844


🏁 Script executed:

#!/bin/bash
# Check get_batch_view endpoint (line 51) - main batch view
sed -n '51,110p' dashboard/routes/batch.py | head -40

Repository: andrewm4894/anomstack

Length of output: 1356


🏁 Script executed:

#!/bin/bash
# Check get_anomaly_list endpoint (line 319)
sed -n '319,380p' dashboard/routes/batch.py | head -50

Repository: andrewm4894/anomstack

Length of output: 1757


🏁 Script executed:

#!/bin/bash
# Get the full get_batch_view return statement - need to see the end
sed -n '51,145p' dashboard/routes/batch.py | tail -60

Repository: andrewm4894/anomstack

Length of output: 2029


🏁 Script executed:

#!/bin/bash
# Check get_anomaly_list full return
sed -n '319,400p' dashboard/routes/batch.py | tail -60

Repository: andrewm4894/anomstack

Length of output: 2114


🏁 Script executed:

#!/bin/bash
# Check if any other routes call create_main_content
rg -n "create_main_content" dashboard/routes/

Repository: andrewm4894/anomstack

Length of output: 317


🏁 Script executed:

#!/bin/bash
# Double-check the /refresh-all endpoint path again
grep -n "def post" dashboard/routes/index.py -B 5 | grep -A 5 "refresh-all"

Repository: andrewm4894/anomstack

Length of output: 120


Fix nested duplicate #main-content IDs in HTMX responses.

The /refresh-all endpoint (line 166-167) targets #main-content with the default innerHTML swap. However, it returns main_content (a Div with id="main-content"), which creates nested duplicate IDs in the DOM. Similarly, the index route (called from toolbar.py with hx_target="#main-content") has the same issue.

Two solutions: either use hx_swap="outerHTML" on the caller, or have these endpoints return only the inner content (not wrapped in id="main-content") for HTMX requests. The batch routes already handle this correctly by returning unwrapped content for HTMX.

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

In `@dashboard/routes/index.py` around lines 166 - 168, The HTMX callers (the
toolbar button using hx_post="/refresh-all" and the index route invoked from
toolbar.py) are swapping innerHTML into an element that already contains a Div
named main_content, causing nested duplicate id="main-content" elements; fix by
either changing the caller to use hx_swap="outerHTML" or (preferred) modify the
refresh_all and index handlers so when the request is an HTMX request (check
HX-Request header) they return only the inner content instead of the wrapper Div
with id="main-content" — update the functions referenced as refresh_all and
index to detect request.headers.get("HX-Request") and return the unwrapped
content (e.g., the inner children or template fragment) while keeping the full
Div (main_content) for normal non-HTMX requests; alternatively, if you choose
the caller-side fix, add hx_swap="outerHTML" to the toolbar/button that posts to
/refresh-all.

Comment thread dashboard/routes/index.py
Comment on lines +172 to +182
A(
DivLAligned(
UkIcon("github"),
P("GitHub", cls="hidden sm:inline"),
cls="space-x-2",
),
href="https://github.com/andrewm4894/anomstack",
cls=ButtonT.secondary,
uk_tooltip="View the source code on GitHub",
target="_blank",
),

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 rel to the external new-tab link.

target="_blank" should include rel="noopener noreferrer" to prevent the opened page from getting access to window.opener.

🔒 Proposed fix
                             A(
                                 DivLAligned(
                                     UkIcon("github"),
                                     P("GitHub", cls="hidden sm:inline"),
                                     cls="space-x-2",
                                 ),
                                 href="https://github.com/andrewm4894/anomstack",
                                 cls=ButtonT.secondary,
                                 uk_tooltip="View the source code on GitHub",
                                 target="_blank",
+                                rel="noopener noreferrer",
                             ),
📝 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
A(
DivLAligned(
UkIcon("github"),
P("GitHub", cls="hidden sm:inline"),
cls="space-x-2",
),
href="https://github.com/andrewm4894/anomstack",
cls=ButtonT.secondary,
uk_tooltip="View the source code on GitHub",
target="_blank",
),
A(
DivLAligned(
UkIcon("github"),
P("GitHub", cls="hidden sm:inline"),
cls="space-x-2",
),
href="https://github.com/andrewm4894/anomstack",
cls=ButtonT.secondary,
uk_tooltip="View the source code on GitHub",
target="_blank",
rel="noopener noreferrer",
),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dashboard/routes/index.py` around lines 172 - 182, The external link A
component that wraps DivLAligned/UkIcon/P and uses target="_blank" should also
include rel="noopener noreferrer" to prevent window.opener access; update the
A(...) invocation (the component instance using ButtonT.secondary, uk_tooltip
and target="_blank") to add rel="noopener noreferrer".

Comment on lines +63 to +71
script = Script(
f"""
(() => {{
const franken = JSON.parse(localStorage.getItem("__FRANKEN__") || "{{}}");
franken.mode = "{mode}";
localStorage.setItem("__FRANKEN__", JSON.stringify(franken));
document.documentElement.classList.toggle("dark", {str(app.state.dark_mode).lower()});
document.body.classList.toggle("dark-mode", {str(app.state.dark_mode).lower()});
}})();

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

Guard theme toggling against corrupt localStorage.

If __FRANKEN__ contains invalid JSON, JSON.parse throws before the DOM classes are toggled, leaving the UI stuck in the old theme.

🛡️ Proposed guard
         (() => {{
-            const franken = JSON.parse(localStorage.getItem("__FRANKEN__") || "{{}}");
+            let franken = {{}};
+            try {{
+                franken = JSON.parse(localStorage.getItem("__FRANKEN__") || "{{}}");
+            }} catch (_) {{}}
             franken.mode = "{mode}";
             localStorage.setItem("__FRANKEN__", JSON.stringify(franken));
             document.documentElement.classList.toggle("dark", {str(app.state.dark_mode).lower()});
📝 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
script = Script(
f"""
(() => {{
const franken = JSON.parse(localStorage.getItem("__FRANKEN__") || "{{}}");
franken.mode = "{mode}";
localStorage.setItem("__FRANKEN__", JSON.stringify(franken));
document.documentElement.classList.toggle("dark", {str(app.state.dark_mode).lower()});
document.body.classList.toggle("dark-mode", {str(app.state.dark_mode).lower()});
}})();
script = Script(
f"""
(() => {{
let franken = {{}};
try {{
franken = JSON.parse(localStorage.getItem("__FRANKEN__") || "{{}}");
}} catch (_) {{}}
franken.mode = "{mode}";
localStorage.setItem("__FRANKEN__", JSON.stringify(franken));
document.documentElement.classList.toggle("dark", {str(app.state.dark_mode).lower()});
document.body.classList.toggle("dark-mode", {str(app.state.dark_mode).lower()});
}})();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dashboard/routes/toggles.py` around lines 63 - 71, The inline Script that
parses localStorage["__FRANKEN__"] can throw and prevent the theme toggle;
update the JS payload constructed in Script(...) to guard JSON.parse with a
try/catch (e.g., attempt parse, on failure set franken = {}), then always set
franken.mode and call localStorage.setItem and the
document.documentElement/document.body class toggles (the existing
document.documentElement.classList.toggle and document.body.classList.toggle
lines) so the DOM classes are applied even if localStorage is corrupt; keep
references to "__FRANKEN__" and the use of app.state.dark_mode unchanged.

Comment thread requirements-dashboard.txt Outdated
Comment thread requirements.txt Outdated
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