Skip to content

[dashboard,mcp] feat: refresh dashboard with tailwind UI stack#67

Merged
Wangmerlyn merged 2 commits intomainfrom
feat/dashboard-ui-stack-refresh
Mar 4, 2026
Merged

[dashboard,mcp] feat: refresh dashboard with tailwind UI stack#67
Wangmerlyn merged 2 commits intomainfrom
feat/dashboard-ui-stack-refresh

Conversation

@Wangmerlyn
Copy link
Owner

@Wangmerlyn Wangmerlyn commented Mar 4, 2026

Summary

  • migrate the dashboard frontend to a Tailwind/PostCSS stack with a cleaner premium control-console visual language
  • refactor dashboard session form handling into reusable parsing helpers and add Vitest coverage for input/session-state edge cases
  • rebuild served static assets under src/keep_gpu/mcp/static/assets/ to match the new UI implementation

Validation

  • npm test (in web/dashboard)
  • npm run build (in web/dashboard)
  • pre-commit run --all-files

Summary by CodeRabbit

  • New Features

    • Redesigned dashboard layout with dedicated sections for session management and GPU telemetry
    • Enhanced API error handling with user-friendly timeout messages
  • Style

    • Updated typography and color system
    • Migrated to modern CSS framework with utility-based styling
  • Tests

    • Added test suite for session configuration utilities
  • Chores

    • Updated build configuration and styling pipeline
    • Improved code organization and tracking

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 4, 2026

Warning

Rate limit exceeded

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

⌛ 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.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fcacb791-51e5-4239-9b26-63d6974efe6b

📥 Commits

Reviewing files that changed from the base of the PR and between 42fe567 and 4cd8447.

⛔ Files ignored due to path filters (1)
  • web/dashboard/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (12)
  • .gitignore
  • src/keep_gpu/mcp/static/assets/dashboard.js
  • src/keep_gpu/mcp/static/assets/index.css
  • web/dashboard/package.json
  • web/dashboard/postcss.config.js
  • web/dashboard/src/App.jsx
  • web/dashboard/src/index.css
  • web/dashboard/src/lib/session.js
  • web/dashboard/src/lib/session.test.js
  • web/dashboard/src/main.jsx
  • web/dashboard/src/styles.css
  • web/dashboard/tailwind.config.js
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/dashboard-ui-stack-refresh

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.

@Wangmerlyn
Copy link
Owner Author

/gemini review\n@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 4, 2026

@Wangmerlyn Sure, I'll review the changes in this PR!

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly modernizes the dashboard's frontend by transitioning its styling infrastructure from custom CSS to a Tailwind CSS and PostCSS stack. This change aims to provide a cleaner, more premium visual language for the control console. Alongside the UI overhaul, session form handling logic has been refactored into dedicated, reusable parsing helpers, improving maintainability and robustness. Comprehensive Vitest coverage has also been introduced for these new helpers to ensure reliable handling of input and session-state edge cases.

Highlights

  • Frontend Migration: Migrated the dashboard frontend to a Tailwind CSS and PostCSS stack, enhancing the visual language and design system.
  • Session Form Refactoring: Refactored dashboard session form handling into reusable parsing helpers for improved maintainability and clarity.
  • Test Coverage: Added Vitest coverage for input and session-state edge cases, ensuring robustness of the new form handling logic.
  • Asset Rebuild: Rebuilt served static assets under src/keep_gpu/mcp/static/assets/ to align with the new UI implementation.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • src/keep_gpu/mcp/static/assets/index.css
    • Replaced the entire content with the compiled output from Tailwind CSS and PostCSS.
  • web/dashboard/package-lock.json
    • Updated to reflect the addition of new development dependencies including autoprefixer, postcss, tailwindcss, and vitest, along with their transitive dependencies.
  • web/dashboard/package.json
    • Added autoprefixer, postcss, tailwindcss, and vitest as development dependencies.
    • Introduced a test script for running Vitest tests.
  • web/dashboard/postcss.config.js
    • Introduced a new configuration file to enable Tailwind CSS and Autoprefixer for PostCSS processing.
  • web/dashboard/src/App.jsx
    • Integrated new utility functions (buildSessionPayload, isSessionStopping) for session handling.
    • Removed the old parseGpuIds function, as its logic is now encapsulated in buildSessionPayload.
    • Updated formatGpuTarget and statusTone functions to align with new styling conventions.
    • Added utilizationWidth function for dynamic meter bar styling.
    • Extensively updated JSX elements with Tailwind CSS classes for a complete UI redesign.
    • Modified session start logic to utilize buildSessionPayload.
    • Adjusted session stop logic to use isSessionStopping for determining button disabled states.
  • web/dashboard/src/index.css
    • Created a new CSS file to import Tailwind's base, components, and utilities.
    • Defined custom CSS variables and utility classes for the new design system.
  • web/dashboard/src/main.jsx
    • Updated the CSS import path from ./styles.css to ./index.css.
  • web/dashboard/src/styles.css
    • Removed the deprecated custom CSS file.
  • web/dashboard/tailwind.config.js
    • Created a new Tailwind CSS configuration file, specifying content sources and defining custom font families.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1cc2a878ce

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@@ -1,5 +1,7 @@
import { useEffect, useMemo, useState } from "react"

