[dashboard,mcp] feat: refresh dashboard with tailwind UI stack#67
[dashboard,mcp] feat: refresh dashboard with tailwind UI stack#67Wangmerlyn merged 2 commits intomainfrom
Conversation
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (12)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
/gemini review\n@coderabbitai review |
|
✅ Actions performedReview triggered.
|
Summary of ChangesHello, 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
🧠 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
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this comment.
💡 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" | |||
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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'sonChangehandlers to reduce code duplication.
Overall, this is a high-quality contribution that modernizes the dashboard's frontend.
| onChange={(event) => | ||
| setForm((previous) => ({ ...previous, gpuIds: event.target.value })) | ||
| } |
There was a problem hiding this comment.
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.
| .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); | ||
| } |
There was a problem hiding this comment.
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;| .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)); | |
| } | |
There was a problem hiding this comment.
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.
| .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); | ||
| } |
There was a problem hiding this comment.
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.
| .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)); | |
| } |
|
/gemini review\n@coderabbitai review |
|
✅ Actions performedReview triggered.
|
|
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 |
There was a problem hiding this comment.
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.csscontains legitimate stylelint violations including unquoted font-family names (Segoe UI,JetBrains Mono) that triggerfont-family-name-quotes, and vendor-prefixed pseudo-elements (::-webkit-*,::-moz-*) that triggerselector-no-vendor-prefix. Rather than editing this generated Tailwind output, addsrc/keep_gpu/mcp/static/assets/*.cssto.stylelintrc.jsonviaignoreFilesand ensure linting targets the source file atweb/dashboard/src/index.cssinstead.🤖 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
⛔ Files ignored due to path filters (1)
web/dashboard/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (12)
.gitignoresrc/keep_gpu/mcp/static/assets/dashboard.jssrc/keep_gpu/mcp/static/assets/index.cssweb/dashboard/package.jsonweb/dashboard/postcss.config.jsweb/dashboard/src/App.jsxweb/dashboard/src/index.cssweb/dashboard/src/lib/session.jsweb/dashboard/src/lib/session.test.jsweb/dashboard/src/main.jsxweb/dashboard/src/styles.cssweb/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))}} | |||
There was a problem hiding this comment.
🧩 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 -5Repository: 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
fiRepository: Wangmerlyn/KeepGPU
Length of output: 134
🏁 Script executed:
cat web/dashboard/tailwind.config.jsRepository: 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.cssRepository: 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.
| <span className={`font-mono text-xs ${statusTone(gpu.utilization)}`}> | ||
| {gpu.utilization ?? "n/a"}% |
There was a problem hiding this comment.
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.
| <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.
| 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 |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find and examine the session.js file
find . -type f -name "session.js" -path "*/web/dashboard/src/lib/*" | head -20Repository: 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
fiRepository: 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('')));
EOFRepository: 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.
Summary
src/keep_gpu/mcp/static/assets/to match the new UI implementationValidation
npm test(inweb/dashboard)npm run build(inweb/dashboard)pre-commit run --all-filesSummary by CodeRabbit
New Features
Style
Tests
Chores