Skip to content

feat: redesign server views and add tags support#102

Merged
ZingerLittleBee merged 22 commits intomainfrom
pangyo-v3
Apr 17, 2026
Merged

feat: redesign server views and add tags support#102
ZingerLittleBee merged 22 commits intomainfrom
pangyo-v3

Conversation

@ZingerLittleBee
Copy link
Copy Markdown
Owner

@ZingerLittleBee ZingerLittleBee commented Apr 17, 2026

Summary

  • add server tag APIs and related server/browser sync changes on the Rust side
  • redesign the server table and card UX, including compact latency severity visuals, traffic quota helpers, status dots, and i18n polish in the web app
  • include the supporting specs, plans, and lint/clippy cleanup needed for the branch to pass static checks

Test Plan

  • make lint
  • make typecheck
  • make docs-lint
  • make build-web
  • make cargo-clippy
  • cargo test -p serverbee-server test_resend_config_reads_env_var
  • cargo test -p serverbee-server rewrite_server_ids_json_replaces_source_with_target_once
  • cargo test -p serverbee-server finalize_target_server_row_copies_runtime_fields_and_cleans_source_rows

Summary by CodeRabbit

Release Notes v0.8.10

  • New Features

    • Added server tag management in the edit dialog with validated, deduplicated tags
    • Redesigned server card latency display combining latency and packet loss indicators
    • Added date picker for server expiration field configuration
  • Improvements

    • Reorganized servers table layout for improved density and readability
    • Enhanced table controls with better internationalization coverage
    • Refined server edit and recovery dialogs with improved layout and scroll behavior
  • Bug Fixes

    • Fixed SVG pattern isolation in failed latency bar charts
    • Corrected translation key references and dialog sizing

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
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
server-bee-docs Ready Ready Preview, Comment Apr 17, 2026 5:58pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

📝 Walkthrough

Walkthrough

Release 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