import { buildSessionPayload, isSessionStopping } from "./lib/session"

Choose a reason for hiding this comment

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

P1 Badge Add the missing session helper module

App.jsx now imports buildSessionPayload and isSessionStopping from ./lib/session, but this commit never adds web/dashboard/src/lib/session (the commit tree only contains App.jsx, index.css, and main.jsx under web/dashboard/src). On a fresh checkout, vite cannot resolve this import, so the dashboard source can no longer run or rebuild even though the prebuilt asset was checked in.

Useful? React with 👍 / 👎.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request is a significant and well-executed migration of the dashboard's frontend to a modern Tailwind CSS stack. The code is cleaner, more maintainable, and the new UI looks great. The refactoring of form handling logic into separate helpers is a solid improvement.

I have a couple of suggestions to further improve maintainability:

  • In web/dashboard/src/index.css, I've pointed out an inconsistency in how custom color utilities are defined. Using CSS variables for all of them would be better for theming.
  • In web/dashboard/src/App.jsx, I've suggested a way to refactor the form's onChange handlers to reduce code duplication.

Overall, this is a high-quality contribution that modernizes the dashboard's frontend.

Comment on lines +248 to +250
onChange={(event) =>
setForm((previous) => ({ ...previous, gpuIds: event.target.value }))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The onChange handlers for your form inputs are repetitive. You can reduce duplication by creating a single handler function. This would make the code more maintainable.
You would add a name attribute to each input corresponding to its state key.

For example:

// Add a single handler to your component:
function handleFormChange(event) {
  const { name, value } = event.target;
  setForm(previous => ({ ...previous, [name]: value }));
}

// Then use it on your inputs:
<input
  name="gpuIds"
  className="field-input"
  value={form.gpuIds}
  onChange={handleFormChange}
  placeholder="0,1"
/>

This pattern can be applied to all inputs in this form.

Comment on lines +101 to +115
.text-shell-300 {
color: rgb(196 205 216);
}

.text-shell-400 {
color: rgb(var(--shell-400));
}

.text-shell-500 {
color: rgb(var(--shell-500));
}

.text-shell-600 {
color: rgb(111 124 141);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The utility classes .text-shell-300 and .text-shell-600 use hardcoded RGB values. This is inconsistent with the other color utilities in this file that use CSS variables (e.g., rgb(var(--shell-950)))

For better maintainability and theming consistency, I recommend defining --shell-300 and --shell-600 variables in the :root block and using them here.

You would need to add the following to your :root definition:

--shell-300: 196 205 216;
--shell-600: 111 124 141;
Suggested change
.text-shell-300 {
color: rgb(196 205 216);
}
.text-shell-400 {
color: rgb(var(--shell-400));
}
.text-shell-500 {
color: rgb(var(--shell-500));
}
.text-shell-600 {
color: rgb(111 124 141);
}
.text-shell-300 {
color: rgb(var(--shell-300));
}
.text-shell-400 {
color: rgb(var(--shell-400));
}
.text-shell-500 {
color: rgb(var(--shell-500));
}
.text-shell-600 {
color: rgb(var(--shell-600));
}

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request is a great improvement. It successfully migrates the dashboard's frontend from a custom CSS implementation to a modern Tailwind CSS stack, which significantly improves maintainability and developer experience. The visual refresh gives the dashboard a cleaner, more professional look. I appreciate the refactoring of session form handling logic into dedicated helper functions and the addition of unit tests using Vitest, which increases the robustness of the application. The code is well-structured and the changes are consistent. I have one minor suggestion regarding CSS variable usage for colors to further improve consistency.

Comment on lines +101 to +115
.text-shell-300 {
color: rgb(196 205 216);
}

.text-shell-400 {
color: rgb(var(--shell-400));
}

.text-shell-500 {
color: rgb(var(--shell-500));
}

.text-shell-600 {
color: rgb(111 124 141);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

For consistency with the other shell color utilities, it's better to define --shell-300 and --shell-600 as CSS variables in the :root block and use them here. This improves maintainability.

You should add the following to your :root definition (lines 9-18):

--shell-300: 196 205 216;
--shell-600: 111 124 141;

Then you can update these utility classes to use the variables.

Suggested change
.text-shell-300 {
color: rgb(196 205 216);
}
.text-shell-400 {
color: rgb(var(--shell-400));
}
.text-shell-500 {
color: rgb(var(--shell-500));
}
.text-shell-600 {
color: rgb(111 124 141);
}
.text-shell-300 {
color: rgb(var(--shell-300));
}
.text-shell-400 {
color: rgb(var(--shell-400));
}
.text-shell-500 {
color: rgb(var(--shell-500));
}
.text-shell-600 {
color: rgb(var(--shell-600));
}

@Wangmerlyn
Copy link
Owner Author

/gemini review\n@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 4, 2026

@Wangmerlyn Sure, I'll review the changes in this PR!

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@gemini-code-assist
Copy link
Contributor

It looks like you're trying to trigger a review from me and CodeRabbit. I've already posted my summary in comment #4. If you'd like me to perform a full code review, please use the /gemini review command. If you're trying to trigger a review from CodeRabbit, please use @coderabbitai review.

Copy link
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

🧹 Nitpick comments (2)
src/keep_gpu/mcp/static/assets/index.css (1)

1-1: Exclude generated assets from stylelint linting scope.

The compiled CSS in src/keep_gpu/mcp/static/assets/index.css contains legitimate stylelint violations including unquoted font-family names (Segoe UI, JetBrains Mono) that trigger font-family-name-quotes, and vendor-prefixed pseudo-elements (::-webkit-*, ::-moz-*) that trigger selector-no-vendor-prefix. Rather than editing this generated Tailwind output, add src/keep_gpu/mcp/static/assets/*.css to .stylelintrc.json via ignoreFiles and ensure linting targets the source file at web/dashboard/src/index.css instead.

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

In `@src/keep_gpu/mcp/static/assets/index.css` at line 1, The generated Tailwind
CSS at index.css is producing stylelint violations; update .stylelintrc.json by
adding an ignoreFiles entry for the generated asset glob (e.g.
src/keep_gpu/mcp/static/assets/*.css) so stylelint skips those files, and ensure
your lint target (package.json script or CI step) points to the source
stylesheet web/dashboard/src/index.css instead of the compiled index.css; modify
the ignoreFiles array and the lint target accordingly.
web/dashboard/src/App.jsx (1)

344-345: Guard session params during rendering for payload resilience.

Directly reading session.params.* can crash the list if one malformed/partial session slips through.

💡 Proposed refactor
                 sessions.map((session) => {
+                  const params = session.params ?? {}
                   const currentlyStopping = isSessionStopping(
                     session.job_id,
                     stoppingIds,
                     stoppingAll
                   )
@@
-                          GPUs {formatGpuTarget(session.params.gpu_ids)} · {session.params.vram}
-                          · {session.params.interval}s · threshold {session.params.busy_threshold}%
+                          GPUs {formatGpuTarget(params.gpu_ids)} · {params.vram ?? "n/a"}
+                          · {params.interval ?? "n/a"}s · threshold {params.busy_threshold ?? "n/a"}%
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/dashboard/src/App.jsx` around lines 344 - 345, Guard against missing or
malformed session.params when rendering the session line: check that
session.params exists before accessing its fields (or use optional chaining like
session.params?.gpu_ids, ?.vram, ?.interval, ?.busy_threshold) and supply safe
defaults (e.g., empty array or placeholder strings/numbers) or skip rendering
that part if params is falsy; update the JSX that calls
formatGpuTarget(session.params.gpu_ids) and the interpolations for
vram/interval/busy_threshold to use these guarded accesses so a
partial/malformed session can't crash the list.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/keep_gpu/mcp/static/assets/index.css`:
- Line 1: The compiled CSS is missing opacity-variant utilities for the shell
color tokens used by App.jsx (selectors bg-shell-900/60, bg-shell-900/65,
bg-shell-900/70); update the Tailwind theme to declare the shell color scale
(e.g., shell.950, shell.900, etc.) using CSS variables with alpha support
(pattern: "rgb(var(--shell-900) / <alpha-value>)") in
web/dashboard/tailwind.config.js so Tailwind generates the /<opacity> variants,
then rebuild the CSS so the background classes referenced in App.jsx take
effect.

In `@web/dashboard/src/App.jsx`:
- Around line 389-390: The span currently renders "{gpu.utilization ?? 'n/a'}%"
which produces "n/a%" for missing values; update the JSX in the component that
renders the GPU utilization (the span using statusTone(gpu.utilization)) to
conditionally include the percent sign only when gpu.utilization is a defined
number (e.g., render gpu.utilization followed by "%" when not null/undefined,
otherwise render "n/a" or another plain placeholder), and ensure statusTone
still receives the raw gpu.utilization value.

In `@web/dashboard/src/lib/session.js`:
- Around line 17-30: parsePositiveInt and parseBusyThreshold currently use
Number() which accepts "1e2", "0x10" and coerces "" to 0; replace this with
strict regex validation on the raw input string first (reject empty strings and
non-decimal tokens) then convert: for parsePositiveInt ensure the input matches
/^\+?\d+$/ then parse with Number or parseInt and enforce >=1; for
parseBusyThreshold allow either "-1" or a decimal non-negative integer by
matching either /^-1$/ or /^\+?\d+$/ then parse and enforce >=-1; update error
messages accordingly and keep using the same function names parsePositiveInt and
parseBusyThreshold.

---

Nitpick comments:
In `@src/keep_gpu/mcp/static/assets/index.css`:
- Line 1: The generated Tailwind CSS at index.css is producing stylelint
violations; update .stylelintrc.json by adding an ignoreFiles entry for the
generated asset glob (e.g. src/keep_gpu/mcp/static/assets/*.css) so stylelint
skips those files, and ensure your lint target (package.json script or CI step)
points to the source stylesheet web/dashboard/src/index.css instead of the
compiled index.css; modify the ignoreFiles array and the lint target
accordingly.

In `@web/dashboard/src/App.jsx`:
- Around line 344-345: Guard against missing or malformed session.params when
rendering the session line: check that session.params exists before accessing
its fields (or use optional chaining like session.params?.gpu_ids, ?.vram,
?.interval, ?.busy_threshold) and supply safe defaults (e.g., empty array or
placeholder strings/numbers) or skip rendering that part if params is falsy;
update the JSX that calls formatGpuTarget(session.params.gpu_ids) and the
interpolations for vram/interval/busy_threshold to use these guarded accesses so
a partial/malformed session can't crash the list.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cab9492f-4c78-41e8-9b3d-f11b84626613

📥 Commits

Reviewing files that changed from the base of the PR and between 42fe567 and 4cd8447.

⛔ Files ignored due to path filters (1)
  • web/dashboard/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (12)
  • .gitignore
  • src/keep_gpu/mcp/static/assets/dashboard.js
  • src/keep_gpu/mcp/static/assets/index.css
  • web/dashboard/package.json
  • web/dashboard/postcss.config.js
  • web/dashboard/src/App.jsx
  • web/dashboard/src/index.css
  • web/dashboard/src/lib/session.js
  • web/dashboard/src/lib/session.test.js
  • web/dashboard/src/main.jsx
  • web/dashboard/src/styles.css
  • web/dashboard/tailwind.config.js
💤 Files with no reviewable changes (1)
  • web/dashboard/src/styles.css

@@ -1 +1 @@
@import"https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&family=Fraunces:opsz,wght@9..144,500;9..144,700&display=swap";:root{--bg-0: #0e1116;--bg-1: #171c24;--bg-2: #1f2630;--panel: rgba(25, 31, 40, .82);--border: rgba(196, 171, 127, .24);--text-main: #ece5d9;--text-muted: #b2a996;--accent: #c4ab7f;--accent-soft: #e6d3ac;--cool: #6c8ca9;--alert: #c0695d;--warm: #bf8f52;--shadow: 0 18px 34px rgba(0, 0, 0, .38)}*{box-sizing:border-box}body{margin:0;min-height:100vh;color:var(--text-main);font-family:IBM Plex Sans,Segoe UI,sans-serif;background:radial-gradient(circle at 80% 0%,rgba(196,171,127,.08) 0%,transparent 35%),linear-gradient(180deg,#12161d,#0d1015)}.deck{position:relative;min-height:100vh;padding:2rem clamp(1rem,2vw,2rem) 1.2rem;display:flex;flex-direction:column;gap:1rem}.grid-noise{position:fixed;top:0;right:0;bottom:0;left:0;pointer-events:none;opacity:.2;background-image:linear-gradient(rgba(255,255,255,.02) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.02) 1px,transparent 1px);background-size:36px 36px}.glass{background:linear-gradient(175deg,rgba(34,41,50,.8),var(--panel));border:1px solid var(--border);box-shadow:inset 0 1px #ffffff08,var(--shadow);border-radius:.7rem}.masthead{padding:1.2rem 1.25rem}.eyebrow{margin:0;color:var(--accent-soft);font-family:IBM Plex Mono,monospace;letter-spacing:.14em;text-transform:uppercase;font-size:.72rem}.masthead h1{margin:.55rem 0 .35rem;font-family:Fraunces,serif;font-weight:700;font-size:clamp(1.4rem,3vw,2.2rem);letter-spacing:.01em}.masthead p{margin:0;color:var(--text-muted);max-width:72ch;line-height:1.45}.service-hint{margin-top:.6rem;font-family:IBM Plex Mono,monospace;font-size:.74rem;color:#bdb29e}.service-hint code{margin:0 .25rem;color:var(--accent-soft)}.stats-row{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:.8rem}.stat-card{padding:.85rem .95rem}.stat-card h2{margin:0;color:var(--text-muted);text-transform:uppercase;letter-spacing:.11em;font-size:.72rem;font-family:IBM Plex Mono,monospace}.stat-card p{margin:.45rem 0 0;color:var(--accent-soft);font-size:clamp(1.2rem,2vw,1.75rem);font-weight:600}.panel-grid{display:grid;gap:.85rem;grid-template-columns:minmax(300px,1fr) minmax(300px,1fr)}.panel{padding:.95rem}.span-all{grid-column:1 / -1}.panel-heading{display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem;gap:.6rem}.panel h2{margin:0;font-family:Fraunces,serif;font-weight:500;font-size:1.05rem}.chip{border:1px solid rgba(196,171,127,.4);color:var(--accent-soft);border-radius:999px;padding:.2rem .55rem;font-size:.68rem;font-family:IBM Plex Mono,monospace;letter-spacing:.08em;text-transform:uppercase}.form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.68rem}label{display:flex;flex-direction:column;gap:.34rem}label span{font-family:IBM Plex Mono,monospace;text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted);font-size:.68rem}input{border:1px solid rgba(196,171,127,.28);background:#0e1218e6;border-radius:.5rem;color:var(--text-main);padding:.6rem .66rem;font:inherit}input:focus{outline:none;border-color:var(--accent)}button{border:none;border-radius:.55rem;padding:.64rem .8rem;font:inherit;font-family:IBM Plex Mono,monospace;text-transform:uppercase;letter-spacing:.08em;cursor:pointer}button:disabled{cursor:default;opacity:.5}.primary{background:linear-gradient(180deg,#d6c09a,#b89a67);color:#1a1307}.ghost{color:var(--accent-soft);background:#c4ab7f17;border:1px solid rgba(196,171,127,.35)}.danger{color:#f1cec8;background:#c0695d29;border:1px solid rgba(192,105,93,.5)}.session-list{display:flex;flex-direction:column;gap:.6rem}.session-row{border:1px solid rgba(196,171,127,.2);border-radius:.55rem;padding:.65rem;display:flex;justify-content:space-between;align-items:center;gap:.7rem;background:#0e1218bd}.session-row h3{margin:0;font-size:.88rem;font-family:IBM Plex Mono,monospace;color:var(--accent-soft)}.session-row p{margin:.3rem 0 0;color:var(--text-muted);font-size:.78rem}.telemetry-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:.68rem}.telemetry-card{border:1px solid rgba(196,171,127,.22);background:#0e1218c2;border-radius:.55rem;padding:.68rem}.telemetry-card header{display:flex;justify-content:space-between;align-items:flex-start;gap:.5rem}.telemetry-card h3{margin:0;font-size:.86rem}.telemetry-card h3 small{display:block;margin-top:.24rem;color:var(--text-muted);font-size:.67rem;font-family:IBM Plex Mono,monospace}.meter{margin-top:.58rem;width:100%;height:.44rem;border-radius:999px;overflow:hidden;background:#ffffff12}.meter-fill{height:100%;background:linear-gradient(90deg,#7a96ad,#be9d67,#bf6d61)}.util-pill{font-size:.67rem;border-radius:999px;padding:.2rem .46rem;border:1px solid;font-family:IBM Plex Mono,monospace}.util-pill.cool{color:#c0d4e4;border-color:#6c8ca973}.util-pill.warm{color:#efd4a9;border-color:#bf8f5280}.util-pill.alert{color:#f2cac4;border-color:#c0695d80}.util-pill.muted{color:var(--text-muted);border-color:#b2a99666}.telemetry-card p,.empty{margin:.5rem 0 0;color:var(--text-muted);font-size:.78rem}.status-line{margin-top:auto;border:1px solid rgba(196,171,127,.28);border-radius:.52rem;background:#0f1318d9;padding:.56rem .72rem;display:flex;gap:.42rem;align-items:center;color:var(--text-muted);font-family:IBM Plex Mono,monospace;font-size:.72rem}.blink{width:.42rem;height:.42rem;border-radius:50%;background:var(--accent);opacity:.8;animation:pulse 1.4s ease-in-out infinite}@keyframes pulse{0%,to{transform:scale(.85)}50%{transform:scale(1.1)}}@media (max-width: 980px){.stats-row,.panel-grid,.form-grid{grid-template-columns:1fr}}
@import"https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Manrope:wght@400;500;600;700&family=Newsreader:opsz,wght@6..72,500;6..72,700&display=swap";*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Manrope,Segoe UI,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:JetBrains Mono,ui-monospace,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--shell-950: 12 15 20;--shell-900: 18 23 30;--shell-850: 30 36 46;--shell-700: 65 78 95;--shell-500: 143 154 168;--shell-400: 174 183 196;--shell-200: 214 220 228;--shell-100: 236 241 248;--shell-50: 248 250 252}*{--tw-border-opacity: 1;border-color:rgb(30 41 59 / var(--tw-border-opacity, 1))}body{margin:0;font-family:Manrope,Segoe UI,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background-color:rgb(var(--shell-950));color:rgb(var(--shell-100));background-image:radial-gradient(circle at 10% 0%,rgba(28,35,46,.65),transparent 45%),radial-gradient(circle at 90% 0%,rgba(19,27,36,.55),transparent 44%),linear-gradient(180deg,#0b0f14,#0b0f14)}.field-label{display:flex;flex-direction:column;gap:.5rem}.field-label span{font-family:JetBrains Mono,ui-monospace,monospace;font-size:11px;text-transform:uppercase;letter-spacing:.12em;color:rgb(var(--shell-500))}.field-input{width:100%;border-radius:.5rem;border-width:1px;border-color:#ffffff1a;padding:.5rem .75rem;font-size:.875rem;line-height:1.25rem;outline:2px solid transparent;outline-offset:2px;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;background-color:rgb(var(--shell-900));color:rgb(var(--shell-100))}.field-input:focus{border-color:#ffffff40}.btn-primary{border-radius:.5rem;padding:.5rem 1rem;font-size:.875rem;line-height:1.25rem;font-weight:600;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;background-color:rgb(var(--shell-100));color:rgb(var(--shell-950))}.btn-primary:hover{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.btn-primary:disabled{cursor:default;background-color:rgb(var(--shell-700));color:#c4cdd8}.btn-muted{border-radius:.5rem;border-width:1px;border-color:#ffffff26;padding:.5rem 1rem;font-size:.875rem;line-height:1.25rem;font-weight:500;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;background-color:rgb(var(--shell-900));color:rgb(var(--shell-200))}.btn-muted:hover{background-color:rgb(var(--shell-850))}.btn-muted:disabled{cursor:default;border-color:#ffffff0d;background-color:rgb(var(--shell-900));color:#6f7c8d}.btn-danger{border-radius:.5rem;border-width:1px;border-color:#fb71854d;background-color:#f43f5e1a;padding:.5rem 1rem;font-size:.875rem;line-height:1.25rem;font-weight:500;--tw-text-opacity: 1;color:rgb(254 205 211 / var(--tw-text-opacity, 1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.btn-danger:hover{background-color:#f43f5e33}.btn-danger:disabled{cursor:default;border-color:#fda4af33;background-color:#f43f5e1a;color:#fda4af80}.visible{visibility:visible}.col-span-full{grid-column:1 / -1}.mx-auto{margin-left:auto;margin-right:auto}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.block{display:block}.flex{display:flex}.grid{display:grid}.h-1\.5{height:.375rem}.h-full{height:100%}.min-h-screen{min-height:100vh}.w-full{width:100%}.max-w-2xl{max-width:42rem}.max-w-7xl{max-width:80rem}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-dashed{border-style:dashed}.border-white\/10{border-color:#ffffff1a}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-emerald-300{--tw-gradient-from: #6ee7b7 var(--tw-gradient-from-position);--tw-gradient-to: rgb(110 231 183 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.via-amber-300{--tw-gradient-to: rgb(252 211 77 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), #fcd34d var(--tw-gradient-via-position), var(--tw-gradient-to)}.to-rose-300{--tw-gradient-to: #fda4af var(--tw-gradient-to-position)}.p-4{padding:1rem}.p-5{padding:1.25rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pb-6{padding-bottom:1.5rem}.pt-8{padding-top:2rem}.font-mono{font-family:JetBrains Mono,ui-monospace,monospace}.font-serif{font-family:Newsreader,Georgia,serif}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-\[11px\]{font-size:11px}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-relaxed{line-height:1.625}.tracking-\[0\.14em\]{letter-spacing:.14em}.tracking-\[0\.16em\]{letter-spacing:.16em}.tracking-\[0\.1em\]{letter-spacing:.1em}.text-amber-300{--tw-text-opacity: 1;color:rgb(252 211 77 / var(--tw-text-opacity, 1))}.text-emerald-300{--tw-text-opacity: 1;color:rgb(110 231 183 / var(--tw-text-opacity, 1))}.text-rose-400{--tw-text-opacity: 1;color:rgb(251 113 133 / var(--tw-text-opacity, 1))}.text-slate-500{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity, 1))}.bg-shell{background-color:rgb(var(--shell-950))}.bg-panel{background-color:rgba(var(--shell-900),.78);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.bg-shell-800{background-color:rgba(var(--shell-700),.36)}.text-shell-50{color:rgb(var(--shell-50))}.text-shell-100{color:rgb(var(--shell-100))}.text-shell-300{color:#c4cdd8}.text-shell-400{color:rgb(var(--shell-400))}.text-shell-500{color:rgb(var(--shell-500))}.shadow-soft{box-shadow:0 20px 45px #00000054}@media (min-width: 640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width: 768px){.md\:col-span-2{grid-column:span 2 / span 2}.md\:min-w-28{min-width:7rem}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:justify-between{justify-content:space-between}.md\:px-6{padding-left:1.5rem;padding-right:1.5rem}.md\:text-4xl{font-size:2.25rem;line-height:2.5rem}.md\:text-base{font-size:1rem;line-height:1.5rem}.md\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width: 1024px){.lg\:col-span-12{grid-column:span 12 / span 12}.lg\:col-span-5{grid-column:span 5 / span 5}.lg\:col-span-7{grid-column:span 7 / span 7}.lg\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:items-end{align-items:flex-end}.lg\:justify-between{justify-content:space-between}.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (min-width: 1280px){.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}
Copy link
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
set -euo pipefail

echo "1) Opacity-variant classes used in App.jsx:"
rg -n 'bg-shell-900/[0-9]+' web/dashboard/src/App.jsx

echo
echo "2) Matching escaped selectors present in compiled CSS:"
rg -n 'bg-shell-900\\/(60|65|70)' src/keep_gpu/mcp/static/assets/index.css || true

echo
echo "Expected: every class used in App.jsx should have a compiled selector in static/assets/index.css."

Repository: Wangmerlyn/KeepGPU

Length of output: 637


🏁 Script executed:

# Check Tailwind config for color definitions
find . -name "tailwind.config.*" -o -name "tailwind.config.ts" -o -name "tailwind.config.js" | head -5

Repository: Wangmerlyn/KeepGPU

Length of output: 96


🏁 Script executed:

# Check if App.jsx file exists and is accessible
if [ -f web/dashboard/src/App.jsx ]; then
  echo "Found App.jsx at web/dashboard/src/App.jsx"
  wc -l web/dashboard/src/App.jsx
else
  echo "App.jsx not found at web/dashboard/src/App.jsx"
  find . -name "App.jsx" -type f | head -10
fi

Repository: Wangmerlyn/KeepGPU

Length of output: 134


🏁 Script executed:

cat web/dashboard/tailwind.config.js

Repository: Wangmerlyn/KeepGPU

Length of output: 420


🏁 Script executed:

# Check what shell color classes ARE present in compiled CSS
rg 'bg-shell' src/keep_gpu/mcp/static/assets/index.css

Repository: Wangmerlyn/KeepGPU

Length of output: 12816


Missing compiled utilities for opacity variants used in App.jsx.

The compiled CSS lacks selectors for bg-shell-900/60, bg-shell-900/65, and bg-shell-900/70, which are used at lines 201, 339, and 380 of web/dashboard/src/App.jsx. These elements will not receive their intended background styling.

The Tailwind config (web/dashboard/tailwind.config.js) does not define custom colors for the shell tokens, preventing Tailwind from generating opacity variants. Add shell colors to the theme with alpha-value support and rebuild:

theme: {
  extend: {
    colors: {
      shell: {
        950: 'rgb(var(--shell-950) / <alpha-value>)',
        900: 'rgb(var(--shell-900) / <alpha-value>)',
        // ... other shell colors
      }
    }
  }
}
🧰 Tools
🪛 Stylelint (17.3.0)

[error] 1-1: Expected quotes around "Segoe UI" (font-family-name-quotes)

(font-family-name-quotes)


[error] 1-1: Expected quotes around "JetBrains Mono" (font-family-name-quotes)

(font-family-name-quotes)


[error] 1-1: Expected quotes around "Segoe UI" (font-family-name-quotes)

(font-family-name-quotes)


[error] 1-1: Expected quotes around "JetBrains Mono" (font-family-name-quotes)

(font-family-name-quotes)


[error] 1-1: Expected quotes around "JetBrains Mono" (font-family-name-quotes)

(font-family-name-quotes)


[error] 1-1: Unexpected vendor-prefixed selector "::-moz-placeholder" (selector-no-vendor-prefix)

(selector-no-vendor-prefix)


[error] 1-1: Unexpected vendor-prefixed selector "::-moz-placeholder" (selector-no-vendor-prefix)

(selector-no-vendor-prefix)

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

In `@src/keep_gpu/mcp/static/assets/index.css` at line 1, The compiled CSS is
missing opacity-variant utilities for the shell color tokens used by App.jsx
(selectors bg-shell-900/60, bg-shell-900/65, bg-shell-900/70); update the
Tailwind theme to declare the shell color scale (e.g., shell.950, shell.900,
etc.) using CSS variables with alpha support (pattern: "rgb(var(--shell-900) /
<alpha-value>)") in web/dashboard/tailwind.config.js so Tailwind generates the
/<opacity> variants, then rebuild the CSS so the background classes referenced
in App.jsx take effect.

Comment on lines +389 to +390
<span className={`font-mono text-xs ${statusTone(gpu.utilization)}`}>
{gpu.utilization ?? "n/a"}%
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid rendering n/a% for unknown utilization.

When utilization is missing, the UI currently shows n/a%, which is misleading formatting.

💡 Proposed fix
-                      <span className={`font-mono text-xs ${statusTone(gpu.utilization)}`}>
-                        {gpu.utilization ?? "n/a"}%
-                      </span>
+                      <span className={`font-mono text-xs ${statusTone(gpu.utilization)}`}>
+                        {gpu.utilization == null ? "n/a" : `${gpu.utilization}%`}
+                      </span>
📝 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
<span className={`font-mono text-xs ${statusTone(gpu.utilization)}`}>
{gpu.utilization ?? "n/a"}%
<span className={`font-mono text-xs ${statusTone(gpu.utilization)}`}>
{gpu.utilization == null ? "n/a" : `${gpu.utilization}%`}
</span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/dashboard/src/App.jsx` around lines 389 - 390, The span currently renders
"{gpu.utilization ?? 'n/a'}%" which produces "n/a%" for missing values; update
the JSX in the component that renders the GPU utilization (the span using
statusTone(gpu.utilization)) to conditionally include the percent sign only when
gpu.utilization is a defined number (e.g., render gpu.utilization followed by
"%" when not null/undefined, otherwise render "n/a" or another plain
placeholder), and ensure statusTone still receives the raw gpu.utilization
value.

Comment on lines +17 to +30
export function parsePositiveInt(value, fieldName) {
const parsed = Number(value)
if (!Number.isInteger(parsed) || parsed < 1) {
throw new Error(`${fieldName} must be an integer >= 1`)
}
return parsed
}

export function parseBusyThreshold(value) {
const parsed = Number(value)
if (!Number.isInteger(parsed) || parsed < -1) {
throw new Error("Busy threshold must be an integer >= -1")
}
return parsed
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, find and examine the session.js file
find . -type f -name "session.js" -path "*/web/dashboard/src/lib/*" | head -20

Repository: Wangmerlyn/KeepGPU

Length of output: 96


🏁 Script executed:

# If found, read the file to see the full context
if [ -f "web/dashboard/src/lib/session.js" ]; then
  cat -n "web/dashboard/src/lib/session.js" | head -50
else
  # Try broader search
  find . -type f -name "session.js" | head -10
fi

Repository: Wangmerlyn/KeepGPU

Length of output: 1687


🏁 Script executed:

# Test Number() coercion behavior with edge cases to verify the claims
node <<'EOF'
console.log("Testing Number() coercion behavior:");
console.log("Number('1e2'):", Number('1e2'));
console.log("Number('0x10'):", Number('0x10'));
console.log("Number(''):", Number(''));
console.log("Number.isInteger(Number('1e2')):", Number.isInteger(Number('1e2')));
console.log("Number.isInteger(Number('0x10')):", Number.isInteger(Number('0x10')));
console.log("Number.isInteger(Number('')):", Number.isInteger(Number('')));
EOF

Repository: Wangmerlyn/KeepGPU

Length of output: 261


Strict integer validation needed for form inputs using Number() coercion.

The current implementation accepts unintended formats: Number('1e2') coerces to 100, Number('0x10') coerces to 16, and empty strings coerce to 0. These cases bypass intent-based validation for positive integers and busy thresholds in form inputs.

Use regex pattern validation before numeric conversion:

🔧 Suggested strict-integer patch
-const INTEGER_PATTERN = /^\d+$/
+const UNSIGNED_INTEGER_PATTERN = /^\d+$/
+const SIGNED_INTEGER_PATTERN = /^-?\d+$/

 export function parseGpuIds(raw) {
   const value = raw.trim()
@@
-  if (parts.some((part) => !INTEGER_PATTERN.test(part))) {
+  if (parts.some((part) => !UNSIGNED_INTEGER_PATTERN.test(part))) {
     throw new Error("GPU IDs must be comma-separated integers, for example: 0,1")
   }
@@
 export function parsePositiveInt(value, fieldName) {
-  const parsed = Number(value)
-  if (!Number.isInteger(parsed) || parsed < 1) {
+  const raw = String(value).trim()
+  if (!UNSIGNED_INTEGER_PATTERN.test(raw)) {
     throw new Error(`${fieldName} must be an integer >= 1`)
   }
+  const parsed = Number(raw)
+  if (parsed < 1) {
+    throw new Error(`${fieldName} must be an integer >= 1`)
+  }
   return parsed
 }

 export function parseBusyThreshold(value) {
-  const parsed = Number(value)
-  if (!Number.isInteger(parsed) || parsed < -1) {
+  const raw = String(value).trim()
+  if (!SIGNED_INTEGER_PATTERN.test(raw)) {
+    throw new Error("Busy threshold must be an integer >= -1")
+  }
+  const parsed = Number(raw)
+  if (parsed < -1) {
     throw new Error("Busy threshold must be an integer >= -1")
   }
   return parsed
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/dashboard/src/lib/session.js` around lines 17 - 30, parsePositiveInt and
parseBusyThreshold currently use Number() which accepts "1e2", "0x10" and
coerces "" to 0; replace this with strict regex validation on the raw input
string first (reject empty strings and non-decimal tokens) then convert: for
parsePositiveInt ensure the input matches /^\+?\d+$/ then parse with Number or
parseInt and enforce >=1; for parseBusyThreshold allow either "-1" or a decimal
non-negative integer by matching either /^-1$/ or /^\+?\d+$/ then parse and
enforce >=-1; update error messages accordingly and keep using the same function
names parsePositiveInt and parseBusyThreshold.

@Wangmerlyn Wangmerlyn merged commit fb5df62 into main Mar 4, 2026
5 checks passed
@Wangmerlyn Wangmerlyn deleted the feat/dashboard-ui-stack-refresh branch March 4, 2026 11:52
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