feat: redesign server views and add tags support#102
Conversation
Replace MetricBarRow with text-only "used / total" via new MetricTextRow helper, and add Activity icon to the disk/network I/O sub-rows for vertical alignment with the top row.
Move the percentage from the bar row to the right end of the sub-line in CpuCell and MemoryCell, and remove the swap indicator. Add `showPct` prop to MetricBarRow to opt out of the inline percentage. Also de-emphasize the disk/network I/O sub-rows by dropping the Activity icon and using muted text instead of bold foreground.
Wrap R/W letters in rounded background chips to separate them visually from the muted speed values that follow.
Drop the pl-5 indent and resize R/W chips to size-3.5 so they occupy the same x-position and footprint as the HardDrive icon above.
Add formatSpeedOrZero helper that returns '0' when bytes/s is zero (dropping the trailing unit). Wrap every speed value in a w-14 span so that value length changes don't shift the W/↑ badges beside them.
- DataTable cells padded to px-3 for wider column gutters - split meta.className vs cellClassName so TableHead keeps default align-middle while body cells can align-top - narrow disk/network table columns to 150px - network column header uses DataTableColumnHeader for consistency - server grid card disk read/write labels use R/W letter badges to match table visual
- grid card disk read/write and network i/o speed render values as bold text-sm with smaller muted units - table MemoryCell/DiskCell/NetworkCell bytes and speed values render bold text-xs with smaller text-[9px] units - speed value of 0 shows just "0" without unit and stays non-bold - bytes value of 0 (e.g. "0 B") skips the bold styling for parity - CompactMetric label/value accept ReactNode to allow inline badge and split-styled values
Replace missing translation keys (edit_hide_status, edit_none/monthly/..., edit_total_in_out/...) with their canonical forms from the locale files so the dialog shows labels instead of raw keys. Swap the native date input for a Popover+Calendar to display YYYY-MM-DD reliably, and wrap the popover in a DIV so base-ui's FloatingFocusManager sentinel spans no longer become direct siblings under space-y-1, which was shifting the Field height by 4px when the calendar opened.
…les by adjusting formatting and reordering imports
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughRelease v0.8.10 implements a server card latency visualization redesign featuring a new combined-severity bar component and merged latency/loss indicators, updates data table controls with i18n coverage, refactors dialog layouts using new body/footer containers, and includes table cell metric redesign with position indicators for improved density. Changes
Sequence DiagramsequenceDiagram
actor User as User
participant UI as Frontend UI<br/>(ServerCard)
participant Calc as Latency/Loss<br/>Logic
participant Chart as Recharts<br/>Chart
participant Bar as SeverityBar<br/>Component
participant API as Backend<br/>Server Metrics
API->>UI: WebSocket: server metrics<br/>(latencyMs, lossRatio)
User->>UI: View server card
activate UI
UI->>Calc: getCombinedSeverity<br/>(latencyMs, lossRatio)
Calc->>Calc: Evaluate thresholds<br/>(healthy/warning/severe/failed)
Calc-->>UI: CombinedSeverity
UI->>Calc: getCombinedBarColor<br/>(latencyMs, lossRatio)
Calc-->>UI: fill color (hex)
UI->>Calc: getLossDotBgClass<br/>(lossRatio)
Calc-->>UI: CSS class (bg-*)
UI->>UI: Render header:<br/>latency + loss dot
UI->>Chart: Prepare data points<br/>with SeverityBarDatum
Chart->>Bar: Render per-point bars<br/>with shape function
alt Loss = 100% (failed)
Bar->>Bar: Generate failPatternId
Bar->>Bar: Use SVG pattern fill<br/>(diagonal stripes)
else Other severities
Bar->>Bar: Use fillColor rect
end
Bar-->>Chart: Rendered bars
Chart-->>UI: Latency chart
UI-->>User: Display card with<br/>compact severity bars
deactivate UI
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly Related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/src/components/server/server-edit-dialog.tsx (1)
133-133:⚠️ Potential issue | 🟡 MinorTimezone skew when serializing
expired_at.
expiredAtis a localYYYY-MM-DDfromformatIsoDate(usesgetFullYear/getMonth/getDate), but is sent as${expiredAt}T00:00:00Z— UTC midnight. For users in negative UTC offsets, the stored instant represents the previous local day; re-reading viaserver.expired_at?.slice(0, 10)then echoes the UTC date, causing the picker to display a date off by one from what the user selected. Consider either serializing as a plain date (no Z), or deriving the UTC Y-M-D usinggetUTCFullYear()etc. so the round-trip is stable.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/components/server/server-edit-dialog.tsx` at line 133, The current serialization sets expired_at to `${expiredAt}T00:00:00Z` which turns a local YYYY-MM-DD into a UTC instant and causes off-by-one day for negative offsets; instead serialize a timezone-neutral plain date or compute the UTC Y/M/D explicitly. Update the serialization in server-edit-dialog.tsx (the expired_at field that uses expiredAt and the formatIsoDate output) to send expired_at as the plain YYYY-MM-DD string (i.e. expiredAt) or, if you must send an instant, build the UTC date using UTC getters (getUTCFullYear/getUTCMonth/getUTCDate) or a Date -> toISOString() slice so the round-trip with server.expired_at?.slice(0,10) remains stable.
🧹 Nitpick comments (8)
crates/server/src/task/record_writer.rs (1)
68-85: First-observation branch looks correct; minor duplication with the tail state-write.The new branch populates the in-memory cache before attempting the DB upsert, which is fine: if
upsert_statefails here, the next tick will recompute a delta against the cached values and line 106–114 will retry the state upsert, so the system is self-healing. Note that with thiscontinue, the first observation still produces notraffic_hourlyrow for that tick — this is consistent with the prior behavior, just worth keeping in mind for any “missing first hour” reports.The body at lines 75–83 is structurally identical to the tail state-write at lines 106–114. Consider extracting a small helper to avoid drift between the two sites.
♻️ Proposed helper to deduplicate the two state-write sites
+async fn write_traffic_state( + db: &sea_orm::DatabaseConnection, + server_id: &str, + curr_in: i64, + curr_out: i64, + writes_allowed: bool, +) { + if writes_allowed { + if let Err(e) = TrafficService::upsert_state(db, server_id, curr_in, curr_out).await { + tracing::error!("Failed to upsert traffic state for {server_id}: {e}"); + } + } else { + tracing::info!("Skipping recovery-frozen traffic state write for {server_id}"); + } +}Then replace both blocks (75–83 and 106–114) with a single call to
write_traffic_state(...).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/server/src/task/record_writer.rs` around lines 68 - 85, Extract a small helper (e.g., write_traffic_state) to deduplicate the state-write logic used in the first-observation branch and the tail state-write: the helper should accept the transfer cache, server_id, curr_in, curr_out, writes_allowed and a reference to state.db, insert/update the in-memory cache (transfer_cache.insert(server_id.clone(), (curr_in, curr_out))), then if writes_allowed call TrafficService::upsert_state(&state.db, server_id, curr_in, curr_out).await and log errors with tracing::error!, otherwise log the recovery-frozen info; replace the duplicated blocks around transfer_cache.get(...) and the later tail-write (the sites using transfer_cache, compute_delta, and TrafficService::upsert_state) with a single call to write_traffic_state so both paths share the same behavior.docs/superpowers/specs/2026-04-18-server-card-latency-compact-design.md (1)
74-91: Return type annotationLatencyStatus | 'severe'is inconsistent with the plan'sCombinedSeveritytype.The implementation plan (
2026-04-18-server-card-latency-compact.mdL123) introduces a dedicatedCombinedSeverity = 'unknown' | 'healthy' | 'warning' | 'severe' | 'failed'type, which is cleaner than wideningLatencyStatus. Consider updating this spec snippet to referenceCombinedSeverityso the two docs stay in sync and contributors don't accidentally extendLatencyStatuswith'severe'(which would ripple through other latency-color call sites).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/superpowers/specs/2026-04-18-server-card-latency-compact-design.md` around lines 74 - 91, The function getCombinedSeverity currently uses the union return type LatencyStatus | 'severe'; update its signature to return the dedicated CombinedSeverity type (CombinedSeverity = 'unknown' | 'healthy' | 'warning' | 'severe' | 'failed') instead of widening LatencyStatus, and ensure CombinedSeverity is imported or declared in the same module so callers remain unchanged; keep the function body logic as-is but replace the return type reference from LatencyStatus | 'severe' to CombinedSeverity to keep the spec consistent and avoid extending LatencyStatus.apps/web/src/components/data-table/data-table.tsx (1)
18-18: Global[&_td]:px-3 [&_th]:px-3will override any per-column horizontal padding set viameta.className/cellClassName.Tailwind utility specificity is equal, but descendant selectors like
[&_td]:px-3compile to.table-class td { padding-inline: 0.75rem }, which has higher specificity than a class applied directly on the<td>(e.g., a column that setspx-4orpx-0viameta.className). If any column later needs custom horizontal padding, the override will silently fail. Worth noting in the component docs or switching to a default viacn()on the cells instead.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/components/data-table/data-table.tsx` at line 18, The global descendant Tailwind selectors on the Table ("[&_td]:px-3 [&_th]:px-3") force horizontal padding on all cells and will override per-column meta.className/cellClassName; remove those descendant selectors from the Table className and instead apply the default horizontal padding when rendering cells (merge a default "px-3" with any per-column meta.className or cellClassName using the existing cn() helper in the cell render path). Update the cell rendering logic (where you read meta.className / cellClassName) to compose defaults with column-level classes so columns can override padding.docs/superpowers/plans/2026-04-18-server-card-latency-compact.md (1)
286-294: Import replacement instruction dropsisLatencyFailurefrom the old line but the new line keeps it — align the narrative.Step 1 says to replace the old import line with the new one. The old line shown (L287) is
import { getLatencyBarColor, isLatencyFailure } ...and the new one (L293) isimport { getCombinedBarColor, getCombinedSeverity, getLossDotBgClass, isLatencyFailure } .... That's fine in isolation, but Step 6 ("清理未使用 import") states ultracite will auto-removegetLatencyBarColor— that symbol isn't present in the post-replacement import, so the comment is misleading. Minor doc polish only.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/superpowers/plans/2026-04-18-server-card-latency-compact.md` around lines 286 - 294, The import replacement narrative is inconsistent: update the doc steps so Step 1 shows replacing "getLatencyBarColor, isLatencyFailure" with "getCombinedBarColor, getCombinedSeverity, getLossDotBgClass, isLatencyFailure" and adjust Step 6 ("清理未使用 import") to state that ultracite will remove getLatencyBarColor (which is no longer present) while keeping isLatencyFailure; ensure references to getLatencyBarColor are removed or marked as cleaned up and that isLatencyFailure is documented as still imported.apps/web/src/components/data-table/data-table-column-header.tsx (1)
51-51: Consider removingmin-w-28to use the defaultmin-w-32.
DropdownMenuContentappliesmin-w-32as a base class, and theclassNameprop merges with it viatwMerge. Sincemin-w-28appears later in the merged class string, it overrides the default, producing a 112px width instead of 128px. This appears to be unique to this dropdown—other instances usemin-w-56,w-36, or the default. If the narrower width is intentional, leave it; otherwise, drop the class.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/components/data-table/data-table-column-header.tsx` at line 51, The DropdownMenuContent in data-table-column-header.tsx currently passes className="min-w-28", which via twMerge overrides the component's default min-w-32 and makes the dropdown 112px wide; if the narrower width is not intentional, remove the "min-w-28" from the className (or replace it with "min-w-32" to be explicit) on the DropdownMenuContent usage to restore the default 128px width and keep other classes intact.apps/web/src/components/server/server-card.tsx (3)
31-35:isSeverityBarShapePropsis a trivial object guard.The predicate is typed as
value is SeverityBarShapePropsbut only assertstypeof value === 'object' && value !== null, so TypeScript narrows to a stricter shape than what's actually validated. In practice Recharts always passes the expected bar-shape props, so this is fine, but if you ever get unexpected props (e.g., during a Recharts upgrade) this will silently claim validity and<SeverityBar>will fall back to its own null/width guards.If you want a tighter guard without a heavy runtime cost, consider checking for the two fields
SeverityBaractually reads (width,payload):-function isSeverityBarShapeProps(value: unknown): value is SeverityBarShapeProps { - return typeof value === 'object' && value !== null -} +function isSeverityBarShapeProps(value: unknown): value is SeverityBarShapeProps { + return typeof value === 'object' && value !== null && 'width' in value +}Otherwise safe to leave as is —
SeverityBaralready handles missingpayload/ non-positivewidth.Also applies to: 412-418
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/components/server/server-card.tsx` around lines 31 - 35, The current isSeverityBarShapeProps predicate only checks for non-null object and overclaims the stronger type; update isSeverityBarShapeProps to also verify the minimal fields SeverityBar consumes by checking that the value has a numeric, positive 'width' property and a 'payload' property (e.g., ensure typeof (value as any).width === 'number' and (value as any).payload !== undefined) so TypeScript narrowing matches runtime validation; apply the same fix to the other identical guard at the second occurrence around the SeverityBar usage.
2-2: Preferimport type { ReactNode } from 'react'overReact.ReactNode.Line 2 only imports specific named members from
reactand doesn't bring theReactnamespace into scope, yet line 104 annotates the return type asReact.ReactNode. This compiles today thanks to@types/react's UMD global, but it's inconsistent withapps/web/src/routes/_authed/servers/index.cells.tsxwhich usesimport type { ReactNode } from 'react'. Aligning on the named import is the recommended form under"jsx": "react-jsx"and avoids relying on the UMD global.✏️ Proposed diff
-import { type ComponentProps, memo, useId, useMemo } from 'react' +import { type ComponentProps, memo, type ReactNode, useId, useMemo } from 'react' @@ -function renderSpeedValue(bytesPerSec: number): React.ReactNode { +function renderSpeedValue(bytesPerSec: number): ReactNode {Also applies to: 104-104
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/components/server/server-card.tsx` at line 2, The file uses React.ReactNode in the component return type but only imports named members from 'react'; update the top import to add a type import for ReactNode (import type { ReactNode } from 'react') and replace the return annotation React.ReactNode with ReactNode in the component (e.g., the ServerCard component's return type). This keeps imports consistent with ComponentProps/memo/useId/useMemo usage and avoids relying on the UMD global.
104-119: DuplicaterenderSpeedValueacross files — consolidate into a shared helper.
renderSpeedValuehere and inapps/web/src/routes/_authed/servers/index.cells.tsx(line 35) render the same concept (bytes/sec with a small muted unit suffix) but diverge in the zero-case and unit styling:
- Here: returns the bare string
'0'; unit span usestext-[10px]andtext-muted-foreground.index.cells.tsx: returns<span className="text-xs">0</span>; unit span usestext-[9px]without muted color.That's two slightly different implementations of the same primitive, which will drift again as styles evolve. Consider extracting a single helper (e.g.,
renderSpeedValuein@/lib/utils) that accepts avariantor a className override for the unit span, and have both call sites use it. Same forrenderBytesValueinindex.cells.tsxif it's worth unifying.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/components/server/server-card.tsx` around lines 104 - 119, Duplicate renderSpeedValue implementations should be consolidated into a single shared helper (e.g., export a renderSpeedValue from a shared utils module) that accepts parameters for zero-case rendering and a unitClassName/variant override so callers can control the unit span styling; update the two call sites (the existing renderSpeedValue in this component and the one in index.cells.tsx) to import and use the shared helper with appropriate class overrides, and optionally extract/renderBytesValue the same way if it duplicates behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/web/src/components/server/capabilities-dialog.tsx`:
- Line 144: The title prop on the element uses a hardcoded Chinese string
(title={isLocked ? '客户端关闭' : undefined}); replace this with the i18n helper
(e.g., title={isLocked ? t('client.closed') : undefined}) and make sure
useTranslation (or the project's t function) is imported/available in
capabilities-dialog.tsx; also add the corresponding translation key
("client.closed") to the locale files. Ensure you reference the isLocked
variable and the title prop when making the change.
In `@apps/web/src/components/server/server-edit-dialog.tsx`:
- Around line 222-242: The Select component is incorrectly given an unused items
prop; remove the items prop from each Select usage in server-edit-dialog.tsx
(the Select inside Field label={t('edit_group')} that uses groupId/setGroupId
and SelectItem children, and the two other Selects in the same file) so the
component relies solely on its <SelectItem> children pattern; leave
onValueChange, value, SelectTrigger, SelectContent and SelectItem entries
intact.
In `@apps/web/src/components/server/severity-bar.tsx`:
- Around line 44-47: The 2px minimum for safeHeight causes zero-value bars to
float above the baseline; change the logic in severity-bar.tsx so the height
floor is only applied when the original height > 0 (e.g., compute safeHeight =
height > 0 ? Math.max(height, 2) : 0) and compute safeY accordingly (use safeY =
y + (height - safeHeight) when height > 0, otherwise safeY = y) before returning
the <rect>; update references to safeHeight and safeY in the returned rect to
avoid drawing a small bar above the axis for zero values.
In `@apps/web/src/components/ui/dialog.tsx`:
- Line 79: The ScrollArea invocation currently passes data-slot="dialog-body"
which overrides ScrollArea's own data-slot="scroll-area"; remove data-slot from
the ScrollArea component and instead add data-slot="dialog-body" to the inner
wrapper <div> used for the DialogBody content so the ScrollArea keeps its
internal data-slot="scroll-area" (preserving CSS/test selectors like
scroll-area.test.tsx) while the dialog-specific identifier remains on the inner
wrapper; update any related tests (e.g., server-edit-dialog.test.tsx)
expectations if needed.
In `@crates/server/src/service/agent_manager.rs`:
- Around line 669-674: The database write Result from
ServerService::update_features is being ignored; change the call that currently
does `let _ = ...` to handle the Result and log failures (e.g., using your
crate's logging/tracing facility) so DB errors are not silently discarded.
Specifically, await the result of ServerService::update_features(&state.db,
server_id, &persisted_features).await, and on Err(e) call error logging with
context including server_id and a brief description (and optional
persisted_features summary) so failures are visible and in-memory vs persisted
state divergence can be diagnosed.
---
Outside diff comments:
In `@apps/web/src/components/server/server-edit-dialog.tsx`:
- Line 133: The current serialization sets expired_at to
`${expiredAt}T00:00:00Z` which turns a local YYYY-MM-DD into a UTC instant and
causes off-by-one day for negative offsets; instead serialize a timezone-neutral
plain date or compute the UTC Y/M/D explicitly. Update the serialization in
server-edit-dialog.tsx (the expired_at field that uses expiredAt and the
formatIsoDate output) to send expired_at as the plain YYYY-MM-DD string (i.e.
expiredAt) or, if you must send an instant, build the UTC date using UTC getters
(getUTCFullYear/getUTCMonth/getUTCDate) or a Date -> toISOString() slice so the
round-trip with server.expired_at?.slice(0,10) remains stable.
---
Nitpick comments:
In `@apps/web/src/components/data-table/data-table-column-header.tsx`:
- Line 51: The DropdownMenuContent in data-table-column-header.tsx currently
passes className="min-w-28", which via twMerge overrides the component's default
min-w-32 and makes the dropdown 112px wide; if the narrower width is not
intentional, remove the "min-w-28" from the className (or replace it with
"min-w-32" to be explicit) on the DropdownMenuContent usage to restore the
default 128px width and keep other classes intact.
In `@apps/web/src/components/data-table/data-table.tsx`:
- Line 18: The global descendant Tailwind selectors on the Table ("[&_td]:px-3
[&_th]:px-3") force horizontal padding on all cells and will override per-column
meta.className/cellClassName; remove those descendant selectors from the Table
className and instead apply the default horizontal padding when rendering cells
(merge a default "px-3" with any per-column meta.className or cellClassName
using the existing cn() helper in the cell render path). Update the cell
rendering logic (where you read meta.className / cellClassName) to compose
defaults with column-level classes so columns can override padding.
In `@apps/web/src/components/server/server-card.tsx`:
- Around line 31-35: The current isSeverityBarShapeProps predicate only checks
for non-null object and overclaims the stronger type; update
isSeverityBarShapeProps to also verify the minimal fields SeverityBar consumes
by checking that the value has a numeric, positive 'width' property and a
'payload' property (e.g., ensure typeof (value as any).width === 'number' and
(value as any).payload !== undefined) so TypeScript narrowing matches runtime
validation; apply the same fix to the other identical guard at the second
occurrence around the SeverityBar usage.
- Line 2: The file uses React.ReactNode in the component return type but only
imports named members from 'react'; update the top import to add a type import
for ReactNode (import type { ReactNode } from 'react') and replace the return
annotation React.ReactNode with ReactNode in the component (e.g., the ServerCard
component's return type). This keeps imports consistent with
ComponentProps/memo/useId/useMemo usage and avoids relying on the UMD global.
- Around line 104-119: Duplicate renderSpeedValue implementations should be
consolidated into a single shared helper (e.g., export a renderSpeedValue from a
shared utils module) that accepts parameters for zero-case rendering and a
unitClassName/variant override so callers can control the unit span styling;
update the two call sites (the existing renderSpeedValue in this component and
the one in index.cells.tsx) to import and use the shared helper with appropriate
class overrides, and optionally extract/renderBytesValue the same way if it
duplicates behavior.
In `@crates/server/src/task/record_writer.rs`:
- Around line 68-85: Extract a small helper (e.g., write_traffic_state) to
deduplicate the state-write logic used in the first-observation branch and the
tail state-write: the helper should accept the transfer cache, server_id,
curr_in, curr_out, writes_allowed and a reference to state.db, insert/update the
in-memory cache (transfer_cache.insert(server_id.clone(), (curr_in, curr_out))),
then if writes_allowed call TrafficService::upsert_state(&state.db, server_id,
curr_in, curr_out).await and log errors with tracing::error!, otherwise log the
recovery-frozen info; replace the duplicated blocks around
transfer_cache.get(...) and the later tail-write (the sites using
transfer_cache, compute_delta, and TrafficService::upsert_state) with a single
call to write_traffic_state so both paths share the same behavior.
In `@docs/superpowers/plans/2026-04-18-server-card-latency-compact.md`:
- Around line 286-294: The import replacement narrative is inconsistent: update
the doc steps so Step 1 shows replacing "getLatencyBarColor, isLatencyFailure"
with "getCombinedBarColor, getCombinedSeverity, getLossDotBgClass,
isLatencyFailure" and adjust Step 6 ("清理未使用 import") to state that ultracite
will remove getLatencyBarColor (which is no longer present) while keeping
isLatencyFailure; ensure references to getLatencyBarColor are removed or marked
as cleaned up and that isLatencyFailure is documented as still imported.
In `@docs/superpowers/specs/2026-04-18-server-card-latency-compact-design.md`:
- Around line 74-91: The function getCombinedSeverity currently uses the union
return type LatencyStatus | 'severe'; update its signature to return the
dedicated CombinedSeverity type (CombinedSeverity = 'unknown' | 'healthy' |
'warning' | 'severe' | 'failed') instead of widening LatencyStatus, and ensure
CombinedSeverity is imported or declared in the same module so callers remain
unchanged; keep the function body logic as-is but replace the return type
reference from LatencyStatus | 'severe' to CombinedSeverity to keep the spec
consistent and avoid extending LatencyStatus.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 92eb6e7d-b894-4d69-9429-a31d5bfcceea
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (45)
CHANGELOG.mdCargo.tomlapps/web/src/components/data-table/data-table-column-header.tsxapps/web/src/components/data-table/data-table-view-options.tsxapps/web/src/components/data-table/data-table.tsxapps/web/src/components/server/capabilities-dialog.tsxapps/web/src/components/server/compact-metric.tsxapps/web/src/components/server/recovery-merge-dialog.tsxapps/web/src/components/server/server-card.test.tsxapps/web/src/components/server/server-card.tsxapps/web/src/components/server/server-edit-dialog.test.tsxapps/web/src/components/server/server-edit-dialog.tsxapps/web/src/components/server/severity-bar.tsxapps/web/src/components/ui/dialog.tsxapps/web/src/components/ui/scroll-area.test.tsxapps/web/src/components/ui/scroll-area.tsxapps/web/src/lib/network-latency-constants.test.tsapps/web/src/lib/network-latency-constants.tsapps/web/src/locales/en/common.jsonapps/web/src/locales/en/servers.jsonapps/web/src/locales/zh/common.jsonapps/web/src/locales/zh/servers.jsonapps/web/src/routes/_authed/servers/index.cells.test.tsxapps/web/src/routes/_authed/servers/index.cells.tsxapps/web/src/routes/_authed/servers/index.tsxapps/web/src/types/data-table.tsbiome.jsoncrates/server/src/config.rscrates/server/src/entity/mod.rscrates/server/src/router/api/server.rscrates/server/src/router/api/server_recovery.rscrates/server/src/router/ws/agent.rscrates/server/src/router/ws/browser.rscrates/server/src/service/agent_manager.rscrates/server/src/service/alert.rscrates/server/src/service/notification.rscrates/server/src/service/recovery_job.rscrates/server/src/service/recovery_merge.rscrates/server/src/service/server_tag.rscrates/server/src/service/upgrade_release.rscrates/server/src/task/record_writer.rscrates/server/tests/email_migration_integration.rscrates/server/tests/integration.rsdocs/superpowers/plans/2026-04-18-server-card-latency-compact.mddocs/superpowers/specs/2026-04-18-server-card-latency-compact-design.md
💤 Files with no reviewable changes (2)
- apps/web/src/locales/zh/servers.json
- apps/web/src/locales/en/servers.json
| checked={isEnabled} | ||
| disabled={mutation.isPending || isLocked} | ||
| onCheckedChange={() => toggle(capability.bit)} | ||
| title={isLocked ? '客户端关闭' : undefined} |
There was a problem hiding this comment.
Hardcoded Chinese tooltip string.
title={isLocked ? '客户端关闭' : undefined} hardcodes Chinese text, bypassing i18n. Given this PR advertises "i18n polish," this should use t(...).
Proposed fix
- title={isLocked ? '客户端关闭' : undefined}
+ title={isLocked ? t('cap_client_locked', { defaultValue: 'Locked by client' }) : undefined}📝 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.
| title={isLocked ? '客户端关闭' : undefined} | |
| title={isLocked ? t('cap_client_locked', { defaultValue: 'Locked by client' }) : undefined} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/components/server/capabilities-dialog.tsx` at line 144, The
title prop on the element uses a hardcoded Chinese string (title={isLocked ?
'客户端关闭' : undefined}); replace this with the i18n helper (e.g., title={isLocked
? t('client.closed') : undefined}) and make sure useTranslation (or the
project's t function) is imported/available in capabilities-dialog.tsx; also add
the corresponding translation key ("client.closed") to the locale files. Ensure
you reference the isLocked variable and the title prop when making the change.
| <Field label={t('edit_group')}> | ||
| <Select | ||
| items={{ | ||
| __none__: t('edit_none'), | ||
| monthly: t('edit_monthly'), | ||
| quarterly: t('edit_quarterly'), | ||
| yearly: t('edit_yearly') | ||
| }} | ||
| onValueChange={(v) => setBillingCycle(v === '__none__' || v === null ? '' : v)} | ||
| value={billingCycle || '__none__'} | ||
| items={[ | ||
| { value: '__none__', label: t('edit_no_group') }, | ||
| ...(groups?.map((g) => ({ value: g.id, label: g.name })) ?? []) | ||
| ]} | ||
| onValueChange={(v) => setGroupId(v === '__none__' || v === null ? '' : v)} | ||
| value={groupId || '__none__'} | ||
| > | ||
| <SelectTrigger className="w-full"> | ||
| <SelectValue /> | ||
| </SelectTrigger> | ||
| <SelectContent> | ||
| <SelectItem value="__none__">{t('edit_none')}</SelectItem> | ||
| <SelectItem value="monthly">{t('edit_monthly')}</SelectItem> | ||
| <SelectItem value="quarterly">{t('edit_quarterly')}</SelectItem> | ||
| <SelectItem value="yearly">{t('edit_yearly')}</SelectItem> | ||
| <SelectItem value="__none__">{t('edit_no_group')}</SelectItem> | ||
| {groups?.map((g) => ( | ||
| <SelectItem key={g.id} value={g.id}> | ||
| {g.name} | ||
| </SelectItem> | ||
| ))} | ||
| </SelectContent> | ||
| </Select> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd -t f 'select.tsx' apps/web/src/components/ui
ast-grep --pattern $'function Select($_) { $$$ }'
rg -nP '\bitems\s*[:=]' apps/web/src/components/ui/select.tsx -C3Repository: ZingerLittleBee/ServerBee
Length of output: 106
🏁 Script executed:
cat -n apps/web/src/components/ui/select.tsx | head -100Repository: ZingerLittleBee/ServerBee
Length of output: 5443
🏁 Script executed:
cat -n apps/web/src/components/server/server-edit-dialog.tsx | sed -n '215,250p'Repository: ZingerLittleBee/ServerBee
Length of output: 2031
🏁 Script executed:
cat -n apps/web/src/components/server/server-edit-dialog.tsx | sed -n '310,375p'Repository: ZingerLittleBee/ServerBee
Length of output: 3626
Remove unsupported items prop from Select components.
The Select component wraps @base-ui/react/select's Root, which does not support an items prop. All three Select usages (lines 223–242, 314–334, and 354–372) pass an unused items prop alongside the correct <SelectItem> children pattern. Remove the items prop from all three instances to clean up dead code and avoid confusion about which pattern actually renders.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/components/server/server-edit-dialog.tsx` around lines 222 -
242, The Select component is incorrectly given an unused items prop; remove the
items prop from each Select usage in server-edit-dialog.tsx (the Select inside
Field label={t('edit_group')} that uses groupId/setGroupId and SelectItem
children, and the two other Selects in the same file) so the component relies
solely on its <SelectItem> children pattern; leave onValueChange, value,
SelectTrigger, SelectContent and SelectItem entries intact.
| const safeHeight = Math.max(height, 2) | ||
| const safeY = y + (height - safeHeight) | ||
|
|
||
| return <rect fill={payload.fillColor} height={safeHeight} rx={radius} ry={radius} width={width} x={x} y={safeY} /> |
There was a problem hiding this comment.
safeHeight minimum of 2 can push a zero-value bar above the axis baseline.
For a healthy point with value near 0, Recharts may pass height === 0 with y at the axis baseline. safeHeight = max(height, 2) gives 2, and safeY = y + (height - safeHeight) = y - 2, so a 2px rect is drawn two pixels above the axis — i.e., outside the bar plot area. In a 32px chart this is barely visible, but it does make zero-latency bars look slightly "floating" on the top side.
Consider either:
- not bumping the minimum height for zero-value bars (only enforce the floor when
height > 0), or - keeping the floor but anchoring to the baseline, e.g.
safeY = y + height - safeHeightonly whenheight > 0, elsesafeY = y.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/components/server/severity-bar.tsx` around lines 44 - 47, The
2px minimum for safeHeight causes zero-value bars to float above the baseline;
change the logic in severity-bar.tsx so the height floor is only applied when
the original height > 0 (e.g., compute safeHeight = height > 0 ?
Math.max(height, 2) : 0) and compute safeY accordingly (use safeY = y + (height
- safeHeight) when height > 0, otherwise safeY = y) before returning the <rect>;
update references to safeHeight and safeY in the returned rect to avoid drawing
a small bar above the axis for zero values.
|
|
||
| function DialogBody({ className, children, ...props }: React.ComponentProps<'div'>) { | ||
| return ( | ||
| <ScrollArea className="-mx-4 max-h-[calc(100vh-14rem)] min-h-0 flex-1" data-slot="dialog-body"> |
There was a problem hiding this comment.
data-slot="dialog-body" overrides ScrollArea's internal data-slot="scroll-area".
ScrollArea spreads props onto ScrollAreaPrimitive.Root after its own data-slot="scroll-area", so this prop replaces it. Any CSS or test selectors targeting [data-slot="scroll-area"] (including the assertions in scroll-area.test.tsx) won't match the DialogBody root. If you want both identifiers, attach data-slot="dialog-body" to the inner wrapper <div> instead.
Proposed fix
- <ScrollArea className="-mx-4 max-h-[calc(100vh-14rem)] min-h-0 flex-1" data-slot="dialog-body">
- <div className={cn('px-4', className)} {...props}>
+ <ScrollArea className="-mx-4 max-h-[calc(100vh-14rem)] min-h-0 flex-1">
+ <div className={cn('px-4', className)} data-slot="dialog-body" {...props}>
{children}
</div>
</ScrollArea>Note: server-edit-dialog.test.tsx asserts form?.querySelector('[data-slot="dialog-body"]') — still works since it mocks DialogBody.
📝 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.
| <ScrollArea className="-mx-4 max-h-[calc(100vh-14rem)] min-h-0 flex-1" data-slot="dialog-body"> | |
| <ScrollArea className="-mx-4 max-h-[calc(100vh-14rem)] min-h-0 flex-1"> | |
| <div className={cn('px-4', className)} data-slot="dialog-body" {...props}> | |
| {children} | |
| </div> | |
| </ScrollArea> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/components/ui/dialog.tsx` at line 79, The ScrollArea invocation
currently passes data-slot="dialog-body" which overrides ScrollArea's own
data-slot="scroll-area"; remove data-slot from the ScrollArea component and
instead add data-slot="dialog-body" to the inner wrapper <div> used for the
DialogBody content so the ScrollArea keeps its internal data-slot="scroll-area"
(preserving CSS/test selectors like scroll-area.test.tsx) while the
dialog-specific identifier remains on the inner wrapper; update any related
tests (e.g., server-edit-dialog.test.tsx) expectations if needed.
| let _ = crate::service::server::ServerService::update_features( | ||
| &state.db, | ||
| server_id, | ||
| &persisted_features, | ||
| ) | ||
| .await; |
There was a problem hiding this comment.
Log database errors instead of silently discarding them.
The update_features call returns Result<(), DbErr>, but the error is discarded with let _ = .... If the database write fails silently, the in-memory state (line 666) will diverge from the persisted state, potentially causing the server to incorrectly display Docker availability after a restart.
📝 Proposed fix to add error logging
- let _ = crate::service::server::ServerService::update_features(
+ if let Err(e) = crate::service::server::ServerService::update_features(
&state.db,
server_id,
&persisted_features,
)
- .await;
+ .await
+ {
+ tracing::error!("Failed to persist docker feature removal for {server_id}: {e}");
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/server/src/service/agent_manager.rs` around lines 669 - 674, The
database write Result from ServerService::update_features is being ignored;
change the call that currently does `let _ = ...` to handle the Result and log
failures (e.g., using your crate's logging/tracing facility) so DB errors are
not silently discarded. Specifically, await the result of
ServerService::update_features(&state.db, server_id, &persisted_features).await,
and on Err(e) call error logging with context including server_id and a brief
description (and optional persisted_features summary) so failures are visible
and in-memory vs persisted state divergence can be diagnosed.
Summary
Test Plan
make lintmake typecheckmake docs-lintmake build-webmake cargo-clippycargo test -p serverbee-server test_resend_config_reads_env_varcargo test -p serverbee-server rewrite_server_ids_json_replaces_source_with_target_oncecargo test -p serverbee-server finalize_target_server_row_copies_runtime_fields_and_cleans_source_rowsSummary by CodeRabbit
Release Notes v0.8.10
New Features
Improvements
Bug Fixes