Cohort / File(s) Summary
Version & Release Notes
CHANGELOG.md, Cargo.toml
Bump version to v0.8.10; document server tags, server card latency redesign, table density improvements, and polish/fix entries.
Data Table i18n & Layout
apps/web/src/components/data-table/data-table-column-header.tsx, data-table-view-options.tsx, data-table.tsx, apps/web/src/types/data-table.ts
Add useTranslation('common') and replace hardcoded dropdown/menu strings with i18n keys; adjust dropdown width from w-28 to min-w-28; add default horizontal padding (px-3) to table cells; introduce optional cellClassName metadata field for per-column cell styling.
Dialog Container Components
apps/web/src/components/ui/dialog.tsx
Add new exported DialogBody component wrapping content in ScrollArea with flex growth and max-height constraints; update DialogContent from grid to flex column layout with overflow-hidden.
Dialog Refactorings
apps/web/src/components/server/capabilities-dialog.tsx, recovery-merge-dialog.tsx, server-edit-dialog.tsx
Wrap dialog content in new DialogBody/DialogFooter components; remove inline scroll/max-height from DialogContent; improve spacing and layout organization; add custom DatePickerField component using Popover+Calendar for date selection in server-edit-dialog.
Network Latency Logic
apps/web/src/lib/network-latency-constants.ts, network-latency-constants.test.ts
Add loss ratio thresholds, introduce CombinedSeverity type and getCombinedSeverity/getCombinedBarColor/getLossDotBgClass functions; add comprehensive unit tests covering severity classification and color/class mappings.
Severity Bar Component
apps/web/src/components/server/severity-bar.tsx
New component renders latency/loss combined-severity bars; uses SVG pattern fills for failures and min-height clamping for small values; accepts failPatternId for diagonal stripe pattern rendering.
Server Card Latency Redesign
apps/web/src/components/server/server-card.tsx, server-card.test.tsx
Replace latency header layout and loss indicator; introduce SeverityBar shape for chart; precompute per-point severity via new helpers; update latency header to show when missing and loss as colored dot; update metric formatting with speed/byte value renderers; adjust test helper to use findHeroLatency for latency assertions.
Servers Table Metric Redesign
apps/web/src/routes/_authed/servers/index.cells.tsx, index.cells.test.tsx, index.tsx, apps/web/src/components/server/compact-metric.tsx
Add renderBytesValue/renderSpeedValue helpers; introduce PositionIndicator component; refactor CPU/Memory/Disk/Network cells with header-style rows and position indicators; update column meta with label translations and cellClassName; adjust column widths; change CompactMetricProps to accept React.ReactNode for label/value.
ScrollArea Component Updates
apps/web/src/components/ui/scroll-area.tsx, scroll-area.test.tsx
Adjust ScrollAreaPrimitive.Root to include flex layout constraints (flex, min-h-0); change viewport from size-full to min-h-0 flex-1; wrap children in ScrollAreaPrimitive.Content; add test coverage for flex sizing.
Localization
apps/web/src/locales/en/common.json, en/servers.json, zh/common.json, zh/servers.json
Add table control i18n keys (view options, column toggle, search, sort actions, hide column) to common.json in both languages; remove unused card_latency key from servers.json in both languages.
Backend Refactoring & Tests
crates/server/src/config.rs, entity/mod.rs, router/api/server.rs, router/api/server_recovery.rs, router/ws/agent.rs, router/ws/browser.rs, service/alert.rs, service/agent_manager.rs, service/notification.rs, service/recovery_job.rs, service/recovery_merge.rs, service/server_tag.rs, service/upgrade_release.rs, task/record_writer.rs, tests/email_migration_integration.rs, tests/integration.rs
Code formatting and structure adjustments: reorder imports, collapse/expand assertions and expressions for readability, adjust error handling in test code, remove internal pub(crate) wrapper functions in recovery_merge.rs, adjust control flow in record_writer.rs for traffic delta computation.
Configuration
biome.json
Add .superpowers glob to files.includes ignore patterns.
Documentation
docs/superpowers/plans/2026-04-18-server-card-latency-compact.md, docs/superpowers/specs/2026-04-18-server-card-latency-compact-design.md
Add plan and design spec documenting the server card latency/loss redesign, severity bar implementation, color/class mappings, and testing guidance.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

  • PR #92: Touches the same i18n surface—adding translation keys and wiring useTranslation across UI components (dialogs, server-card, data-table, locales).
  • PR #101: Directly related—both implement server tags backend and frontend (server tag CRUD, /api/servers/{id}/tags, ServerEditDialog tag flow, tag propagation in WebSocket sync).
  • PR #94: Both modify apps/web/src/components/server/server-card.tsx and its tests to rework latency/loss rendering and performance optimizations.

Poem

🐰 Charts now bloom with merged severity hues,
Bar patterns dance when networks misconstrue,
Compact tables shine with density bright,
Dialog bounds reformatted just right—
A rabbit's redesign, precise and tight! 🎨✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: server view redesigns and tag support features are central throughout the PR across both backend and frontend code.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pangyo-v3

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 | 🟡 Minor

Timezone skew when serializing expired_at.

expiredAt is a local YYYY-MM-DD from formatIsoDate (uses getFullYear/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 via server.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 using getUTCFullYear() 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_state fails 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 this continue, the first observation still produces no traffic_hourly row 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 annotation LatencyStatus | 'severe' is inconsistent with the plan's CombinedSeverity type.

The implementation plan (2026-04-18-server-card-latency-compact.md L123) introduces a dedicated CombinedSeverity = 'unknown' | 'healthy' | 'warning' | 'severe' | 'failed' type, which is cleaner than widening LatencyStatus. Consider updating this spec snippet to reference CombinedSeverity so the two docs stay in sync and contributors don't accidentally extend LatencyStatus with '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-3 will override any per-column horizontal padding set via meta.className/cellClassName.

Tailwind utility specificity is equal, but descendant selectors like [&_td]:px-3 compile 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 sets px-4 or px-0 via meta.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 via cn() 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 drops isLatencyFailure from 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) is import { getCombinedBarColor, getCombinedSeverity, getLossDotBgClass, isLatencyFailure } .... That's fine in isolation, but Step 6 ("清理未使用 import") states ultracite will auto-remove getLatencyBarColor — 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 removing min-w-28 to use the default min-w-32.

DropdownMenuContent applies min-w-32 as a base class, and the className prop merges with it via twMerge. Since min-w-28 appears 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 use min-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: isSeverityBarShapeProps is a trivial object guard.

