feat: built-in web dashboard#1342
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a full Clash RS Dashboard SPA (Vite + React + TypeScript) and embeds it into the server binary. Frontend includes UI components, pages, WebSocket hooks, and API client; backend adds build integration, embedded static serving, WebSocket log replay, rule-provider routes, and partial listener restart support. Changes
Sequence Diagram(s)sequenceDiagram
participant Dev as Developer (build)
participant NPM as npm (ci/build)
participant RustBuild as build.rs
participant Embed as rust-embed
participant Server as clash-rs (Axum)
participant Client as Browser SPA
Dev->>NPM: run npm ci && npm run build (clash-dashboard/dist)
NPM-->>RustBuild: output dist/
RustBuild->>Embed: embed dist/ into binary (rust-embed)
RustBuild->>Server: include embedded assets at compile-time
Client->>Server: GET /ui/ (or /ui/<asset>)
Server->>Embed: resolve asset
Embed-->>Server: return file bytes + content-type
Server-->>Client: serve asset (index.html or static file)
Client->>Server: REST /api/* and WebSocket /ws/*
Server->>Client: JSON responses and WS messages (including replayed recent_logs)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 15
🧹 Nitpick comments (8)
clash-dashboard/src/components/ui/badge.tsx (1)
29-36: ReusebadgeVariantsinsideBadgeto avoid style drift.
Lines 32-35 duplicate the same composition logic already encoded inbadgeVariants.♻️ Suggested refactor
function Badge({ className, variant = 'default', ...props }: BadgeProps) { return ( <span - className={cn( - 'inline-flex items-center text-[11px] font-semibold px-2 py-0.5 rounded-full', - variantStyles[variant] ?? variantStyles.default, - className - )} + className={cn(badgeVariants({ variant }), className)} {...props} /> ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-dashboard/src/components/ui/badge.tsx` around lines 29 - 36, The Badge component duplicates style composition; update Badge to call the existing badgeVariants helper instead of manually combining variantStyles and className: in the Badge function (keeping the signature and default variant), compute the span's className by invoking badgeVariants({ variant, className }) (or the correct badgeVariants API used in the repo) and remove the explicit variantStyles lookup, then keep spreading ...props onto the span; ensure badgeVariants is imported where Badge is defined.clash-dashboard/src/components/ui/button.tsx (1)
4-38: Tighten variant/size typing to preserve literal unions.
On lines 4 and 14, usingRecord<string, string>widens keys tostring, sovariant/sizelose strict key safety.♻️ Suggested refactor
-const variantStyles: Record<string, string> = { +const variantStyles = { default: 'bg-[`#0071e3`] hover:bg-[`#0077ed`] text-white', primary: 'bg-[`#0071e3`] hover:bg-[`#0077ed`] text-white', outline: 'border border-black/[0.08] bg-white hover:bg-[`#f2f2f7`] text-[`#1d1d1f`]', secondary: 'bg-black/[0.06] hover:bg-black/[0.1] text-[`#1d1d1f`]', ghost: 'text-[`#6e6e73`] hover:text-[`#1d1d1f`] hover:bg-black/[0.05]', destructive: 'bg-red-500 hover:bg-red-600 text-white', link: 'text-[`#0071e3`] underline-offset-4 hover:underline', -} +} as const -const sizeStyles: Record<string, string> = { +const sizeStyles = { default: 'h-8 px-3 text-[15px]', xs: 'h-6 px-2 text-xs rounded-md', sm: 'h-7 px-2.5 text-[0.8rem]', lg: 'h-10 px-4 text-[15px]', icon: 'size-8', 'icon-xs': 'size-6', 'icon-sm': 'size-7', 'icon-lg': 'size-9', -} +} as const + +type ButtonVariant = keyof typeof variantStyles +type ButtonSize = keyof typeof sizeStyles export const buttonVariants = ({ variant = 'default', size = 'default', -}: { variant?: string; size?: string } = {}) => +}: { variant?: ButtonVariant; size?: ButtonSize } = {}) => cn(🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-dashboard/src/components/ui/button.tsx` around lines 4 - 38, The variantStyles and sizeStyles objects are typed as Record<string,string>, which widens their keys and loses literal union types; make them const-typed so their keys are preserved (e.g., declare variantStyles and sizeStyles as plain object literals with "as const" or inferred const types), then use keyof typeof variantStyles and keyof typeof sizeStyles for the buttonVariants parameters and ButtonProps variant/size types; update the buttonVariants signature (and its default param types) to reference those keyof types so TypeScript enforces the exact allowed keys while leaving the runtime values unchanged (refer to variantStyles, sizeStyles, buttonVariants, and ButtonProps).clash-lib/src/app/api/embedded_dashboard.rs (2)
60-76: MIME type coverage is solid; consider adding a few more common types.The extension-to-MIME mapping covers the essentials. You might want to add
.webp→image/webp,.gif→image/gif, and.map→application/json(for source maps during development), though these may not be needed for the current dashboard build output.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-lib/src/app/api/embedded_dashboard.rs` around lines 60 - 76, The content_type function's MIME mapping is missing a few common extensions; update the match in content_type(path: &str) to handle "webp" -> "image/webp", "gif" -> "image/gif", and "map" -> "application/json" (or "application/json; charset=utf-8" if you prefer consistency), keeping the existing default branch intact and using the same pattern of matching path.rsplit('.').next() as in the current function.
39-43: Consider propagating errors instead of unwrap.The
Response::builder().unwrap()calls are practically safe since only valid headers are set, but for defensive coding they could return aResultor useunwrap_or_elsewith a fallback 500 response. This is a minor consideration given the controlled input.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-lib/src/app/api/embedded_dashboard.rs` around lines 39 - 43, Replace the final .unwrap() on the Response builder with proper error propagation/handling: change the enclosing function to return a Result (e.g. Result<Response<Body>, E>) or map the builder error into an HTTP 500 response instead of panicking. Specifically, handle failures from Response::builder().header(header::CONTENT_TYPE, mime).header(header::CACHE_CONTROL, cache).body(Body::from(asset.data.into_owned())) by using ? (to propagate) or .map_err(|e| /* convert to your error type or build a 500 Response */) so the code no longer calls .unwrap() and either returns the error or a fallback Response.clash-dashboard/src/pages/Connections.tsx (2)
51-52: Mutations lack error handling and user feedback.Both
closeAllMutationandcloseOneMutationdon't haveonErrororonSuccesscallbacks. Users won't see feedback if a close operation fails. Consider adding toast notifications or error states:♻️ Add error/success callbacks
-const closeAllMutation = useMutation({ mutationFn: closeAllConnections }); -const closeOneMutation = useMutation({ mutationFn: closeConnection }); +const closeAllMutation = useMutation({ + mutationFn: closeAllConnections, + onError: (err) => { + // Consider using a toast library or error state + console.error('Failed to close all connections:', err); + }, +}); +const closeOneMutation = useMutation({ + mutationFn: closeConnection, + onError: (err) => { + console.error('Failed to close connection:', err); + }, +});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-dashboard/src/pages/Connections.tsx` around lines 51 - 52, The mutations closeAllMutation and closeOneMutation (wrapping closeAllConnections and closeConnection) lack onError/onSuccess handlers, so add callbacks to each useMutation call to surface user feedback and refresh state: implement onSuccess to show a success toast (or set a success state) and invalidate or refetch the connections query via queryClient.invalidateQueries(['connections']) or similar, and implement onError to show an error toast with the error message and optionally set an error state; update the useMutation calls for closeAllMutation and closeOneMutation to include these onSuccess/onError callbacks.
59-66: "Close All" is a destructive action without confirmation.Clicking "Close All" immediately terminates all connections without a confirmation dialog. For a destructive bulk action, consider adding a confirmation step to prevent accidental clicks.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-dashboard/src/pages/Connections.tsx` around lines 59 - 66, The "Close All" button calls closeAllMutation.mutate() directly and needs a confirmation step; replace the inline onClick with a handler (e.g., confirmCloseAll or handleCloseAll) in the Connections component that shows a confirmation dialog (native window.confirm or your app modal component), and only calls closeAllMutation.mutate() if the user confirms; ensure the handler is used by the button and that the modal flow handles cancelation and prevents accidental double submissions.clash-dashboard/src/pages/Proxies.tsx (1)
119-146: Provider operations lack error feedback.Similar to the Connections page,
runHealthcheckandrunUpdatedon't provide user feedback on failure. Consider adding try/catch with error notification:♻️ Add error handling
async function runHealthcheck(provider: ProxyProvider) { setTestingProviders((s) => new Set(s).add(provider.name)); setExpandedProviders((prev) => new Set(prev).add(provider.name)); try { await healthcheckProvider(provider.name); await queryClient.invalidateQueries({ queryKey: ['providers'] }); + } catch (err) { + console.error('Healthcheck failed:', err); + // Consider adding toast/notification } finally { setTestingProviders((s) => { const next = new Set(s); next.delete(provider.name); return next; }); } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-dashboard/src/pages/Proxies.tsx` around lines 119 - 146, Both runHealthcheck and runUpdate lack error feedback to the user; wrap the async work in try/catch so failures display a user-facing error notification. Specifically, in runHealthcheck and runUpdate catch the thrown error from healthcheckProvider/updateProxyProvider, call the app's existing notification/error handler (e.g., toast/snackbar/notify) with a clear message that includes provider.name and the error.message, then re-run queryClient.invalidateQueries on success or keep it where it is; keep the existing finally blocks that clear testing/updating state so cleanup still happens. Locate the functions runHealthcheck and runUpdate to implement the try/catch + notify behavior.clash-dashboard/src/components/TrafficChart.tsx (1)
75-91: Consider debouncing ResizeObserver to avoid rapid chart rebuilds.The ResizeObserver callback fires on every size change during window resize, triggering full chart destruction and recreation. For smoother performance, consider debouncing:
♻️ Example debounce implementation
+import { useEffect, useRef, useCallback } from 'react'; + +// Add a simple debounce helper or use lodash.debounce +function debounce<T extends (...args: unknown[]) => void>(fn: T, ms: number): T { + let timer: ReturnType<typeof setTimeout>; + return ((...args: unknown[]) => { + clearTimeout(timer); + timer = setTimeout(() => fn(...args), ms); + }) as T; +} useEffect(() => { if (!containerRef.current) return; buildChart(containerRef.current.clientWidth); + const debouncedBuild = debounce((width: number) => buildChart(width), 100); const observer = new ResizeObserver((entries) => { const width = entries[0]?.contentRect.width; - if (width) buildChart(width); + if (width) debouncedBuild(width); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-dashboard/src/components/TrafficChart.tsx` around lines 75 - 91, The ResizeObserver in the useEffect (observing containerRef) currently calls buildChart on every size change which causes expensive destroy/recreate cycles; debounce the observer callback by introducing a ref-based timer (e.g., resizeTimerRef) and only call buildChart(width) after a short delay (100–200ms) with clearTimeout before scheduling to coalesce rapid events; ensure the initial buildChart(containerRef.current.clientWidth) stays immediate and that you clear the timer in the cleanup along with observer.disconnect() and chartRef.current?.destroy().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@clash-dashboard/src/components/Layout.tsx`:
- Around line 44-45: The header text in Layout.tsx (the <span
className="font-semibold text-[15px]" ...> and the similar spans at lines noted)
hardcodes style={{ color: '#1d1d1f' }}, which prevents dark-mode theming; remove
the inline color and replace it with theme-aware classes (e.g., Tailwind text
utility plus dark: variant such as text-neutral-900 and dark:text-neutral-100 or
a semantic class like .brand-text with dark-mode rules) so the brand/nav/status
text responds to prefers-color-scheme and the rules in src/index.css.
In `@clash-dashboard/src/components/ui/button.tsx`:
- Around line 40-45: The Button component currently renders a plain <button>
which will default to type="submit" inside forms; update the Button function to
ensure a safe default by using the provided props.type when present and
otherwise setting type="button" (e.g., ensure Button uses props.type ?? 'button'
or extracts type from props before spreading). Modify the JSX in Button (the
button element inside function Button) to explicitly set that computed type
while still allowing callers to override via props.
In `@clash-dashboard/src/lib/api.ts`:
- Around line 222-223: The closeConnection function inserts the raw id into the
URL path which can break routes for IDs with reserved characters; update the
call that constructs the path in closeConnection to URL-encode the id (use
encodeURIComponent on the id) before passing it to request so the DELETE
endpoint becomes `/connections/${encodeURIComponent(id)}` and behaves
consistently with other encoded path params.
- Around line 170-174: The code currently only guards 204 and explicit
content-length: 0 but still calls response.json() which throws on empty 200
bodies; update the response handling (the block using response.json()) to first
read the raw body with await response.text(), return undefined if the text is
empty or only whitespace, otherwise parse and return JSON (e.g.,
JSON.parse(text) or JSON.parse after trimming) so empty successful responses
(including 200) don't cause an exception; keep the existing 204/content-length
checks or replace them with this text-based check in the same function where
response and response.json() are used.
- Around line 236-241: The getWsUrl function always appends the token with
'?token=...', which breaks if the provided path already contains query
parameters; update the token concatenation logic in getWsUrl (which calls
getApiUrl and getSecret and uses baseUrl, wsUrl, path, secret) to detect whether
path already contains a '?' and use '&token=...' in that case (otherwise use
'?token=...'), keeping encodeURIComponent(secret) and preserving existing
behavior when secret is empty.
In `@clash-dashboard/src/pages/Config.tsx`:
- Around line 154-162: Add onError handlers to the existing useMutation calls
for patchMutation and reloadMutation so failures surface to the user: update the
useMutation options for the mutationFn entries that call patchConfigs and
reloadConfigs to include an onError callback that logs the error and shows
user-visible feedback (e.g., call your toast/error UI or set an error state to
display inline). Keep the existing onSuccess behavior
(queryClient.invalidateQueries({ queryKey: ['configs'] })) and ensure the
onError receives the error and context so you can include actionable text (e.g.,
"Failed to save config" or "Failed to reload config") when invoking your toast
or inline error component.
- Around line 152-153: The component currently only reads const { data,
isLoading } = useQuery({ queryKey: ['configs'], queryFn: getConfigs }) and ends
up returning null when the fetch fails (cfg undefined); update the hook usage to
also destructure error and isError (e.g., const { data, isLoading, error,
isError } = useQuery(...)) and add an explicit early return rendering a clear
error state (a message or ErrorComponent) when isError is true (or error is set)
so the component (referencing cfg later) shows a failure UI instead of blank;
apply this same pattern wherever the component queries /configs (the other
useQuery occurrences around the cfg usage at the referenced blocks).
- Line 178: The reload button is passing an empty string into
reloadMutation.mutate(''), which becomes Some("") in reloadConfigs() and
triggers the backend path-handling branch that errors; fix by changing the
onClick so it does not pass an empty string — call reloadMutation.mutate() with
no argument (or supply a valid config path or send the config payload) so the
backend receives None instead of Some(""), or alternatively remove/disable the
button if reload is not intended; update the code referencing
reloadMutation.mutate and the reloadConfigs handler usage accordingly.
In `@clash-dashboard/src/pages/Logs.tsx`:
- Around line 98-105: The hover handlers are mutating the same paused state used
by the user's Pause/Resume toggle; introduce a separate hover state (e.g.,
hoverPaused or isHovering) and stop writing directly to paused in
onMouseEnter/onMouseLeave—have those setHoverPaused(true/false) instead, keep
the button toggling only the user-controlled state (e.g., userPaused via
setUserPaused), and derive the runtime behavior/UI from a computed
effectivePaused = userPaused || hoverPaused (use effectivePaused where paused
was read). Update references to paused in the component (Logs.tsx) to use
userPaused for the button label and effectivePaused for auto-scroll logic and
styling; change onMouseEnter/onMouseLeave to setHoverPaused and leave setPaused
only in the button onClick.
In `@clash-dashboard/src/pages/Overview.tsx`:
- Around line 76-79: The mutation allows overlapping writes causing race
conditions; fix by adding an optimistic update flow: in the useMutation for
modeMutation (and the similar mutation at lines 183-190) add onMutate that calls
queryClient.cancelQueries(['configs']), snapshots the previous configs via
queryClient.getQueryData(['configs']), and calls
queryClient.setQueryData(['configs'], draft => ({...draft, mode})); return the
snapshot as context; implement onError to rollback with
queryClient.setQueryData(['configs'], context.previous) and onSettled to
invalidate queries with queryClient.invalidateQueries({ queryKey: ['configs']
}); keep mutationFn as updateConfigs({ mode }). This ensures serial/optimistic
updates and prevents applied mode from reflecting out-of-order network
responses.
In `@clash-dashboard/src/pages/Rules.tsx`:
- Around line 17-18: The UI is treating query failures as an empty rules list;
update the component to surface query errors instead of mapping failures to "no
matches": use the useQuery return values (data, isLoading, isError, error) from
the call that uses getRules and change the logic that computes rules (the
current mapping around lines 51-61) so it only derives an empty array when data
is defined and filtering yields no results; when isError is true show an error
state/message (or propagate error) and when isLoading show a loader—do not
coerce undefined data into [].
- Around line 20-25: The filter callback used to compute filtered (rules.filter)
calls r.payload.toLowerCase() unconditionally and will throw if payload is
undefined; update the predicate to guard payload (e.g., use optional chaining or
a default empty string for r.payload) before calling toLowerCase() — similarly
consider normalizing r.type and r.proxy with toLowerCase() only after ensuring
they exist (or defaulting to '') so the filter never invokes toLowerCase on
undefined.
In `@clash-dashboard/src/pages/Settings.tsx`:
- Around line 33-35: handleSave currently calls setSecret which persists the
admin token via the settings module (setSecret/getSecret) into localStorage;
remove that persistence by stopping the call to setSecret from handleSave and
instead keep the secret only in component state (or introduce an explicit
"persist token" opt-in flag and only call setSecret when that flag is true).
Locate handleSave and the usages of setSecret/getSecret and refactor so the
token is session-scoped (in-memory React state) by default; if persistence is
required, add an explicit checkbox/setting that must be true before calling
setSecret to write to storage.
In `@clash-dashboard/vite.config.ts`:
- Around line 12-15: The alias currently uses __dirname (resolve.alias ->
path.resolve(__dirname, './src')), which fails in ESM; replace it by deriving a
dirname from import.meta.url (e.g., import { fileURLToPath } from 'url' and
const __dirname = path.dirname(fileURLToPath(import.meta.url'))) and then use
path.resolve(__dirname, './src') (or use path.resolve(fileURLToPath(new
URL('./src', import.meta.url)))) so the resolve.alias entry no longer references
the undefined __dirname in ESM.
In `@clash-lib/build.rs`:
- Around line 66-71: The build script's input-watch array (the for loop
iterating over the string array containing "index.html", "vite.config.ts",
"package.json", "tsconfig.json") is missing "tsconfig.app.json", so the
dashboard UI build isn't retriggered when that file changes; add
"tsconfig.app.json" to that same array literal (the one being iterated by file)
so the script emits a rerun trigger for it alongside the other UI inputs.
---
Nitpick comments:
In `@clash-dashboard/src/components/TrafficChart.tsx`:
- Around line 75-91: The ResizeObserver in the useEffect (observing
containerRef) currently calls buildChart on every size change which causes
expensive destroy/recreate cycles; debounce the observer callback by introducing
a ref-based timer (e.g., resizeTimerRef) and only call buildChart(width) after a
short delay (100–200ms) with clearTimeout before scheduling to coalesce rapid
events; ensure the initial buildChart(containerRef.current.clientWidth) stays
immediate and that you clear the timer in the cleanup along with
observer.disconnect() and chartRef.current?.destroy().
In `@clash-dashboard/src/components/ui/badge.tsx`:
- Around line 29-36: The Badge component duplicates style composition; update
Badge to call the existing badgeVariants helper instead of manually combining
variantStyles and className: in the Badge function (keeping the signature and
default variant), compute the span's className by invoking badgeVariants({
variant, className }) (or the correct badgeVariants API used in the repo) and
remove the explicit variantStyles lookup, then keep spreading ...props onto the
span; ensure badgeVariants is imported where Badge is defined.
In `@clash-dashboard/src/components/ui/button.tsx`:
- Around line 4-38: The variantStyles and sizeStyles objects are typed as
Record<string,string>, which widens their keys and loses literal union types;
make them const-typed so their keys are preserved (e.g., declare variantStyles
and sizeStyles as plain object literals with "as const" or inferred const
types), then use keyof typeof variantStyles and keyof typeof sizeStyles for the
buttonVariants parameters and ButtonProps variant/size types; update the
buttonVariants signature (and its default param types) to reference those keyof
types so TypeScript enforces the exact allowed keys while leaving the runtime
values unchanged (refer to variantStyles, sizeStyles, buttonVariants, and
ButtonProps).
In `@clash-dashboard/src/pages/Connections.tsx`:
- Around line 51-52: The mutations closeAllMutation and closeOneMutation
(wrapping closeAllConnections and closeConnection) lack onError/onSuccess
handlers, so add callbacks to each useMutation call to surface user feedback and
refresh state: implement onSuccess to show a success toast (or set a success
state) and invalidate or refetch the connections query via
queryClient.invalidateQueries(['connections']) or similar, and implement onError
to show an error toast with the error message and optionally set an error state;
update the useMutation calls for closeAllMutation and closeOneMutation to
include these onSuccess/onError callbacks.
- Around line 59-66: The "Close All" button calls closeAllMutation.mutate()
directly and needs a confirmation step; replace the inline onClick with a
handler (e.g., confirmCloseAll or handleCloseAll) in the Connections component
that shows a confirmation dialog (native window.confirm or your app modal
component), and only calls closeAllMutation.mutate() if the user confirms;
ensure the handler is used by the button and that the modal flow handles
cancelation and prevents accidental double submissions.
In `@clash-dashboard/src/pages/Proxies.tsx`:
- Around line 119-146: Both runHealthcheck and runUpdate lack error feedback to
the user; wrap the async work in try/catch so failures display a user-facing
error notification. Specifically, in runHealthcheck and runUpdate catch the
thrown error from healthcheckProvider/updateProxyProvider, call the app's
existing notification/error handler (e.g., toast/snackbar/notify) with a clear
message that includes provider.name and the error.message, then re-run
queryClient.invalidateQueries on success or keep it where it is; keep the
existing finally blocks that clear testing/updating state so cleanup still
happens. Locate the functions runHealthcheck and runUpdate to implement the
try/catch + notify behavior.
In `@clash-lib/src/app/api/embedded_dashboard.rs`:
- Around line 60-76: The content_type function's MIME mapping is missing a few
common extensions; update the match in content_type(path: &str) to handle "webp"
-> "image/webp", "gif" -> "image/gif", and "map" -> "application/json" (or
"application/json; charset=utf-8" if you prefer consistency), keeping the
existing default branch intact and using the same pattern of matching
path.rsplit('.').next() as in the current function.
- Around line 39-43: Replace the final .unwrap() on the Response builder with
proper error propagation/handling: change the enclosing function to return a
Result (e.g. Result<Response<Body>, E>) or map the builder error into an HTTP
500 response instead of panicking. Specifically, handle failures from
Response::builder().header(header::CONTENT_TYPE,
mime).header(header::CACHE_CONTROL,
cache).body(Body::from(asset.data.into_owned())) by using ? (to propagate) or
.map_err(|e| /* convert to your error type or build a 500 Response */) so the
code no longer calls .unwrap() and either returns the error or a fallback
Response.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 8eacdd42-45a2-4adf-bee5-23a2f77ba32e
⛔ Files ignored due to path filters (7)
Cargo.lockis excluded by!**/*.lockclash-dashboard/package-lock.jsonis excluded by!**/package-lock.jsonclash-dashboard/public/favicon.svgis excluded by!**/*.svgclash-dashboard/public/icons.svgis excluded by!**/*.svgclash-dashboard/src/assets/hero.pngis excluded by!**/*.pngclash-dashboard/src/assets/react.svgis excluded by!**/*.svgclash-dashboard/src/assets/vite.svgis excluded by!**/*.svg
📒 Files selected for processing (41)
clash-dashboard/.gitignoreclash-dashboard/README.mdclash-dashboard/components.jsonclash-dashboard/eslint.config.jsclash-dashboard/index.htmlclash-dashboard/package.jsonclash-dashboard/src/App.tsxclash-dashboard/src/components/Layout.tsxclash-dashboard/src/components/TrafficChart.tsxclash-dashboard/src/components/ui/badge.tsxclash-dashboard/src/components/ui/button.tsxclash-dashboard/src/components/ui/card.tsxclash-dashboard/src/components/ui/scroll-area.tsxclash-dashboard/src/components/ui/separator.tsxclash-dashboard/src/components/ui/table.tsxclash-dashboard/src/components/ui/tabs.tsxclash-dashboard/src/components/ui/tooltip.tsxclash-dashboard/src/hooks/useTraffic.tsclash-dashboard/src/hooks/useWebSocket.tsclash-dashboard/src/index.cssclash-dashboard/src/lib/api.tsclash-dashboard/src/lib/settings.tsclash-dashboard/src/lib/utils.tsclash-dashboard/src/main.tsxclash-dashboard/src/pages/Config.tsxclash-dashboard/src/pages/Connections.tsxclash-dashboard/src/pages/DNS.tsxclash-dashboard/src/pages/Logs.tsxclash-dashboard/src/pages/Overview.tsxclash-dashboard/src/pages/Proxies.tsxclash-dashboard/src/pages/Rules.tsxclash-dashboard/src/pages/Settings.tsxclash-dashboard/tsconfig.app.jsonclash-dashboard/tsconfig.jsonclash-dashboard/tsconfig.node.jsonclash-dashboard/vite.config.tsclash-lib/Cargo.tomlclash-lib/build.rsclash-lib/src/app/api/embedded_dashboard.rsclash-lib/src/app/api/mod.rsclash-lib/src/app/api/runner.rs
| <span className="font-semibold text-[15px]" style={{ color: '#1d1d1f' }}> | ||
| clash-rs |
There was a problem hiding this comment.
These inline text colors defeat dark mode in the shell.
The header/nav/status text is pinned to the light palette here, so the new prefers-color-scheme: dark styling in src/index.css doesn't actually theme the shell. In dark mode the brand and nav labels end up low-contrast or effectively invisible.
Also applies to: 56-60, 83-85
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@clash-dashboard/src/components/Layout.tsx` around lines 44 - 45, The header
text in Layout.tsx (the <span className="font-semibold text-[15px]" ...> and the
similar spans at lines noted) hardcodes style={{ color: '#1d1d1f' }}, which
prevents dark-mode theming; remove the inline color and replace it with
theme-aware classes (e.g., Tailwind text utility plus dark: variant such as
text-neutral-900 and dark:text-neutral-100 or a semantic class like .brand-text
with dark-mode rules) so the brand/nav/status text responds to
prefers-color-scheme and the rules in src/index.css.
| function handleSave() { | ||
| setApiUrl(apiUrl); | ||
| setSecret(secret); |
There was a problem hiding this comment.
Don't persist the API secret in localStorage.
handleSave() calls setSecret(), and src/lib/settings.ts:1-4 backs that with localStorage. That makes the Clash admin token long-lived and readable by any script running on this origin. Please keep it session-scoped/in-memory, or at least gate persistence behind an explicit opt-in.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@clash-dashboard/src/pages/Settings.tsx` around lines 33 - 35, handleSave
currently calls setSecret which persists the admin token via the settings module
(setSecret/getSecret) into localStorage; remove that persistence by stopping the
call to setSecret from handleSave and instead keep the secret only in component
state (or introduce an explicit "persist token" opt-in flag and only call
setSecret when that flag is true). Locate handleSave and the usages of
setSecret/getSecret and refactor so the token is session-scoped (in-memory React
state) by default; if persistence is required, add an explicit checkbox/setting
that must be true before calling setSecret to write to storage.
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
📊 Proxy Throughput Results
Tests ran 5 variant(s) in parallel; each direction transfers the full payload. Full test logDownload the |
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
clash-lib/src/app/api/handlers/config.rs (1)
306-358:⚠️ Potential issue | 🟠 MajorBug: bind_address change combined with port change may use incorrect restart method.
When both
bind_addressand a port field are changed in the same PATCH request,port_changedwill betrue(fromchange_ports), causingrestart_idle()to be called instead of the required fullrestart(). However,bind_addresschanges require a full restart since the new address must apply to all listeners, not just port-changed ones.Suggested fix
if let Some(bind_address) = payload.bind_address.clone() { match bind_address.parse::<BindAddress>() { Ok(bind_address) => { inbound_manager.set_bind_address(bind_address).await; need_restart = true; + // bind_address affects all listeners — force full restart + // even if ports also changed. } Err(_) => { return ( StatusCode::BAD_REQUEST, format!("invalid bind address: {bind_address}"), ) .into_response(); } } } let mut port_changed = false; - if payload.rebuild_listeners() { + // Only track port_changed when bind_address wasn't also changed, + // since bind_address requires a full restart regardless. + if payload.rebuild_listeners() && payload.bind_address.is_none() { let ports = Ports {Alternatively, reset
port_changedafter the bind_address block similar to theallow_lanhandling.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-lib/src/app/api/handlers/config.rs` around lines 306 - 358, When handling payload.bind_address in the PATCH handler, setting the new bind address via inbound_manager.set_bind_address(...) requires a full restart, but the current flow can leave port_changed true (set by inbound_manager.change_ports(...)) which causes restart_idle() to be used; after applying the bind address (the block that calls inbound_manager.set_bind_address), clear or set port_changed = false so the later restart branch will call inbound_manager.restart() instead of inbound_manager.restart_idle(); mirror the allow_lan handling where port_changed is reset to force a full restart.
♻️ Duplicate comments (1)
clash-dashboard/src/components/Layout.tsx (1)
38-55:⚠️ Potential issue | 🟠 MajorThe shell still hardcodes light-theme text colors.
These inline/light-palette styles override the dark-mode rules from
src/index.css, so the brand, nav, and status text stay low-contrast in dark mode.Also applies to: 75-77
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-dashboard/src/components/Layout.tsx` around lines 38 - 55, The span in Layout.tsx and the NavLink class strings are hardcoding light-mode hex colors (e.g. style={{ color: '#1d1d1f' }}, 'text-[`#0071e3`]', 'text-[`#6e6e73`]', 'hover:text-[`#1d1d1f`]') which override dark-mode rules; remove the inline style and replace those hardcoded hex classes with theme-aware CSS (use existing index.css dark-mode variables/classes or Tailwind dark: variants such as neutral/text color classes or CSS custom properties) so the brand, nav and status text inherit proper light/dark colors; update the NavLink mapping and the span in Layout.tsx (and the similar occurrences around the status block) to use the theme-aware classes instead of hex values.
🧹 Nitpick comments (7)
clash-dashboard/src/pages/Proxies.tsx (1)
358-476: Significant code duplication withProviders.tsx.The provider cards section (lines 358-476) largely duplicates the rendering logic from
Providers.tsx. Consider extracting a sharedProviderCardcomponent to reduce duplication and ensure consistent behavior.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-dashboard/src/pages/Proxies.tsx` around lines 358 - 476, The provider card rendering in Proxies.tsx is duplicated from Providers.tsx; extract a reusable ProviderCard component that accepts props like provider, isExpanded, isTesting, isUpdating, latencyMap, toggleProviderExpanded, runHealthcheck, runUpdate, getLastDelay and getLatencyColor, move the JSX block that starts with the Providers map (the card container, header buttons, expansion logic and proxy grid) into that component, and replace the inline map in Proxies.tsx with a call to <ProviderCard ...> passing the needed handlers/state (use existing identifiers toggleProviderExpanded, runHealthcheck, runUpdate, latencyMap, getLastDelay, getLatencyColor and expandedProviders/testingProviders/updatingProviders to compute booleans). Ensure ProviderCard emits the same events and preserves keys (proxy.name/provider.name) and styles so behavior remains identical.clash-dashboard/src/components/ProxyGroups.tsx (1)
7-16: Consider extracting shared utility functions.
getLatencyColor,getLastDelay, and similar helpers are duplicated acrossProxyGroups.tsx,Providers.tsx, andProxies.tsx. Consider extracting them to a shared utilities file.Also applies to: 34-37
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-dashboard/src/components/ProxyGroups.tsx` around lines 7 - 16, Duplicate helper functions like getLatencyColor and getLastDelay are present in ProxyGroups.tsx, Providers.tsx and Proxies.tsx; extract these into a single shared utility module (e.g., export functions getLatencyColor, getLastDelay, and any other repeated helpers) and replace the local definitions with imports from that module in ProxyGroups.tsx, Providers.tsx and Proxies.tsx so the components use the centralized implementations; ensure exported function names match (getLatencyColor, getLastDelay) and update all import sites accordingly, removing the duplicated code blocks from each file.clash-dashboard/src/pages/Providers.tsx (1)
117-172: MoveRuleProviderRulesPaneloutside the parent component.Defining
RuleProviderRulesPanelinsideProvidersrecreates the component function on every render ofProviders, causing React Query to re-mount and re-fetch unnecessarily when parent state changes (e.g., when expanding/collapsing other providers).♻️ Proposed fix
Move the component definition outside
Providers:+function RuleProviderRulesPanel({ name, behavior }: { name: string; behavior?: string }) { + const { data, isLoading, isError } = useQuery({ + queryKey: ['rule-provider-rules', name], + queryFn: () => getRuleProviderRules(name), + }); + // ... rest of component +} export function Providers() { // ... - function RuleProviderRulesPanel({ name, behavior }: { name: string; behavior?: string }) { - // ... - } // ... }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-dashboard/src/pages/Providers.tsx` around lines 117 - 172, The RuleProviderRulesPanel function is currently declared inside the Providers component causing it to be recreated on every render and triggering unnecessary React Query remounts; move the entire RuleProviderRulesPanel definition to module scope (outside the Providers component) so it's defined once, keep its signature RuleProviderRulesPanel({ name, behavior }: { name: string; behavior?: string }) unchanged, ensure any values it used from Providers are passed in as props (e.g., name and behavior) and that useQuery remains using ['rule-provider-rules', name] as its queryKey; after moving, verify imports/types still resolve and that Providers simply renders <RuleProviderRulesPanel name={...} behavior={...} />.clash-dashboard/src/pages/Overview.tsx (2)
87-100: Addtype="button"to prevent accidental form submission.The
ToggleSwitchbutton lacks an explicittypeattribute. If this component is ever used inside a<form>, it will default totype="submit"and could trigger unintended form submissions.Suggested fix
return ( <button + type="button" onClick={() => onChange(!value)} className="w-10 h-6 rounded-full transition-colors flex-shrink-0 relative" style={{ background: value ? '#34c759' : 'rgba(0,0,0,0.15)' }} >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-dashboard/src/pages/Overview.tsx` around lines 87 - 100, The ToggleSwitch component's button defaults to type="submit" inside forms which can cause accidental form submissions; update the button in function ToggleSwitch to include an explicit type="button" attribute so clicking the switch never submits a form (locate the button element inside ToggleSwitch and add type="button").
175-185: Consider debouncing config patches.Each input field triggers an immediate
patchConfigsAPI call on blur. If a user quickly tabs through multiple fields, this could cause several sequential listener restarts on the backend. Consider batching changes or adding a "Save" button for the config section.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-dashboard/src/pages/Overview.tsx` around lines 175 - 185, Multiple rapid onBlur calls are causing immediate patchConfigs calls; change the patch behavior to batch/debounce updates instead of mutating on every field blur: either (A) buffer incoming PatchableConfig objects in local component state (e.g., an editsRef or edits state) and start/refresh a debounce timer in the patch(fields) function that calls patchMutation.mutate(combinedEdits) only after a short delay, or (B) switch to a controlled edit form UI that writes to local state and add an explicit "Save" button that calls patchMutation.mutate once with the accumulated changes; update references to patchMutation, patch(fields), and patchConfigs (or alternatively use reloadMutation/reloadConfigs if you need to explicitly refresh) so only the debounced/batched payload is sent to the API.clash-lib/src/app/inbound/manager.rs (1)
614-624: Port change detection extracts listeners even when port value is unchanged.The
extract_ifpredicate checksports.port.is_some()rather than comparing against the current port value. This means a PATCH request withport: 7890will restart the HTTP listener even if it's already on port 7890.This is a minor inefficiency rather than a bug. Consider comparing against the current port to avoid unnecessary restarts:
Example for HTTP listener
-InboundOpts::Http { .. } => ports.port.is_some(), +InboundOpts::Http { common_opts } => { + ports.port.is_some_and(|p| p != common_opts.port) +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-lib/src/app/inbound/manager.rs` around lines 614 - 624, The extract_if predicate in the port-change detection uses only ports.port.is_some() (for InboundOpts::Http, InboundOpts::Socks, InboundOpts::Mixed, etc.) which causes listeners to be selected even when the port value is unchanged; update the predicate used by extract_if to compare the incoming port value against the currently active listener port (e.g., check ports.port == Some(current_http_port) for InboundOpts::Http) so only entries whose port actually changed are extracted—locate the current listener port from the active listener/state (listener map or struct used to track running listeners) and perform equality comparison for each variant (ports.port, ports.socks_port, ports.mixed_port, tproxy_port, redir_port) before returning true.clash-dashboard/src/pages/Connections.tsx (1)
52-53: Consider adding error handling for mutations.The close mutations don't provide user feedback on failure. Consider adding
onErrorcallbacks to show a toast/alert if closing connections fails.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-dashboard/src/pages/Connections.tsx` around lines 52 - 53, The closeAllMutation and closeOneMutation created with useMutation({ mutationFn: closeAllConnections }) and useMutation({ mutationFn: closeConnection }) lack error handling and user feedback; update these two mutations to pass onError callbacks to useMutation that display a toast/alert (or set error state) when the mutation fails, include the mutation name in the message for clarity, and optionally use onSuccess to clear errors or show success feedback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@clash-dashboard/src/components/Layout.tsx`:
- Around line 45-60: The nav icon links in the Layout component render only an
icon on small screens and lack accessible names; update the NavLink created in
the navItems.map callback (the JSX that maps to <NavLink key={to} ...>) to
include an aria-label prop set to the link label (aria-label={label}) so screen
readers receive the visible label even when the <span> is hidden; ensure you add
the aria-label on the NavLink element that wraps <Icon size={14} /> and the
hidden <span>.
In `@clash-dashboard/src/lib/api.ts`:
- Around line 222-229: The RuleProvider interface is missing the
backend-returned type field; update the RuleProvider definition to include a
type: string property (similar to ProxyProvider) so it matches the Rust
backend's as_map() response and other provider interfaces; modify the
RuleProvider interface declaration to add type: string alongside name,
vehicleType, etc.
In `@clash-dashboard/src/pages/Logs.tsx`:
- Around line 75-110: The toolbar in Logs.tsx is using hardcoded colors (e.g.
the h1 inline style color '#1d1d1f', button inactive color '#6e6e73',
LEVEL_ACTIVE_STYLE entries, and translucent black backgrounds on the segmented
control and separator) which break dark-theme contrast; update Logs.tsx to use
the theme-aware CSS variables/classes used by the shell header instead of
literal hex/rgba values: replace the h1 inline style, the per-button inline
style branch in the LEVELS map, the segmented-control background, the separator
background, and the Pause/Resume and Clear button inline styles to reference
existing theme CSS variables or utility classes (or add new variables in
src/index.css if needed) so colors switch correctly with the dark theme (refer
to LEVELS, LEVEL_ACTIVE_STYLE, setLevel, setPaused, and the top-level h1 in this
file to locate spots to change).
- Around line 54-59: The buffered history is being stamped with client reconnect
time because useEffect (the block updating setLogs with lastMessage) uses new
Date() instead of the event's original timestamp; update the logic in the effect
that appends logs so it uses the timestamp carried by the LogEntry/LogEvent
payload (e.g., lastMessage.ts, lastMessage.timestamp, or lastMessage.time) and
format that for display (fall back to the existing toLocaleTimeString only if
the event timestamp is missing); keep the id generation via logId and preserve
the slice(-500) retention behavior.
In `@clash-dashboard/src/pages/Proxies.tsx`:
- Around line 171-184: runHealthcheck currently only calls
healthcheckProvider(provider.name) causing inconsistent behavior with
Providers.tsx which calls getProxyDelay for each proxy; update runHealthcheck to
perform the same per-proxy delay checks (call getProxyDelay for each proxy in
provider.proxies, e.g., await Promise.allSettled(provider.proxies.map(p =>
getProxyDelay(p.name))) with individual try/catch handling) before calling
healthcheckProvider(provider.name), or alternatively rename/run a different
"quick" vs "full" test to make the distinction explicit; touch the
runHealthcheck function and use the same helper getProxyDelay and
healthcheckProvider symbols so behavior matches or is intentionally different.
In `@clash-lib/build.rs`:
- Around line 47-61: The build script's build_dashboard() currently treats
missing ../clash-dashboard or npm as fatal (it checks CARGO_FEATURE_DASHBOARD,
calls PathBuf::canonicalize which errors, then runs npm), making default builds
fail; change build_dashboard() so that if canonicalize() for the dashboard_dir
fails it logs a warning and returns Ok(()) instead of mapping to an anyhow
error, and before invoking npm (the place that runs "npm run build") check for
npm availability (e.g., attempt to spawn "npm --version" or use which) and if
npm is missing or the spawn fails, log a non-fatal warning and return Ok(()),
ensuring CARGO_FEATURE_DASHBOARD remains respected but missing dashboard/npm
produce a silent fallback rather than aborting the whole build.
In `@clash-lib/src/app/api/handlers/provider.rs`:
- Around line 281-284: The handler in provider.rs currently calls
p.list_rules(500) which silently truncates results; change the API to either
accept explicit paging/query parameters (e.g. limit and offset/page) and pass
them into list_rules (or the provider's paging API), or keep the full list but
return truncation metadata (e.g. total_count, limit, truncated: true) alongside
"rules" so callers know results were cut; update the request handling in the
provider route to parse query params (limit/offset) and return a JSON object
containing rules plus explicit metadata (total/limit/offset/truncated) instead
of hardcoding 500.
In `@clash-lib/src/app/api/websocket.rs`:
- Around line 143-156: Subscribe to the broadcast channel before replaying
buffered logs: call state.log_source_tx.subscribe() first (assign to rx), then
take the snapshot of state.recent_logs (lock state.recent_logs and collect
history) and send the buffered messages; while sending the history, drain rx for
any concurrent events and deduplicate by sequence/timestamp (compare each
incoming event from rx against the last sequence/timestamp in the snapshot) so
you don't miss or double-send messages; update the logic around
state.recent_logs.lock(), state.log_source_tx.subscribe(), and socket.send to
perform subscribe-first + sequence-based dedupe/merge.
---
Outside diff comments:
In `@clash-lib/src/app/api/handlers/config.rs`:
- Around line 306-358: When handling payload.bind_address in the PATCH handler,
setting the new bind address via inbound_manager.set_bind_address(...) requires
a full restart, but the current flow can leave port_changed true (set by
inbound_manager.change_ports(...)) which causes restart_idle() to be used; after
applying the bind address (the block that calls
inbound_manager.set_bind_address), clear or set port_changed = false so the
later restart branch will call inbound_manager.restart() instead of
inbound_manager.restart_idle(); mirror the allow_lan handling where port_changed
is reset to force a full restart.
---
Duplicate comments:
In `@clash-dashboard/src/components/Layout.tsx`:
- Around line 38-55: The span in Layout.tsx and the NavLink class strings are
hardcoding light-mode hex colors (e.g. style={{ color: '#1d1d1f' }},
'text-[`#0071e3`]', 'text-[`#6e6e73`]', 'hover:text-[`#1d1d1f`]') which override
dark-mode rules; remove the inline style and replace those hardcoded hex classes
with theme-aware CSS (use existing index.css dark-mode variables/classes or
Tailwind dark: variants such as neutral/text color classes or CSS custom
properties) so the brand, nav and status text inherit proper light/dark colors;
update the NavLink mapping and the span in Layout.tsx (and the similar
occurrences around the status block) to use the theme-aware classes instead of
hex values.
---
Nitpick comments:
In `@clash-dashboard/src/components/ProxyGroups.tsx`:
- Around line 7-16: Duplicate helper functions like getLatencyColor and
getLastDelay are present in ProxyGroups.tsx, Providers.tsx and Proxies.tsx;
extract these into a single shared utility module (e.g., export functions
getLatencyColor, getLastDelay, and any other repeated helpers) and replace the
local definitions with imports from that module in ProxyGroups.tsx,
Providers.tsx and Proxies.tsx so the components use the centralized
implementations; ensure exported function names match (getLatencyColor,
getLastDelay) and update all import sites accordingly, removing the duplicated
code blocks from each file.
In `@clash-dashboard/src/pages/Connections.tsx`:
- Around line 52-53: The closeAllMutation and closeOneMutation created with
useMutation({ mutationFn: closeAllConnections }) and useMutation({ mutationFn:
closeConnection }) lack error handling and user feedback; update these two
mutations to pass onError callbacks to useMutation that display a toast/alert
(or set error state) when the mutation fails, include the mutation name in the
message for clarity, and optionally use onSuccess to clear errors or show
success feedback.
In `@clash-dashboard/src/pages/Overview.tsx`:
- Around line 87-100: The ToggleSwitch component's button defaults to
type="submit" inside forms which can cause accidental form submissions; update
the button in function ToggleSwitch to include an explicit type="button"
attribute so clicking the switch never submits a form (locate the button element
inside ToggleSwitch and add type="button").
- Around line 175-185: Multiple rapid onBlur calls are causing immediate
patchConfigs calls; change the patch behavior to batch/debounce updates instead
of mutating on every field blur: either (A) buffer incoming PatchableConfig
objects in local component state (e.g., an editsRef or edits state) and
start/refresh a debounce timer in the patch(fields) function that calls
patchMutation.mutate(combinedEdits) only after a short delay, or (B) switch to a
controlled edit form UI that writes to local state and add an explicit "Save"
button that calls patchMutation.mutate once with the accumulated changes; update
references to patchMutation, patch(fields), and patchConfigs (or alternatively
use reloadMutation/reloadConfigs if you need to explicitly refresh) so only the
debounced/batched payload is sent to the API.
In `@clash-dashboard/src/pages/Providers.tsx`:
- Around line 117-172: The RuleProviderRulesPanel function is currently declared
inside the Providers component causing it to be recreated on every render and
triggering unnecessary React Query remounts; move the entire
RuleProviderRulesPanel definition to module scope (outside the Providers
component) so it's defined once, keep its signature RuleProviderRulesPanel({
name, behavior }: { name: string; behavior?: string }) unchanged, ensure any
values it used from Providers are passed in as props (e.g., name and behavior)
and that useQuery remains using ['rule-provider-rules', name] as its queryKey;
after moving, verify imports/types still resolve and that Providers simply
renders <RuleProviderRulesPanel name={...} behavior={...} />.
In `@clash-dashboard/src/pages/Proxies.tsx`:
- Around line 358-476: The provider card rendering in Proxies.tsx is duplicated
from Providers.tsx; extract a reusable ProviderCard component that accepts props
like provider, isExpanded, isTesting, isUpdating, latencyMap,
toggleProviderExpanded, runHealthcheck, runUpdate, getLastDelay and
getLatencyColor, move the JSX block that starts with the Providers map (the card
container, header buttons, expansion logic and proxy grid) into that component,
and replace the inline map in Proxies.tsx with a call to <ProviderCard ...>
passing the needed handlers/state (use existing identifiers
toggleProviderExpanded, runHealthcheck, runUpdate, latencyMap, getLastDelay,
getLatencyColor and expandedProviders/testingProviders/updatingProviders to
compute booleans). Ensure ProviderCard emits the same events and preserves keys
(proxy.name/provider.name) and styles so behavior remains identical.
In `@clash-lib/src/app/inbound/manager.rs`:
- Around line 614-624: The extract_if predicate in the port-change detection
uses only ports.port.is_some() (for InboundOpts::Http, InboundOpts::Socks,
InboundOpts::Mixed, etc.) which causes listeners to be selected even when the
port value is unchanged; update the predicate used by extract_if to compare the
incoming port value against the currently active listener port (e.g., check
ports.port == Some(current_http_port) for InboundOpts::Http) so only entries
whose port actually changed are extracted—locate the current listener port from
the active listener/state (listener map or struct used to track running
listeners) and perform equality comparison for each variant (ports.port,
ports.socks_port, ports.mixed_port, tproxy_port, redir_port) before returning
true.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 6267140a-4f6b-46ca-b9db-8028bf883b29
⛔ Files ignored due to path filters (13)
clash-bin/tests/data/config/public/apple-touch-icon-precomposed.pngis excluded by!**/*.pngclash-bin/tests/data/config/public/assets/inter-latin-400-normal.0364d368.woff2is excluded by!**/*.woff2clash-bin/tests/data/config/public/assets/inter-latin-400-normal.3ea830d4.woffis excluded by!**/*.woffclash-bin/tests/data/config/public/assets/inter-latin-800-normal.a51ac27d.woff2is excluded by!**/*.woff2clash-bin/tests/data/config/public/assets/inter-latin-800-normal.d08d7178.woffis excluded by!**/*.woffclash-bin/tests/data/config/public/assets/roboto-mono-latin-400-normal.7295944e.woff2is excluded by!**/*.woff2clash-bin/tests/data/config/public/assets/roboto-mono-latin-400-normal.dffdffa7.woffis excluded by!**/*.woffclash-bin/tests/data/config/public/yacd-128.pngis excluded by!**/*.pngclash-bin/tests/data/config/public/yacd-64.pngis excluded by!**/*.pngclash-bin/tests/data/config/public/yacd.icois excluded by!**/*.icoclash-dashboard/package-lock.jsonis excluded by!**/package-lock.jsonclash-dashboard/public/logo.pngis excluded by!**/*.pngclash-dashboard/src/assets/logo.pngis excluded by!**/*.png
📒 Files selected for processing (67)
clash-bin/Cargo.tomlclash-bin/tests/data/config/public/CNAMEclash-bin/tests/data/config/public/_headersclash-bin/tests/data/config/public/assets/Config.39d8d2ef.cssclash-bin/tests/data/config/public/assets/Config.c09e8dbe.jsclash-bin/tests/data/config/public/assets/Connections.e48eac36.jsclash-bin/tests/data/config/public/assets/Connections.fb8ea59b.cssclash-bin/tests/data/config/public/assets/Fab.a0a7e573.cssclash-bin/tests/data/config/public/assets/Fab.ef67ff10.jsclash-bin/tests/data/config/public/assets/Logs.4b8e75d1.cssclash-bin/tests/data/config/public/assets/Logs.ac990610.jsclash-bin/tests/data/config/public/assets/Proxies.16b46af4.jsclash-bin/tests/data/config/public/assets/Proxies.3fa3509d.cssclash-bin/tests/data/config/public/assets/Rules.70e6962f.jsclash-bin/tests/data/config/public/assets/Rules.e03c54a8.cssclash-bin/tests/data/config/public/assets/Select.1e55eba1.cssclash-bin/tests/data/config/public/assets/Select.6c389032.jsclash-bin/tests/data/config/public/assets/TextFitler.61537a57.jsclash-bin/tests/data/config/public/assets/TextFitler.b21c0577.cssclash-bin/tests/data/config/public/assets/chart-lib.a8ad03fd.jsclash-bin/tests/data/config/public/assets/chevron-down.dd238e96.jsclash-bin/tests/data/config/public/assets/debounce.c2d20996.jsclash-bin/tests/data/config/public/assets/en.fb34eaf7.jsclash-bin/tests/data/config/public/assets/index.171f553a.jsclash-bin/tests/data/config/public/assets/index.8bb012c6.jsclash-bin/tests/data/config/public/assets/index.92e2d967.jsclash-bin/tests/data/config/public/assets/index.b38debfc.cssclash-bin/tests/data/config/public/assets/index.esm.e4dd1508.jsclash-bin/tests/data/config/public/assets/logs.43986220.jsclash-bin/tests/data/config/public/assets/play.7b1a5f99.jsclash-bin/tests/data/config/public/assets/useRemainingViewPortHeight.7395542b.jsclash-bin/tests/data/config/public/assets/zh.9b79b7bf.jsclash-bin/tests/data/config/public/index.htmlclash-bin/tests/data/config/public/manifest.webmanifestclash-bin/tests/data/config/public/registerSW.jsclash-bin/tests/data/config/public/sw.jsclash-bin/tests/data/config/rules.yamlclash-dashboard/package.jsonclash-dashboard/src/App.tsxclash-dashboard/src/components/Layout.tsxclash-dashboard/src/components/ProxyGroups.tsxclash-dashboard/src/components/ui/button.tsxclash-dashboard/src/index.cssclash-dashboard/src/lib/api.tsclash-dashboard/src/lib/settings.tsclash-dashboard/src/pages/Connections.tsxclash-dashboard/src/pages/DNS.tsxclash-dashboard/src/pages/Logs.tsxclash-dashboard/src/pages/Overview.tsxclash-dashboard/src/pages/Providers.tsxclash-dashboard/src/pages/Proxies.tsxclash-dashboard/src/pages/Rules.tsxclash-dashboard/src/pages/Settings.tsxclash-dashboard/vite.config.tsclash-lib/Cargo.tomlclash-lib/build.rsclash-lib/src/app/api/embedded_dashboard.rsclash-lib/src/app/api/handlers/config.rsclash-lib/src/app/api/handlers/log.rsclash-lib/src/app/api/handlers/provider.rsclash-lib/src/app/api/mod.rsclash-lib/src/app/api/runner.rsclash-lib/src/app/api/websocket.rsclash-lib/src/app/inbound/manager.rsclash-lib/src/app/remote_content_manager/providers/rule_provider/provider.rsclash-lib/src/app/router/mod.rsclash-lib/src/lib.rs
💤 Files with no reviewable changes (32)
- clash-bin/tests/data/config/public/assets/Rules.e03c54a8.css
- clash-bin/tests/data/config/public/assets/TextFitler.b21c0577.css
- clash-bin/tests/data/config/public/assets/Proxies.3fa3509d.css
- clash-bin/tests/data/config/public/registerSW.js
- clash-bin/tests/data/config/public/manifest.webmanifest
- clash-bin/tests/data/config/public/_headers
- clash-bin/tests/data/config/public/assets/Config.39d8d2ef.css
- clash-bin/tests/data/config/public/assets/Select.1e55eba1.css
- clash-bin/tests/data/config/public/assets/Logs.4b8e75d1.css
- clash-bin/tests/data/config/public/index.html
- clash-bin/tests/data/config/public/assets/Fab.a0a7e573.css
- clash-bin/tests/data/config/public/assets/index.b38debfc.css
- clash-bin/tests/data/config/public/assets/Config.c09e8dbe.js
- clash-bin/tests/data/config/public/assets/useRemainingViewPortHeight.7395542b.js
- clash-bin/tests/data/config/public/assets/zh.9b79b7bf.js
- clash-bin/tests/data/config/public/assets/Proxies.16b46af4.js
- clash-bin/tests/data/config/public/assets/debounce.c2d20996.js
- clash-bin/tests/data/config/public/assets/chevron-down.dd238e96.js
- clash-bin/tests/data/config/public/assets/en.fb34eaf7.js
- clash-bin/tests/data/config/public/assets/Select.6c389032.js
- clash-bin/tests/data/config/public/assets/Rules.70e6962f.js
- clash-bin/tests/data/config/public/assets/Fab.ef67ff10.js
- clash-bin/tests/data/config/public/assets/Connections.fb8ea59b.css
- clash-bin/tests/data/config/public/assets/Connections.e48eac36.js
- clash-bin/tests/data/config/public/assets/index.92e2d967.js
- clash-bin/tests/data/config/public/assets/play.7b1a5f99.js
- clash-bin/tests/data/config/public/CNAME
- clash-bin/tests/data/config/public/assets/TextFitler.61537a57.js
- clash-bin/tests/data/config/public/sw.js
- clash-bin/tests/data/config/public/assets/logs.43986220.js
- clash-bin/tests/data/config/public/assets/index.esm.e4dd1508.js
- clash-bin/tests/data/config/public/assets/Logs.ac990610.js
✅ Files skipped from review due to trivial changes (3)
- clash-dashboard/package.json
- clash-dashboard/vite.config.ts
- clash-dashboard/src/lib/settings.ts
🚧 Files skipped from review as they are similar to previous changes (6)
- clash-dashboard/src/App.tsx
- clash-lib/src/app/api/mod.rs
- clash-dashboard/src/pages/Settings.tsx
- clash-dashboard/src/pages/DNS.tsx
- clash-dashboard/src/components/ui/button.tsx
- clash-lib/Cargo.toml
| useEffect(() => { | ||
| if (!lastMessage) return; | ||
| setLogs((prev) => [ | ||
| ...prev, | ||
| { ...lastMessage, id: ++logId, ts: new Date().toLocaleTimeString() }, | ||
| ].slice(-500)); |
There was a problem hiding this comment.
Buffered history is stamped with the reconnect time, not the log time.
Now that /ws/logs replays prior events, every replayed entry gets new Date() from the client and appears to happen at connect time. That destroys the chronology the history feature is supposed to preserve. Use a timestamp from LogEntry/LogEvent, or add one to the websocket payload.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@clash-dashboard/src/pages/Logs.tsx` around lines 54 - 59, The buffered
history is being stamped with client reconnect time because useEffect (the block
updating setLogs with lastMessage) uses new Date() instead of the event's
original timestamp; update the logic in the effect that appends logs so it uses
the timestamp carried by the LogEntry/LogEvent payload (e.g., lastMessage.ts,
lastMessage.timestamp, or lastMessage.time) and format that for display (fall
back to the existing toLocaleTimeString only if the event timestamp is missing);
keep the id generation via logId and preserve the slice(-500) retention
behavior.
| <h1 className="text-2xl font-bold tracking-tight" style={{ color: '#1d1d1f' }}>Logs</h1> | ||
| <WsStatus state={readyState} /> | ||
| </div> | ||
| <div className="flex items-center gap-2"> | ||
| {/* Level segmented control */} | ||
| <div className="flex items-center p-1 rounded-full" style={{ background: 'rgba(0,0,0,0.06)' }}> | ||
| {LEVELS.map((l) => { | ||
| const active = level === l; | ||
| return ( | ||
| <button | ||
| key={l} | ||
| onClick={() => setLevel(l)} | ||
| className="px-3 py-1.5 rounded-full text-[12px] font-medium capitalize transition-all" | ||
| style={active ? LEVEL_ACTIVE_STYLE[l] : { color: '#6e6e73' }} | ||
| > | ||
| {l} | ||
| </button> | ||
| ); | ||
| })} | ||
| </div> | ||
|
|
||
| <div className="w-px h-4 mx-1" style={{ background: 'rgba(0,0,0,0.08)' }} /> | ||
|
|
||
| <button | ||
| onClick={() => setPaused((p) => !p)} | ||
| className="px-3 py-1.5 rounded-full text-[12px] font-medium transition-colors" | ||
| style={paused | ||
| ? { background: 'rgba(255,149,0,0.1)', color: '#ff9500' } | ||
| : { background: 'rgba(0,0,0,0.05)', color: '#6e6e73' }} | ||
| > | ||
| {paused ? 'Resume' : 'Pause'} | ||
| </button> | ||
| <button | ||
| onClick={() => setLogs([])} | ||
| className="px-3 py-1.5 rounded-full text-[12px] font-medium transition-colors" | ||
| style={{ background: 'rgba(0,0,0,0.05)', color: '#6e6e73' }} |
There was a problem hiding this comment.
The toolbar is locked to the light palette.
#1d1d1f, #6e6e73, and the translucent black backgrounds work on the light theme, but they become low-contrast against the dark theme introduced in src/index.css. These should be theme-aware classes/variables like the shell header.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@clash-dashboard/src/pages/Logs.tsx` around lines 75 - 110, The toolbar in
Logs.tsx is using hardcoded colors (e.g. the h1 inline style color '#1d1d1f',
button inactive color '#6e6e73', LEVEL_ACTIVE_STYLE entries, and translucent
black backgrounds on the segmented control and separator) which break dark-theme
contrast; update Logs.tsx to use the theme-aware CSS variables/classes used by
the shell header instead of literal hex/rgba values: replace the h1 inline
style, the per-button inline style branch in the LEVELS map, the
segmented-control background, the separator background, and the Pause/Resume and
Clear button inline styles to reference existing theme CSS variables or utility
classes (or add new variables in src/index.css if needed) so colors switch
correctly with the dark theme (refer to LEVELS, LEVEL_ACTIVE_STYLE, setLevel,
setPaused, and the top-level h1 in this file to locate spots to change).
| async function runHealthcheck(provider: ProxyProvider) { | ||
| setTestingProviders((s) => new Set(s).add(provider.name)); | ||
| setExpandedProviders((prev) => new Set(prev).add(provider.name)); | ||
| try { | ||
| await healthcheckProvider(provider.name); | ||
| await queryClient.invalidateQueries({ queryKey: ['providers'] }); | ||
| } finally { | ||
| setTestingProviders((s) => { | ||
| const next = new Set(s); | ||
| next.delete(provider.name); | ||
| return next; | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
Inconsistent healthcheck behavior between pages.
runHealthcheck here only calls healthcheckProvider(provider.name), while in Providers.tsx (lines 65-86) it also runs individual getProxyDelay calls for each proxy before the healthcheck. Users clicking "Test" will see different behavior depending on which page they're on.
Consider aligning the behavior or making the difference intentional (e.g., "Quick Test" vs "Full Test").
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@clash-dashboard/src/pages/Proxies.tsx` around lines 171 - 184, runHealthcheck
currently only calls healthcheckProvider(provider.name) causing inconsistent
behavior with Providers.tsx which calls getProxyDelay for each proxy; update
runHealthcheck to perform the same per-proxy delay checks (call getProxyDelay
for each proxy in provider.proxies, e.g., await
Promise.allSettled(provider.proxies.map(p => getProxyDelay(p.name))) with
individual try/catch handling) before calling
healthcheckProvider(provider.name), or alternatively rename/run a different
"quick" vs "full" test to make the distinction explicit; touch the
runHealthcheck function and use the same helper getProxyDelay and
healthcheckProvider symbols so behavior matches or is intentionally different.
| let rules = p.list_rules(500).await; | ||
| let mut res = HashMap::new(); | ||
| res.insert("rules", rules); | ||
| axum::response::Json(res).into_response() |
There was a problem hiding this comment.
Don't silently cap rule lists at 500.
list_rules(500) truncates larger providers without telling the caller, so the new expansion view can look complete while omitting entries. Please make the limit explicit via query/pagination, or return truncation metadata with the response.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@clash-lib/src/app/api/handlers/provider.rs` around lines 281 - 284, The
handler in provider.rs currently calls p.list_rules(500) which silently
truncates results; change the API to either accept explicit paging/query
parameters (e.g. limit and offset/page) and pass them into list_rules (or the
provider's paging API), or keep the full list but return truncation metadata
(e.g. total_count, limit, truncated: true) alongside "rules" so callers know
results were cut; update the request handling in the provider route to parse
query params (limit/offset) and return a JSON object containing rules plus
explicit metadata (total/limit/offset/truncated) instead of hardcoding 500.
| // Replay recent log history so the client sees context immediately. | ||
| let history: Vec<String> = { | ||
| let buf = state.recent_logs.lock().unwrap(); | ||
| buf.iter() | ||
| .filter_map(|evt| serde_json::to_string(evt).ok()) | ||
| .collect() | ||
| }; | ||
| for s in history { | ||
| if socket.send(Message::Text(s.into())).await.is_err() { | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| let mut rx = state.log_source_tx.subscribe(); |
There was a problem hiding this comment.
Subscribe before replaying buffered logs.
Anything emitted after the snapshot at Line 145 but before the subscription at Line 156 is in neither stream, so reconnecting clients can still miss exactly the logs this replay is meant to preserve. This needs a subscribe-first bridge with dedupe/sequence handling, or another atomic handoff.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@clash-lib/src/app/api/websocket.rs` around lines 143 - 156, Subscribe to the
broadcast channel before replaying buffered logs: call
state.log_source_tx.subscribe() first (assign to rx), then take the snapshot of
state.recent_logs (lock state.recent_logs and collect history) and send the
buffered messages; while sending the history, drain rx for any concurrent events
and deduplicate by sequence/timestamp (compare each incoming event from rx
against the last sequence/timestamp in the snapshot) so you don't miss or
double-send messages; update the logic around state.recent_logs.lock(),
state.log_source_tx.subscribe(), and socket.send to perform subscribe-first +
sequence-based dedupe/merge.
d84c4f0 to
8203a05
Compare
There was a problem hiding this comment.
Actionable comments posted: 13
♻️ Duplicate comments (4)
clash-dashboard/src/pages/Logs.tsx (2)
16-22:⚠️ Potential issue | 🟠 MajorThese toolbar/status colors still do not follow the dark theme.
The page now has a dark theme in
src/index.css, but these segments still hard-code the light palette (#1d1d1f,#6e6e73, translucent black backgrounds). In dark mode the header and controls drop to very low contrast. Please switch them to theme-aware variables/classes so the page stays readable.Also applies to: 31-40, 75-110
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-dashboard/src/pages/Logs.tsx` around lines 16 - 22, LEVEL_ACTIVE_STYLE is using hard-coded hex colors that break dark theme; replace these static color values with theme-aware CSS variables or theme tokens (e.g., use var(--bg-toolbar), var(--text-toolbar), or the app's theme context values) and update all occurrences referenced in Logs.tsx (LEVEL_ACTIVE_STYLE and the other toolbar/status color maps around the 31-40 and 75-110 regions) to read from those variables or compute colors from the theme provider; ensure fallback values exist for older themes and adjust the component classes to apply the CSS variables (or call the theme hook) so the header and controls maintain proper contrast in dark mode.
24-27:⚠️ Potential issue | 🟠 MajorPreserve server timestamps for replayed history.
Line 58 assigns
new Date()on receipt. Replayed entries from/ws/logstherefore all appear at reconnect time, which defeats the history feature’s chronology. Use a timestamp carried inLogEntry, or add one on the backend and render that here.Also applies to: 54-59
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-dashboard/src/pages/Logs.tsx` around lines 24 - 27, The handler is overwriting server timestamps by assigning new Date() when creating TimestampedLog items; update the code that constructs TimestampedLog (interface TimestampedLog and the receiver that currently sets ts = new Date()) to use the timestamp provided on the incoming LogEntry (e.g., use LogEntry.timestamp or a provided ts field), parsing/formatting that value into the TimestampedLog.ts string instead of generating a client-side date; if the backend lacks a timestamp field add one there and consume it here so replayed entries keep their original chronology.clash-lib/src/app/api/websocket.rs (1)
143-156:⚠️ Potential issue | 🟠 MajorSubscribe before replaying the snapshot.
Line 144 snapshots
recent_logs, and Line 156 subscribes afterwards. Any event emitted in that window is in neither stream, so reconnecting clients can still miss logs. This needs a subscribe-first handoff with dedupe/merge instead of replay-then-subscribe.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-lib/src/app/api/websocket.rs` around lines 143 - 156, Take the subscription before taking the snapshot: call state.log_source_tx.subscribe() first to get rx, then lock state.recent_logs and build the history Vec<String>; send the history to the socket, then start receiving from rx. To avoid duplicates, record the serialized history into a HashSet and when processing messages from rx (or when merging streams) skip any event whose serialized form is already in that set; reference state.log_source_tx.subscribe(), state.recent_logs, history, rx, socket.send and Message::Text for where to change the ordering and add the dedupe set/logic.clash-dashboard/src/pages/Overview.tsx (1)
175-185:⚠️ Potential issue | 🟠 MajorSerialize the config PATCH writes from this editor.
These controls still call
patchMutation.mutate()while an earlier PATCH is in flight, so rapid toggles or blur commits can race and leave the server on an older value. The mode buttons were guarded already, but the rest of the config editor still has the same write-order problem.Also applies to: 337-372
🧹 Nitpick comments (10)
clash-dashboard/src/pages/Rules.tsx (2)
6-27: Use the precomputed badgebackgroundto avoid style drift.
getRuleTypeBadgeStylereturnsbackground, but rendering recomputes it fromcolor. This makes the helper partially redundant and easier to desync later.Suggested diff
- <span - className="text-[11px] font-semibold tracking-wider px-2 py-0.5 rounded-full flex-shrink-0" - style={{ background: typeStyle.color + '22', color: typeStyle.color }} - > + <span + className="text-[11px] font-semibold tracking-wider px-2 py-0.5 rounded-full flex-shrink-0" + style={{ background: typeStyle.background, color: typeStyle.color }} + >Also applies to: 102-103
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-dashboard/src/pages/Rules.tsx` around lines 6 - 27, The helper getRuleTypeBadgeStyle currently returns a precomputed background but downstream rendering recomputes the background from the color (causing drift); update all badge render sites (the code that calls getRuleTypeBadgeStyle — including the usages around the earlier switch and the locations referenced at lines ~102-103) to consume and apply the returned background value directly (use style.background or the returned.background) instead of deriving it from returned.color, so the badge color/background stay in sync with the single source of truth.
36-41: Normalize the search token once before filtering.
search.toLowerCase()is recomputed for every rule/field. Precomputing improves readability and avoids repeated work on large rule lists.Suggested diff
- const filtered = rules.filter( - (r) => - r.type.toLowerCase().includes(search.toLowerCase()) || - (r.payload ?? '').toLowerCase().includes(search.toLowerCase()) || - r.proxy.toLowerCase().includes(search.toLowerCase()) - ); + const needle = search.trim().toLowerCase(); + const filtered = !needle + ? rules + : rules.filter( + (r) => + r.type.toLowerCase().includes(needle) || + (r.payload ?? '').toLowerCase().includes(needle) || + r.proxy.toLowerCase().includes(needle) + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-dashboard/src/pages/Rules.tsx` around lines 36 - 41, Precompute a lowercase search token once and use it inside the rules.filter predicate instead of calling search.toLowerCase() repeatedly; create a local const like searchLower = search.toLowerCase() above the filtered = rules.filter(...) and replace each search.toLowerCase() occurrence in the predicate with searchLower (keep the existing payload nullish fallback and calls to r.type.toLowerCase()/r.proxy.toLowerCase()).clash-dashboard/src/components/ui/table.tsx (1)
68-79: Add a default header scope for better table semantics.Consider defaulting
scope="col"inTableHeadwhen consumers don’t provide one.Suggested patch
-function TableHead({ className, ...props }: React.ComponentProps<"th">) { +function TableHead({ className, scope, ...props }: React.ComponentProps<"th">) { return ( <th data-slot="table-head" + scope={scope ?? "col"} className={cn( "h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0", className )} {...props} /> ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-dashboard/src/components/ui/table.tsx` around lines 68 - 79, TableHead currently renders a <th> without a default scope which hurts table semantics; update the TableHead component (function TableHead) to default scope="col" when callers don't supply a scope prop by reading scope from props and passing scope={scope ?? "col"} into the returned th element so consumer-provided scope still overrides the default.clash-dashboard/src/pages/Proxies.tsx (1)
151-159: Batch latency state updates to avoid N rerenders per test run.
setLatencyMapis called once per proxy. For large groups this creates unnecessary render churn.⚡ Suggested batching approach
- await Promise.allSettled( - group.all.map(async (name) => { - try { - const res = await getProxyDelay(name, TEST_URL, TEST_TIMEOUT); - setLatencyMap((prev) => ({ ...prev, [name]: res.delay })); - } catch { - setLatencyMap((prev) => ({ ...prev, [name]: 0 })); - } - }) - ); + const updates: Record<string, number> = {}; + await Promise.allSettled( + group.all.map(async (name) => { + try { + const res = await getProxyDelay(name, TEST_URL, TEST_TIMEOUT); + updates[name] = res.delay; + } catch { + updates[name] = 0; + } + }) + ); + setLatencyMap((prev) => ({ ...prev, ...updates }));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-dashboard/src/pages/Proxies.tsx` around lines 151 - 159, The per-proxy setLatencyMap calls cause N re-renders; instead, inside the group testing block (the async map over group.all using getProxyDelay), collect each proxy's delay into a local updates object (use Promise.allSettled to await all tests), set failed entries to 0, then call setLatencyMap once with setLatencyMap(prev => ({ ...prev, ...updates })); update the code around the Promise.allSettled mapping so it returns results into that local map and performs a single state update for the whole group.clash-dashboard/src/pages/DNS.tsx (1)
50-77: Add explicit accessible labels to DNS form controls.The input/select/button rely on visual context only. Add
aria-label(or<label htmlFor>+id) so screen readers can identify controls reliably.♿ Suggested change
<input type="text" + aria-label="DNS hostname" placeholder="Hostname (e.g. example.com)" @@ <select value={type} + aria-label="DNS record type" onChange={(e) => setType(e.target.value)} @@ <button onClick={handleQuery} + aria-label="Run DNS query" disabled={loading || !hostname.trim()}Also applies to: 78-85
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-dashboard/src/pages/DNS.tsx` around lines 50 - 77, The hostname input and DNS type select (controlled by state variables hostname/setHostname and type/setType and rendered from DNS_TYPES) lack accessible labels; add either aria-label attributes (e.g. aria-label="Hostname" and aria-label="DNS record type") or an associated <label htmlFor="..."> with matching id attributes on the <input> and <select>, and do the same for the query button that calls handleQuery so screen readers can identify each control.clash-dashboard/src/components/ui/badge.tsx (2)
29-36: ReusebadgeVariants(...)insideBadgeto avoid class drift.Line 33 duplicates the base class string already defined in
badgeVariants(Line 21). Calling the helper here keeps one source of truth.Proposed fix
function Badge({ className, variant = 'default', ...props }: BadgeProps) { return ( <span - className={cn( - 'inline-flex items-center text-[11px] font-semibold px-2 py-0.5 rounded-full', - variantStyles[variant] ?? variantStyles.default, - className - )} + className={cn(badgeVariants({ variant }), className)} {...props} /> ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-dashboard/src/components/ui/badge.tsx` around lines 29 - 36, The Badge component duplicates the base class string instead of reusing the badgeVariants helper; update the Badge function to call badgeVariants (the variant helper defined as badgeVariants) to compute the full className for the <span> (pass the current variant and className into badgeVariants) and remove the hard-coded base class string and direct use of variantStyles inside Badge so all classes come from badgeVariants.
4-27: Keepvariantstrongly typed instead of widening tostring.At line 4,
variantStylesis typed asRecord<string, string>, which makeskeyof typeof variantStyleswiden tostringrather than preserve literal keys. Combined with line 19 acceptingvariant?: string, this allows any string to be passed without compile-time validation of valid variant names. Invalid variants silently fall back to the default.Use
as constand a type alias to preserve literal key safety:Proposed fix
-const variantStyles: Record<string, string> = { +const variantStyles = { default: 'bg-black/[0.06] text-[`#6e6e73`]', secondary: 'bg-black/[0.06] text-[`#6e6e73`]', destructive: 'bg-[`#ff3b30`]/10 text-[`#ff3b30`]', outline: 'border border-black/[0.08] text-[`#1d1d1f`]', ghost: 'text-[`#6e6e73`] hover:bg-black/[0.05]', link: 'text-[`#0071e3`] underline-offset-4 hover:underline', blue: 'bg-[`#0071e3`]/10 text-[`#0071e3`]', green: 'bg-[`#34c759`]/10 text-[`#34c759`]', amber: 'bg-[`#ff9500`]/10 text-[`#ff9500`]', violet: 'bg-[`#af52de`]/10 text-[`#af52de`]', gray: 'bg-black/[0.06] text-[`#6e6e73`]', red: 'bg-[`#ff3b30`]/10 text-[`#ff3b30`]', -} +} as const + +type BadgeVariant = keyof typeof variantStyles -export const badgeVariants = ({ variant = 'default' }: { variant?: string } = {}) => +export const badgeVariants = ({ variant = 'default' }: { variant?: BadgeVariant } = {}) => cn( 'inline-flex items-center text-[11px] font-semibold px-2 py-0.5 rounded-full', variantStyles[variant] ?? variantStyles.default ) interface BadgeProps extends React.ComponentProps<"span"> { - variant?: keyof typeof variantStyles; + variant?: BadgeVariant; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-dashboard/src/components/ui/badge.tsx` around lines 4 - 27, variantStyles is currently typed as Record<string,string>, which widens keyof to string and lets any variant pass; change its declaration to a literal readonly object to preserve literal keys (e.g. const variantStyles = { ... } as const) and remove the Record<> typing, then tighten badgeVariants' parameter type to use the preserved keys (change badgeVariants signature to ({ variant = 'default' }: { variant?: keyof typeof variantStyles } = {})). Also ensure BadgeProps stays as variant?: keyof typeof variantStyles so consumers get compile-time validation.clash-lib/src/app/inbound/manager.rs (1)
615-621: Semantic change:change_portsnow restarts listeners even when port values are unchanged.The predicate changed from checking port equality to checking presence (
ports.port.is_some()). This means if the API payload includes a port field with the same value as the current port, the listener is still extracted, aborted, and reinserted—resulting in a brief interruption even when no actual change occurred.The return value
changed(Line 626) will betrueeven if all port values are identical, which may cause unnecessary restarts upstream.Consider comparing actual values to avoid spurious restarts:
♻️ Proposed fix to compare port values
let listeners: HashMap<InboundOpts, Option<_>> = guard .extract_if(|opts, _| match &opts { - InboundOpts::Http { .. } => ports.port.is_some(), - InboundOpts::Socks { .. } => ports.socks_port.is_some(), - InboundOpts::Mixed { .. } => ports.mixed_port.is_some(), + InboundOpts::Http { common_opts } => { + ports.port.is_some_and(|p| p != common_opts.port) + } + InboundOpts::Socks { common_opts, .. } => { + ports.socks_port.is_some_and(|p| p != common_opts.port) + } + InboundOpts::Mixed { common_opts, .. } => { + ports.mixed_port.is_some_and(|p| p != common_opts.port) + } #[cfg(feature = "tproxy")] - InboundOpts::TProxy { .. } => ports.tproxy_port.is_some(), + InboundOpts::TProxy { common_opts, .. } => { + ports.tproxy_port.is_some_and(|p| p != common_opts.port) + } #[cfg(feature = "redir")] - InboundOpts::Redir { .. } => ports.redir_port.is_some(), + InboundOpts::Redir { common_opts } => { + ports.redir_port.is_some_and(|p| p != common_opts.port) + } _ => false, }) .collect();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-lib/src/app/inbound/manager.rs` around lines 615 - 621, The change_ports logic currently treats any presence of a port field (ports.port.is_some(), ports.socks_port.is_some(), etc.) as a change and forces listener restart; update the predicate to compare the incoming Option value to the existing listener port so only actual changes trigger extraction/restart. In practice, inside change_ports use equality checks (e.g., ports.port != current_listener.port) or compare Option values directly for each variant (InboundOpts::Http, ::Socks, ::Mixed, #[cfg(feature = "tproxy")] ::TProxy, #[cfg(feature = "redir")] ::Redir) and only set the local changed flag/perform extract/abort/reinsert when the value differs, ensuring the returned changed reflects real modifications.clash-dashboard/src/components/ui/tooltip.tsx (1)
50-53: Consider extracting tooltip popup classes to a constant.This improves readability and reduces merge-friction in future style updates.
♻️ Optional refactor
+const tooltipContentClasses = + "z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95" + <TooltipPrimitive.Popup data-slot="tooltip-content" - className={cn( - "z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", - className - )} + className={cn(tooltipContentClasses, className)} {...props} >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-dashboard/src/components/ui/tooltip.tsx` around lines 50 - 53, The long inline class list passed into cn(...) for the tooltip popup should be extracted into a named constant (e.g., TOOLTIP_POPUP_CLASSES) to improve readability and make future style changes simpler; update the tooltip component to import/use that constant in place of the long string in the cn call (keep the existing cn(...) and className merge), ensure the constant contains the exact class string shown (including the data-[slot=kbd] and data-[state] variants) and export or colocate it near the Tooltip component so other components can reuse it.clash-dashboard/src/components/ui/scroll-area.tsx (1)
22-23: Consider making the default internal scrollbar configurable.
ScrollAreaalways injects a vertical scrollbar, which can be limiting for specialized layouts.♻️ Optional refactor
+type ScrollAreaProps = ScrollAreaPrimitive.Root.Props & { + showScrollbar?: boolean +} + function ScrollArea({ className, children, + showScrollbar = true, ...props -}: ScrollAreaPrimitive.Root.Props) { +}: ScrollAreaProps) { return ( <ScrollAreaPrimitive.Root data-slot="scroll-area" className={cn("relative", className)} {...props} > @@ - <ScrollBar /> + {showScrollbar && <ScrollBar />} <ScrollAreaPrimitive.Corner /> </ScrollAreaPrimitive.Root> ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-dashboard/src/components/ui/scroll-area.tsx` around lines 22 - 23, The ScrollArea component always renders a vertical scrollbar (ScrollBar) and the Corner (ScrollAreaPrimitive.Corner); make this behaviour configurable by adding a prop (e.g., showScrollbar?: boolean or renderScrollbar?: boolean) to ScrollArea's props with a default of true, update the ScrollArea component to conditionally render <ScrollBar /> and <ScrollAreaPrimitive.Corner /> only when the prop is true, and update the component's TypeScript type/interface and any places where ScrollArea is used to pass the prop or rely on the default.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@clash-dashboard/src/components/TrafficChart.tsx`:
- Around line 28-30: The ResizeObserver callback is closing over a stale
buildChart and old timestamps/up/down; fix by introducing a mutable dataRef
(e.g., dataRef.current = { timestamps, up, down }) that is updated in the data
effect and change buildChart to read from dataRef.current instead of the
closed-over props; also ensure the ResizeObserver (created where buildChart is
currently referenced) is re-created or its callback reads the latest
buildChart/dataRef so resizing uses current data (update references to
containerRef and chartRef as needed and keep setData usage for incremental
updates).
In `@clash-dashboard/src/hooks/useWebSocket.ts`:
- Around line 18-19: When connect() returns early because url is falsy or the
hook is unmounted, reset the hook state so consumers don't see stale values:
inside useWebSocket's connect function (symbol: connect) before the early return
for !url or unmounted.current, set readyState to 'CLOSED' (via setReadyState or
equivalent) and clear/cleanup the socket reference (setSocket(null) or similar).
Also ensure the onclose handler still runs or that cleanup logic doesn't rely
solely on onclose to set readyState so changes of url properly reset state when
the socket is closed or replaced.
In `@clash-dashboard/src/pages/Connections.tsx`:
- Around line 113-128: The search input and the icon-only row close button lack
accessible names; update the search input (the element using value={search} and
onChange={(e) => setSearch(e.target.value)}) to include an accessible name by
adding either a linked <label> or an aria-label/aria-labelledby (e.g.,
aria-label="Filter connections by host, rule, or chain"), and add an explicit
aria-label (or visually-hidden text) to the icon-only close button for each row
(the icon button that performs the row-close action) such as aria-label="Clear
search" or aria-label="Close connection row" so screen readers receive
meaningful descriptions. Ensure the labels reflect the control action and are
localized if needed.
In `@clash-dashboard/src/pages/DNS.tsx`:
- Around line 27-39: handleQuery currently uses the raw hostname and allows
overlapping requests; to fix it, normalize and validate the hostname at the
start (e.g. const normalized = hostname.trim().toLowerCase() or appropriate
normalization) and use that for the queryDNS call, and add a guard at the top of
handleQuery to return early if loading is true to prevent concurrent requests;
ensure you set error/result state based on the normalized query and pass
normalized to queryDNS (references: function handleQuery, state vars hostname,
loading, setLoading, setError, setResult, and call to queryDNS).
In `@clash-dashboard/src/pages/Overview.tsx`:
- Around line 87-149: EditRow and the input components (ToggleSwitch, PortInput,
TextInput, SelectInput) lack accessible programmatic labels; update EditRow to
accept an inputId (or ariaId) prop and render the visible label as a <label
htmlFor={inputId}> (or set aria-labelledby using that id), then pass a stable id
from each parent usage into the child input components; update PortInput,
TextInput, SelectInput to accept an id prop and assign it to the input element,
and update ToggleSwitch to accept id and aria-pressed/aria-label (or
aria-labelledby) so screen readers can identify the control; apply the same
changes to the other editor instances around the 337-372 area so every form
control has either an id linked to a label or an appropriate
aria-label/aria-labelledby.
In `@clash-dashboard/src/pages/Proxies.tsx`:
- Around line 251-255: The expandable headers are currently plain clickable divs
(the onClick using toggleExpanded(group.name)), which blocks keyboard users;
replace each clickable div with a semantic button (or add role="button",
tabindex="0", and keyboard handlers) and add proper ARIA attributes: use a
<button> (or ensure element has role="button") with aria-expanded={isExpanded}
and aria-controls pointing to the associated panel id, keep the existing onClick
handler (toggleExpanded(group.name)), and ensure Enter/Space keypresses call
toggleExpanded as well; apply the same change to both header instances
referenced (the div wrapping the header and the other at 382-385) so keyboard
users can expand/collapse groups/providers.
In `@clash-dashboard/src/pages/Settings.tsx`:
- Around line 121-143: The API URL and Secret fields are missing proper label
bindings; update the JSX around the apiUrl and secret inputs so each visible
text element is a <label> tied to its input via matching id/htmlFor (e.g., give
the API URL input an id like "api-url" and change the span to <label
htmlFor="api-url">, and do the same for the Secret input with "api-secret"),
preserving existing props (value={apiUrl}, onChange={setApiUrlState},
onKeyDown={handleSave} and value={secret}, onChange={setSecretState}) so screen
readers can programmatically associate the labels with the inputs.
- Around line 33-39: handleSave currently starts two setTimeouts that call
setSaved and refetch, which can run after the component unmounts; modify
handleSave to store the timer IDs (e.g., in a ref like saveTimersRef) when
calling setTimeout and add a useEffect cleanup that iterates the stored IDs and
calls clearTimeout for each on unmount; ensure you clear the saved timers when
they have fired (or clear the ref) and update references to the timers when
calling setTimeout so that setSaved and refetch are never invoked after unmount.
In `@clash-lib/Cargo.toml`:
- Line 8: The default features currently include "dashboard", causing plain
cargo build to run clash-lib/build.rs (which shells out to npm) and require
../clash-dashboard/dist/ at compile time; remove "dashboard" from the default =
[...] list in Cargo.toml so the dashboard feature is opt-in, and ensure code
paths are gated by cfg(feature = "dashboard") (e.g., in build.rs and
clash-lib/src/app/api/embedded_dashboard.rs) or provide prebuilt assets that
avoid invoking npm during a default build.
In `@clash-lib/src/app/api/embedded_dashboard.rs`:
- Around line 45-54: The SPA fallback currently serves Assets::get("index.html")
for any missing asset; change the None branch to inspect the incoming request
path (the route handled by this handler) and, if the path looks like a static
asset (contains a file extension such as .js, .css, .png, .svg, etc.), return
StatusCode::NOT_FOUND.into_response() immediately, otherwise continue with
serving index.html via Assets::get("index.html") as before; update the logic
around Assets::get and the index.html fallback so only non-asset/route-like
paths get the SPA HTML while true asset misses return 404.
In `@clash-lib/src/app/inbound/manager.rs`:
- Around line 487-503: The restart logic currently stops both inbound_handlers
and provider_handles but only restarts inbound_handlers; update restart() to
also restart provider listeners by cloning and passing provider_handles (and any
related provider dispatcher/authenticator context) into the startup routine
(either extend start_all_listeners to accept provider_handles or call the
existing provider startup helper), and likewise adjust restart_idle() to
consider provider_handles when appropriate so provider listener tasks are
recreated after being aborted; reference the symbols restart(),
stop_all_listeners(), start_all_listeners(), restart_idle(), inbound_handlers,
and provider_handles to locate and modify the code paths that stop and start
provider listener tasks.
In `@clash-lib/src/app/remote_content_manager/providers/rule_provider/mrs.rs`:
- Line 136: The code is persisting storage-structure lengths
(leaves_len/ranges_len) as the logical ruleCount for Domain/Range variants;
change the persistence to use the MRS header's parsed count (the value extracted
in rules_mrs_parse) instead of leaves_len or ranges_len when constructing
RuleContent::Domain and RuleContent::Range (and wherever ruleCount metadata is
set). Locate the creation sites that currently return
Ok(RuleContent::Domain(domain_set, leaves_len)) and the analogous Range return
using ranges_len and replace the persisted count with the header count passed
from rules_mrs_parse (use the header `count` value rather than
leaves_len/ranges_len) so API/UI ruleCount reflects logical rules, not storage
length.
In `@clash-lib/src/lib.rs`:
- Around line 127-129: The struct field config_path is only set during initial
construction and not updated on reloads; update the reload logic so whenever you
parse or switch to Config::File you assign self.config_path = Some(path.clone())
(and when switching to a non-file variant clear it with self.config_path = None)
— locate the reload/config-loading code that matches on Config::File (and the
other Config variants) and add these assignments; apply the same change to the
other reload handling block around the code referenced at the second location
(lines ~268-275) so the exposed /configs value always reflects the latest chosen
file.
---
Duplicate comments:
In `@clash-dashboard/src/pages/Logs.tsx`:
- Around line 16-22: LEVEL_ACTIVE_STYLE is using hard-coded hex colors that
break dark theme; replace these static color values with theme-aware CSS
variables or theme tokens (e.g., use var(--bg-toolbar), var(--text-toolbar), or
the app's theme context values) and update all occurrences referenced in
Logs.tsx (LEVEL_ACTIVE_STYLE and the other toolbar/status color maps around the
31-40 and 75-110 regions) to read from those variables or compute colors from
the theme provider; ensure fallback values exist for older themes and adjust the
component classes to apply the CSS variables (or call the theme hook) so the
header and controls maintain proper contrast in dark mode.
- Around line 24-27: The handler is overwriting server timestamps by assigning
new Date() when creating TimestampedLog items; update the code that constructs
TimestampedLog (interface TimestampedLog and the receiver that currently sets ts
= new Date()) to use the timestamp provided on the incoming LogEntry (e.g., use
LogEntry.timestamp or a provided ts field), parsing/formatting that value into
the TimestampedLog.ts string instead of generating a client-side date; if the
backend lacks a timestamp field add one there and consume it here so replayed
entries keep their original chronology.
In `@clash-lib/src/app/api/websocket.rs`:
- Around line 143-156: Take the subscription before taking the snapshot: call
state.log_source_tx.subscribe() first to get rx, then lock state.recent_logs and
build the history Vec<String>; send the history to the socket, then start
receiving from rx. To avoid duplicates, record the serialized history into a
HashSet and when processing messages from rx (or when merging streams) skip any
event whose serialized form is already in that set; reference
state.log_source_tx.subscribe(), state.recent_logs, history, rx, socket.send and
Message::Text for where to change the ordering and add the dedupe set/logic.
---
Nitpick comments:
In `@clash-dashboard/src/components/ui/badge.tsx`:
- Around line 29-36: The Badge component duplicates the base class string
instead of reusing the badgeVariants helper; update the Badge function to call
badgeVariants (the variant helper defined as badgeVariants) to compute the full
className for the <span> (pass the current variant and className into
badgeVariants) and remove the hard-coded base class string and direct use of
variantStyles inside Badge so all classes come from badgeVariants.
- Around line 4-27: variantStyles is currently typed as Record<string,string>,
which widens keyof to string and lets any variant pass; change its declaration
to a literal readonly object to preserve literal keys (e.g. const variantStyles
= { ... } as const) and remove the Record<> typing, then tighten badgeVariants'
parameter type to use the preserved keys (change badgeVariants signature to ({
variant = 'default' }: { variant?: keyof typeof variantStyles } = {})). Also
ensure BadgeProps stays as variant?: keyof typeof variantStyles so consumers get
compile-time validation.
In `@clash-dashboard/src/components/ui/scroll-area.tsx`:
- Around line 22-23: The ScrollArea component always renders a vertical
scrollbar (ScrollBar) and the Corner (ScrollAreaPrimitive.Corner); make this
behaviour configurable by adding a prop (e.g., showScrollbar?: boolean or
renderScrollbar?: boolean) to ScrollArea's props with a default of true, update
the ScrollArea component to conditionally render <ScrollBar /> and
<ScrollAreaPrimitive.Corner /> only when the prop is true, and update the
component's TypeScript type/interface and any places where ScrollArea is used to
pass the prop or rely on the default.
In `@clash-dashboard/src/components/ui/table.tsx`:
- Around line 68-79: TableHead currently renders a <th> without a default scope
which hurts table semantics; update the TableHead component (function TableHead)
to default scope="col" when callers don't supply a scope prop by reading scope
from props and passing scope={scope ?? "col"} into the returned th element so
consumer-provided scope still overrides the default.
In `@clash-dashboard/src/components/ui/tooltip.tsx`:
- Around line 50-53: The long inline class list passed into cn(...) for the
tooltip popup should be extracted into a named constant (e.g.,
TOOLTIP_POPUP_CLASSES) to improve readability and make future style changes
simpler; update the tooltip component to import/use that constant in place of
the long string in the cn call (keep the existing cn(...) and className merge),
ensure the constant contains the exact class string shown (including the
data-[slot=kbd] and data-[state] variants) and export or colocate it near the
Tooltip component so other components can reuse it.
In `@clash-dashboard/src/pages/DNS.tsx`:
- Around line 50-77: The hostname input and DNS type select (controlled by state
variables hostname/setHostname and type/setType and rendered from DNS_TYPES)
lack accessible labels; add either aria-label attributes (e.g.
aria-label="Hostname" and aria-label="DNS record type") or an associated <label
htmlFor="..."> with matching id attributes on the <input> and <select>, and do
the same for the query button that calls handleQuery so screen readers can
identify each control.
In `@clash-dashboard/src/pages/Proxies.tsx`:
- Around line 151-159: The per-proxy setLatencyMap calls cause N re-renders;
instead, inside the group testing block (the async map over group.all using
getProxyDelay), collect each proxy's delay into a local updates object (use
Promise.allSettled to await all tests), set failed entries to 0, then call
setLatencyMap once with setLatencyMap(prev => ({ ...prev, ...updates })); update
the code around the Promise.allSettled mapping so it returns results into that
local map and performs a single state update for the whole group.
In `@clash-dashboard/src/pages/Rules.tsx`:
- Around line 6-27: The helper getRuleTypeBadgeStyle currently returns a
precomputed background but downstream rendering recomputes the background from
the color (causing drift); update all badge render sites (the code that calls
getRuleTypeBadgeStyle — including the usages around the earlier switch and the
locations referenced at lines ~102-103) to consume and apply the returned
background value directly (use style.background or the returned.background)
instead of deriving it from returned.color, so the badge color/background stay
in sync with the single source of truth.
- Around line 36-41: Precompute a lowercase search token once and use it inside
the rules.filter predicate instead of calling search.toLowerCase() repeatedly;
create a local const like searchLower = search.toLowerCase() above the filtered
= rules.filter(...) and replace each search.toLowerCase() occurrence in the
predicate with searchLower (keep the existing payload nullish fallback and calls
to r.type.toLowerCase()/r.proxy.toLowerCase()).
In `@clash-lib/src/app/inbound/manager.rs`:
- Around line 615-621: The change_ports logic currently treats any presence of a
port field (ports.port.is_some(), ports.socks_port.is_some(), etc.) as a change
and forces listener restart; update the predicate to compare the incoming Option
value to the existing listener port so only actual changes trigger
extraction/restart. In practice, inside change_ports use equality checks (e.g.,
ports.port != current_listener.port) or compare Option values directly for each
variant (InboundOpts::Http, ::Socks, ::Mixed, #[cfg(feature = "tproxy")]
::TProxy, #[cfg(feature = "redir")] ::Redir) and only set the local changed
flag/perform extract/abort/reinsert when the value differs, ensuring the
returned changed reflects real modifications.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 8bf412b0-ebac-46a5-a2c9-657130c9f8e3
⛔ Files ignored due to path filters (7)
Cargo.lockis excluded by!**/*.lockclash-dashboard/package-lock.jsonis excluded by!**/package-lock.jsonclash-dashboard/public/favicon.svgis excluded by!**/*.svgclash-dashboard/public/icons.svgis excluded by!**/*.svgclash-dashboard/public/logo.pngis excluded by!**/*.pngclash-dashboard/src/assets/hero.pngis excluded by!**/*.pngclash-dashboard/src/assets/logo.pngis excluded by!**/*.png
📒 Files selected for processing (52)
clash-bin/Cargo.tomlclash-dashboard/.gitignoreclash-dashboard/README.mdclash-dashboard/components.jsonclash-dashboard/eslint.config.jsclash-dashboard/index.htmlclash-dashboard/package.jsonclash-dashboard/src/App.tsxclash-dashboard/src/components/Layout.tsxclash-dashboard/src/components/ProxyGroups.tsxclash-dashboard/src/components/TrafficChart.tsxclash-dashboard/src/components/ui/badge.tsxclash-dashboard/src/components/ui/button.tsxclash-dashboard/src/components/ui/card.tsxclash-dashboard/src/components/ui/scroll-area.tsxclash-dashboard/src/components/ui/separator.tsxclash-dashboard/src/components/ui/table.tsxclash-dashboard/src/components/ui/tabs.tsxclash-dashboard/src/components/ui/tooltip.tsxclash-dashboard/src/hooks/useTraffic.tsclash-dashboard/src/hooks/useWebSocket.tsclash-dashboard/src/index.cssclash-dashboard/src/lib/api.tsclash-dashboard/src/lib/settings.tsclash-dashboard/src/lib/utils.tsclash-dashboard/src/main.tsxclash-dashboard/src/pages/Connections.tsxclash-dashboard/src/pages/DNS.tsxclash-dashboard/src/pages/Logs.tsxclash-dashboard/src/pages/Overview.tsxclash-dashboard/src/pages/Providers.tsxclash-dashboard/src/pages/Proxies.tsxclash-dashboard/src/pages/Rules.tsxclash-dashboard/src/pages/Settings.tsxclash-dashboard/tsconfig.app.jsonclash-dashboard/tsconfig.jsonclash-dashboard/tsconfig.node.jsonclash-dashboard/vite.config.tsclash-lib/Cargo.tomlclash-lib/build.rsclash-lib/src/app/api/embedded_dashboard.rsclash-lib/src/app/api/handlers/config.rsclash-lib/src/app/api/handlers/log.rsclash-lib/src/app/api/handlers/provider.rsclash-lib/src/app/api/mod.rsclash-lib/src/app/api/runner.rsclash-lib/src/app/api/websocket.rsclash-lib/src/app/inbound/manager.rsclash-lib/src/app/remote_content_manager/providers/rule_provider/mrs.rsclash-lib/src/app/remote_content_manager/providers/rule_provider/provider.rsclash-lib/src/app/router/mod.rsclash-lib/src/lib.rs
✅ Files skipped from review due to trivial changes (16)
- clash-dashboard/tsconfig.json
- clash-dashboard/index.html
- clash-dashboard/src/main.tsx
- clash-dashboard/.gitignore
- clash-dashboard/src/components/ui/separator.tsx
- clash-dashboard/README.md
- clash-dashboard/tsconfig.node.json
- clash-dashboard/tsconfig.app.json
- clash-dashboard/package.json
- clash-dashboard/components.json
- clash-dashboard/src/lib/settings.ts
- clash-dashboard/src/components/ui/button.tsx
- clash-dashboard/src/components/ui/card.tsx
- clash-lib/src/app/api/handlers/config.rs
- clash-lib/src/app/api/handlers/provider.rs
- clash-dashboard/src/lib/api.ts
🚧 Files skipped from review as they are similar to previous changes (10)
- clash-dashboard/src/lib/utils.ts
- clash-dashboard/eslint.config.js
- clash-dashboard/vite.config.ts
- clash-lib/src/app/router/mod.rs
- clash-dashboard/src/components/Layout.tsx
- clash-dashboard/src/hooks/useTraffic.ts
- clash-dashboard/src/components/ProxyGroups.tsx
- clash-dashboard/src/App.tsx
- clash-lib/src/app/api/runner.rs
- clash-dashboard/src/pages/Providers.tsx
| function ToggleSwitch({ value, onChange }: { value: boolean; onChange: (v: boolean) => void }) { | ||
| return ( | ||
| <button | ||
| onClick={() => onChange(!value)} | ||
| className="w-10 h-6 rounded-full transition-colors flex-shrink-0 relative" | ||
| style={{ background: value ? '#34c759' : 'rgba(0,0,0,0.15)' }} | ||
| > | ||
| <span | ||
| className="absolute top-0.5 w-5 h-5 rounded-full bg-white shadow transition-all" | ||
| style={{ left: value ? '50%' : '2px' }} | ||
| /> | ||
| </button> | ||
| ); | ||
| } | ||
|
|
||
| function PortInput({ value, onCommit }: { value: number | undefined; onCommit: (v: number | null) => void }) { | ||
| const [local, setLocal] = useState<string>(value != null && value !== 0 ? String(value) : ''); | ||
| useEffect(() => { setLocal(value != null && value !== 0 ? String(value) : ''); }, [value]); | ||
| return ( | ||
| <input | ||
| type="number" min="0" max="65535" value={local} | ||
| onChange={(e) => setLocal(e.target.value)} | ||
| onBlur={() => { const n = parseInt(local, 10); onCommit(!local || isNaN(n) ? null : n); }} | ||
| onKeyDown={(e) => { if (e.key === 'Enter') (e.target as HTMLInputElement).blur(); }} | ||
| className="form-input w-24 text-right text-[13px] font-mono rounded-lg border-0 bg-black/[0.04] focus:ring-2 focus:ring-[#0071e3]/30 focus:bg-white py-1 px-2 transition-all" | ||
| placeholder="disabled" | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| function TextInput({ value, onCommit }: { value: string | undefined; onCommit: (v: string) => void }) { | ||
| const [local, setLocal] = useState<string>(value ?? ''); | ||
| useEffect(() => { setLocal(value ?? ''); }, [value]); | ||
| return ( | ||
| <input | ||
| type="text" value={local} | ||
| onChange={(e) => setLocal(e.target.value)} | ||
| onBlur={() => onCommit(local)} | ||
| onKeyDown={(e) => { if (e.key === 'Enter') (e.target as HTMLInputElement).blur(); }} | ||
| className="form-input w-40 text-right text-[13px] font-mono rounded-lg border-0 bg-black/[0.04] focus:ring-2 focus:ring-[#0071e3]/30 focus:bg-white py-1 px-2 transition-all" | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| function SelectInput({ value, options, onChange }: { value: string | undefined; options: readonly string[]; onChange: (v: string) => void }) { | ||
| return ( | ||
| <select | ||
| value={value ?? ''} | ||
| onChange={(e) => onChange(e.target.value)} | ||
| className="form-select text-[13px] font-mono rounded-lg border-0 bg-black/[0.04] focus:ring-2 focus:ring-[#0071e3]/30 py-1 px-2 capitalize transition-all" | ||
| > | ||
| {options.map((o) => <option key={o} value={o} className="capitalize">{o}</option>)} | ||
| </select> | ||
| ); | ||
| } | ||
|
|
||
| function EditRow({ label, icon, iconBg, children }: { label: string; icon: React.ReactNode; iconBg: string; children: React.ReactNode }) { | ||
| return ( | ||
| <div className="flex items-center gap-3 px-4" style={{ minHeight: 52, borderBottom: '1px solid rgba(0,0,0,0.06)' }}> | ||
| <IconBadge bg={iconBg}>{icon}</IconBadge> | ||
| <span className="text-[15px] flex-1" style={{ color: '#1d1d1f' }}>{label}</span> | ||
| {children} | ||
| </div> |
There was a problem hiding this comment.
The config form controls are missing programmatic labels.
EditRow renders the visible label as a plain <span>, and the input/select/button controls don't get id, aria-labelledby, or aria-label. That makes the Ports/Access/Logging editor largely unusable with screen readers.
Also applies to: 337-372
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@clash-dashboard/src/pages/Overview.tsx` around lines 87 - 149, EditRow and
the input components (ToggleSwitch, PortInput, TextInput, SelectInput) lack
accessible programmatic labels; update EditRow to accept an inputId (or ariaId)
prop and render the visible label as a <label htmlFor={inputId}> (or set
aria-labelledby using that id), then pass a stable id from each parent usage
into the child input components; update PortInput, TextInput, SelectInput to
accept an id prop and assign it to the input element, and update ToggleSwitch to
accept id and aria-pressed/aria-label (or aria-labelledby) so screen readers can
identify the control; apply the same changes to the other editor instances
around the 337-372 area so every form control has either an id linked to a label
or an appropriate aria-label/aria-labelledby.
| None => { | ||
| // SPA fallback: any route the React Router doesn't match gets | ||
| // index.html so the client can render the correct page. | ||
| match Assets::get("index.html") { | ||
| Some(asset) => Response::builder() | ||
| .header(header::CONTENT_TYPE, "text/html; charset=utf-8") | ||
| .header(header::CACHE_CONTROL, "no-cache") | ||
| .body(Body::from(asset.data.into_owned())) | ||
| .unwrap(), | ||
| None => StatusCode::NOT_FOUND.into_response(), |
There was a problem hiding this comment.
Don't serve index.html for missing static assets.
This fallback also turns missing /ui/*.js, /ui/*.css, etc. into 200 text/html, which makes stale or mistyped asset URLs fail as blank-page script errors instead of a clean 404. Keep the SPA fallback for route-like paths only, and return 404 when the miss looks like a file asset.
♻️ Possible fix
None => {
+ if std::path::Path::new(path).extension().is_some() {
+ return StatusCode::NOT_FOUND.into_response();
+ }
+
// SPA fallback: any route the React Router doesn't match gets
// index.html so the client can render the correct page.
match Assets::get("index.html") {
Some(asset) => Response::builder()
.header(header::CONTENT_TYPE, "text/html; charset=utf-8")📝 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.
| None => { | |
| // SPA fallback: any route the React Router doesn't match gets | |
| // index.html so the client can render the correct page. | |
| match Assets::get("index.html") { | |
| Some(asset) => Response::builder() | |
| .header(header::CONTENT_TYPE, "text/html; charset=utf-8") | |
| .header(header::CACHE_CONTROL, "no-cache") | |
| .body(Body::from(asset.data.into_owned())) | |
| .unwrap(), | |
| None => StatusCode::NOT_FOUND.into_response(), | |
| None => { | |
| if std::path::Path::new(path).extension().is_some() { | |
| return StatusCode::NOT_FOUND.into_response(); | |
| } | |
| // SPA fallback: any route the React Router doesn't match gets | |
| // index.html so the client can render the correct page. | |
| match Assets::get("index.html") { | |
| Some(asset) => Response::builder() | |
| .header(header::CONTENT_TYPE, "text/html; charset=utf-8") | |
| .header(header::CACHE_CONTROL, "no-cache") | |
| .body(Body::from(asset.data.into_owned())) | |
| .unwrap(), | |
| None => StatusCode::NOT_FOUND.into_response(), | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@clash-lib/src/app/api/embedded_dashboard.rs` around lines 45 - 54, The SPA
fallback currently serves Assets::get("index.html") for any missing asset;
change the None branch to inspect the incoming request path (the route handled
by this handler) and, if the path looks like a static asset (contains a file
extension such as .js, .css, .png, .svg, etc.), return
StatusCode::NOT_FOUND.into_response() immediately, otherwise continue with
serving index.html via Assets::get("index.html") as before; update the logic
around Assets::get and the index.html fallback so only non-asset/route-like
paths get the SPA HTML while true asset misses return 404.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
clash-lib/build.rs (1)
112-128: Consider watching directories to detect new files.The function only emits
rerun-if-changedfor existing files. If a developer adds a new.tsxfile tosrc/, Cargo won't re-run the build script until an already-watched file changes.♻️ Suggested fix to also watch directories
fn emit_rerun_if_changed(dir: &std::path::Path) { if !dir.exists() { return; } + // Watch the directory itself so new files trigger a rebuild + println!("cargo:rerun-if-changed={}", dir.display()); let Ok(entries) = std::fs::read_dir(dir) else { return; };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@clash-lib/build.rs` around lines 112 - 128, The current emit_rerun_if_changed function only prints cargo:rerun-if-changed for files, so adding new files in a watched directory won't retrigger the build; modify emit_rerun_if_changed to also print "cargo:rerun-if-changed={}" for each directory (e.g. when path.is_dir()) before recursing into it, and ensure the initial dir passed to emit_rerun_if_changed is printed as well so Cargo watches the directory itself for new entries.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@clash-lib/build.rs`:
- Around line 112-128: The current emit_rerun_if_changed function only prints
cargo:rerun-if-changed for files, so adding new files in a watched directory
won't retrigger the build; modify emit_rerun_if_changed to also print
"cargo:rerun-if-changed={}" for each directory (e.g. when path.is_dir()) before
recursing into it, and ensure the initial dir passed to emit_rerun_if_changed is
printed as well so Cargo watches the directory itself for new entries.
ffe9d77 to
fd0b02b
Compare
- Add clash-dashboard/ React+Vite+TS+Tailwind+shadcn frontend - Add builtin-dashboard feature flag with rust-embed - Add build.rs to bundle dashboard assets at compile time - Add /ui endpoint to serve the dashboard via clash-lib API - Light theme redesign with top nav layout Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add SVG feTurbulence + feDisplacementMap filter for refraction warp - Add .liquid-glass class: 40px backdrop-blur, specular highlights, angled shimmer overlay, colored shadow — applied to nav bar - Add .liquid-glass-card class: frosted white translucent cards with inner specular top edge — applied to all white content cards - Add .nav-pill-glass: tiny liquid glass chip for active nav item - Add vivid-shimmer keyframe sweep animation on gradient stat cards - Apply liquid-glass distort filter to VividStatCard + VividCard - Full dark mode support for all glass layers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Axum 0.8 requires wildcard segments to be written as `{*name}` rather
than the old v0.7 `*name` form. The `/ui/*path` route panicked at
startup with "Path segments must not start with `*`".
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…logs - Providers (Proxies page): each provider now expands/collapses to show a proxy latency grid; added Test (healthcheck) and Update buttons with loading states; shows updatedAt timestamp when available. - Config page: rewrites display to match actual GetConfigResponse fields (port, socks-port, mixed-port, redir-port, tproxy-port, allow-lan, bind-address, mode, log-level, ipv6, listeners, lan-ips, dns-listen). All supported fields are now inline-editable via PATCH /configs: mode and log-level use dropdowns, allow-lan and ipv6 use toggle switches, ports use number inputs, bind-address uses a text input. Read-only sections show active listeners (with type badge + active dot), LAN IPs (when allow-lan is on), and DNS listener addresses. - Logs page: shows WebSocket connection status indicator (green=OPEN, orange=CONNECTING, red=CLOSED) next to the title. The empty state message now differentiates between "Waiting for logs", "Connecting…", and "Disconnected — check API settings". - api.ts: replaces the old catch-all ClashConfig type with exact InboundEndpoint / DnsListenInfo / ClashConfig interfaces that match GetConfigResponse; adds PatchableConfig type (excludes read-only fields); adds patchConfigs() (PATCH /configs); fixes reloadConfigs() to use PUT instead of PATCH for path-based reload. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…view - Replace placeholder 'C' square with actual clash-rs favicon.svg in nav - Wrap nav content and page content in max-w-5xl mx-auto for large screens - Hide wordmark and version on mobile (sm:inline) to save nav space - Add Network + System info sections to Overview (ports, allow-lan, log level, IPv6, bind address) — read-only SwiftUI inset-group cards - Remove per-page max-w overrides now handled by layout wrapper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add asn?: string to Connection.metadata type (serialized by session.as_map()) - Display ASN as a blue pill badge below the host name in the Host column (shows country code or org name depending on which MMDB is configured) - Include ASN in the search filter - Process name shown alongside ASN when both are present Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… styles - Install @tailwindcss/forms plugin, register via @plugin directive with class strategy - Apply form-input class to PortInput and TextInput (border-0, focus ring, bg transition) - Apply form-select class to SelectInput (consistent styling with inputs) - Remove Config.tsx (fully merged into Overview) - Fix Overview.tsx: remove duplicate IconBadge, add missing export function declaration, remove stale unused helpers (InfoSection/PortValue/BoolPill/InfoRow), trim orphaned return block Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mode is already controlled by the segmented switcher above. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
3-column grid (1-col on mobile): Ports card has all 5 port inputs, Access card has Allow LAN + Bind Address + IPv6, Logging card has Log Level. Replaces the unbalanced 2-col Logging/Network split. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace invalidateQueries-only approach with proper optimistic update: - Cancel in-flight queries before mutating - Immediately update cache so selected proxy highlights instantly - Revert on error, then background-refetch to confirm server state Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- CSS: add global cursor:pointer for button:not(:disabled) and select - Overview: sort Active Listeners active-first then alphabetically - Proxies: all group types (not just Selector) get clickable proxy chips; non-Selector groups show a hint explaining auto-select + force-override; optimistic update reverts on backend rejection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The feature existed in clash-lib but was never wired into clash-bin, so the binary was always compiled without the embedded dashboard. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Direct: full-page empty state, no groups shown - Global: only the GLOBAL selector group shown - Rule: all groups except GLOBAL shown - Mode badge shown in page header Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Rename feature flag from `builtin-dashboard` → `dashboard` - Add `dashboard` to clash-lib default features - Remove unused scaffold SVGs (vite.svg, react.svg) - Fix button.tsx: default type='button' to prevent accidental form submits - Fix api.ts: handle empty response bodies before JSON parse - Fix api.ts: encode connection ID in DELETE /connections/:id - Fix api.ts: use URL API for WS token to handle existing query params - Fix vite.config.ts: use import.meta.url instead of __dirname (ESM compat) - Fix build.rs: watch tsconfig.app.json as dashboard build input - Fix Rules.tsx: surface fetch errors; guard payload before toLowerCase - Fix Overview.tsx: disable mode buttons while mutation is pending - Fix Providers.tsx: show error state in RuleProviderRulesPanel - Fix Settings.tsx: use sessionStorage for secret (not localStorage) - Add rule provider expand: list_rules() on RuleProvider trait, GET /providers/rules/:name/rules endpoint, frontend expand panel - Fix double #[async_trait] annotation on RuleProvider impl Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Store the original payload length in RuleContent::Domain and RuleContent::Ipcidr so as_map() can return the correct count. YAML/Text parsers capture len() before building the trie/set. MRS parser uses leaves_len/ranges_len as the count. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add GET /providers/rules/:name/match?target=<domain|ip> endpoint that builds a minimal Session and calls provider.search() so users can test whether a given host or IP is covered by a rule provider. In the Providers page, expand panel for every rule provider now shows: - A search input + Test button (all behavior types) - ✓ Match / ✗ No match result badge after testing - Rule list still shown for Classical providers as before - Info note for Domain/IPCIDR explaining entries aren't enumerable Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CI runners don't have node_modules pre-populated, so npm run build fails with missing type definitions. npm ci installs from package-lock.json reliably on a clean checkout. Also watch package-lock.json so cargo reruns when dependencies change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… API changes Move all non-embed Rust API improvements (config reload, rule provider routes, log lag handling, inbound restart_idle) to feat/api-improvements PR #1343. Dashboard branch now contains only: - Embedded dashboard serving (embedded_dashboard.rs, /ui/* routing) - build.rs npm ci steps - Cargo.toml dashboard feature flag - All clash-dashboard frontend files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
546801f to
e4b3714
Compare
- Separate hover auto-scroll pause from user-controlled Pause/Resume toggle - Add aria-label/title to nav links for screen readers - Add type field to RuleProvider interface - Add timer cleanup on unmount in Settings (saveTimersRef) - Prevent overlapping DNS queries; trim hostname before querying - Add aria-label to Connections search input and row close button - Add keyboard support (role=button, tabIndex, onKeyDown) to expandable proxy/provider headers in Proxies page - Fix TrafficChart resize to always use latest data via dataRef - Reset useWebSocket state (CLOSED) when url becomes null - Make build.rs dashboard build non-fatal when clash-dashboard dir or npm is missing (warns instead of aborting the build) - Toolbar text uses theme-aware Tailwind classes for dark mode compat Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When npm build is skipped (missing npm or dashboard dir), the dist/ folder may not exist. rust-embed's proc-macro requires the folder to exist at compile time — if it's absent, the derive generates an empty struct with no impl, causing 'no method get found for Assets'. Always create dist/ upfront (empty if needed) so rust-embed can compile cleanly even when the frontend build is skipped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
On Windows npm is a .cmd script, not a binary — Command::new("npm")
fails to spawn it. Use npm.cmd when cfg!(windows).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
What does this PR do?
Adds a built-in web dashboard to clash-rs, compiled into the binary via
rust-embedand served at/ui. No external dashboard binary needed.The dashboard uses an Apple/SwiftUI-inspired design with liquid-glass cards and a clean minimal layout.
Features
Technical
dashboard(enabled by default in all builds)build.rsrunsnpm run buildwhenCARGO_FEATURE_DASHBOARDis set; dashboarddist/embedded viarust-embed/providers/rules(list + update),/providers/rules/:name/rules(Classical rule enumeration), WebSocket log history ring buffer (200 entries, replayed on connect)Type
Checklist
Summary by CodeRabbit
New Features
Documentation
Chores