The predicate is typed as value is SeverityBarShapeProps but only asserts typeof 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 SeverityBar actually 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 — SeverityBar already handles missing payload / non-positive width.

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: Prefer import type { ReactNode } from 'react' over React.ReactNode.

Line 2 only imports specific named members from react and doesn't bring the React namespace into scope, yet line 104 annotates the return type as React.ReactNode. This compiles today thanks to @types/react's UMD global, but it's inconsistent with apps/web/src/routes/_authed/servers/index.cells.tsx which uses import 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: Duplicate renderSpeedValue across files — consolidate into a shared helper.

renderSpeedValue here and in apps/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 uses text-[10px] and text-muted-foreground.
  • index.cells.tsx: returns <span className="text-xs">0</span>; unit span uses text-[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., renderSpeedValue in @/lib/utils) that accepts a variant or a className override for the unit span, and have both call sites use it. Same for renderBytesValue in index.cells.tsx if 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8099fff and d701c3e.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (45)
  • CHANGELOG.md
  • Cargo.toml
  • apps/web/src/components/data-table/data-table-column-header.tsx
  • apps/web/src/components/data-table/data-table-view-options.tsx
  • apps/web/src/components/data-table/data-table.tsx
  • apps/web/src/components/server/capabilities-dialog.tsx
  • apps/web/src/components/server/compact-metric.tsx
  • apps/web/src/components/server/recovery-merge-dialog.tsx
  • apps/web/src/components/server/server-card.test.tsx
  • apps/web/src/components/server/server-card.tsx
  • apps/web/src/components/server/server-edit-dialog.test.tsx
  • apps/web/src/components/server/server-edit-dialog.tsx
  • apps/web/src/components/server/severity-bar.tsx
  • apps/web/src/components/ui/dialog.tsx
  • apps/web/src/components/ui/scroll-area.test.tsx
  • apps/web/src/components/ui/scroll-area.tsx
  • apps/web/src/lib/network-latency-constants.test.ts
  • apps/web/src/lib/network-latency-constants.ts
  • apps/web/src/locales/en/common.json
  • apps/web/src/locales/en/servers.json
  • apps/web/src/locales/zh/common.json
  • apps/web/src/locales/zh/servers.json
  • apps/web/src/routes/_authed/servers/index.cells.test.tsx
  • apps/web/src/routes/_authed/servers/index.cells.tsx
  • apps/web/src/routes/_authed/servers/index.tsx
  • apps/web/src/types/data-table.ts
  • biome.json
  • crates/server/src/config.rs
  • crates/server/src/entity/mod.rs
  • crates/server/src/router/api/server.rs
  • crates/server/src/router/api/server_recovery.rs
  • crates/server/src/router/ws/agent.rs
  • crates/server/src/router/ws/browser.rs
  • crates/server/src/service/agent_manager.rs
  • crates/server/src/service/alert.rs
  • crates/server/src/service/notification.rs
  • crates/server/src/service/recovery_job.rs
  • crates/server/src/service/recovery_merge.rs
  • crates/server/src/service/server_tag.rs
  • crates/server/src/service/upgrade_release.rs
  • crates/server/src/task/record_writer.rs
  • crates/server/tests/email_migration_integration.rs
  • crates/server/tests/integration.rs
  • docs/superpowers/plans/2026-04-18-server-card-latency-compact.md
  • docs/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}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +222 to 242
<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>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/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 -C3

Repository: ZingerLittleBee/ServerBee

Length of output: 106


🏁 Script executed:

cat -n apps/web/src/components/ui/select.tsx | head -100

Repository: 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.

Comment on lines +44 to +47
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} />
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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 - safeHeight only when height > 0, else safeY = 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">
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
<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.

Comment on lines +669 to +674
let _ = crate::service::server::ServerService::update_features(
&state.db,
server_id,
&persisted_features,
)
.await;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

@ZingerLittleBee ZingerLittleBee merged commit 2c6cae1 into main Apr 17, 2026
10 checks passed
@ZingerLittleBee ZingerLittleBee deleted the pangyo-v3 branch April 17, 2026 18:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant