Skip to content

feat(web,server): servers table row visual redesign with tags#101

Merged
ZingerLittleBee merged 44 commits intomainfrom
little-rock
Apr 17, 2026
Merged

feat(web,server): servers table row visual redesign with tags#101
ZingerLittleBee merged 44 commits intomainfrom
little-rock

Conversation

@ZingerLittleBee
Copy link
Copy Markdown
Owner

@ZingerLittleBee ZingerLittleBee commented Apr 17, 2026

Summary

Rebuilds every row in /servers?view=table so each metric cell is a two-line block (icon + bar + % on top, compact sub-data below), replaces the status text badge with a pulsing dot column, shows server_tags under the server name, and surfaces monthly traffic quota usage as a bar in the Network cell. Also extends the backend ServerStatus WS payload with tags + cpu_cores, adds a server_tags REST surface (GET /api/servers/:id/tags, PUT /api/servers/:id/tags), and wires a tag editor into ServerEditDialog.

  • Phase A — frontend visual refactor (new MetricBarRow, StatusDot, TagChipRow, CpuCell/MemoryCell/DiskCell/NetworkCell/UptimeCell/NameCell, shared lib/traffic.ts)
  • Phase B — backend tags + cpu_cores in ServerStatus.full_sync, service/server_tag.rs (validation + CRUD), REST router with admin-only write, integration tests for RBAC/validation/full_sync payload
  • Tag editor in ServerEditDialog with client-side validation, sequential PATCH→PUT save, and revert-on-PUT-failure UX

Spec & plan

  • Spec: docs/superpowers/specs/2026-04-17-servers-table-row-visual-redesign-design.md
  • Plan: docs/superpowers/plans/2026-04-17-servers-table-row-visual-redesign.md

Test plan

  • bun run --cwd apps/web test → 382/382 green (+35 new)
  • bun run typecheck → exit 0
  • bun x ultracite check apps/web/src → clean
  • cargo clippy --workspace -- -D warnings → clean
  • cargo test --workspace → 57 pass + 9 new integration tests for /api/servers/:id/tags and browser_ws full_sync includes tags + cpu_cores
  • Manual QA using tests/servers/table-row-visual-redesign.md

Notes

  • One pre-existing unrelated test failure (test_server_detail_returns_runtime_capability_fields: CAP_DEFAULT 56 vs 60) confirmed present on commit 3700922 before any of this work. Tracked separately.
  • Pre-existing bun run build TS inference errors in existing test fixtures (also present on 3700922) are unchanged by this PR.
  • Phase C (live tag propagation over WS) is explicitly out of scope.

Summary by CodeRabbit

  • New Features

    • Server tags: view, edit, and persist tags per server; tag parsing/validation prevents invalid input.
    • Per-server CPU cores now displayed when available.
  • UI/UX Improvements

    • Redesigned servers table rows with compact metric cells, new pulsing status dot, stable tag chips, and improved network quota display (fallback & clamped percent).
    • Admin action buttons gained icon sizing, labels and tooltips for better accessibility.
  • Localization

    • Added English and Chinese strings for tag editor, validation messages, and offline/last-seen text.

@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 11:15am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

📝 Walkthrough

Walkthrough

Adds server tagging (validation, persistence, REST hooks), refactors servers table into composable metric cells (status-dot, tag chips, traffic quota), introduces traffic-quota utilities and tests, extends WebSocket payloads/types to include tags and cpu_cores, and wires backend tag service + APIs and integration tests.

Changes

Cohort / File(s) Summary
Dashboard Switcher
apps/web/src/components/dashboard/dashboard-switcher.tsx
Adjusted star/name rendering and changed admin action buttons to size="icon-sm" with aria-label/title.
Server Card / Traffic
apps/web/src/components/server/server-card.tsx, apps/web/src/lib/traffic.ts, apps/web/src/lib/traffic.test.ts
Replaced inline traffic computation with computeTrafficQuota; added DEFAULT_TRAFFIC_LIMIT_BYTES and unit tests.
Server Edit Dialog (Tags)
apps/web/src/components/server/server-edit-dialog.tsx, apps/web/src/locales/en/servers.json, apps/web/src/locales/zh/servers.json
Added tags editor, client-side parsing/validation, tags save flow (PATCH then PUT), and i18n strings for tag UI/errors.
Status & Tag UI Components + Tests
apps/web/src/components/server/status-dot.tsx, .../status-dot.test.tsx, apps/web/src/components/server/tag-chip.tsx, .../tag-chip.test.tsx
New StatusDot and TagChipRow components with corresponding Vitest tests (visual/state assertions, stable palette hashing).
Server Tags Hooks + Tests
apps/web/src/hooks/use-server-tags.ts, .../use-server-tags.test.tsx
Added useServerTags (query) and useUpdateServerTags (mutation) with cache updates; tests mock fetch and assert cache updates.
Realtime / WS Merge Logic & Types
apps/web/src/hooks/use-servers-ws.ts, .../use-servers-ws.test.tsx, crates/common/src/types.rs
Extended ServerMetrics/ServerStatus with tags and cpu_cores, made disk I/O fields required, added static-field guard for empty arrays; tests for merge preservation.
Servers Table Cells & Tests
apps/web/src/routes/_authed/servers/index.cells.tsx, .../index.cells.test.tsx
Extracted per-metric cell components (CpuCell, MemoryCell, DiskCell, NetworkCell, UptimeCell, NameCell), MetricBarRow, color logic, and comprehensive RTL tests including network quota behavior.
Servers Route Rewire
apps/web/src/routes/_authed/servers/index.tsx
Replaced inline renders with new cells, swapped StatusBadgeStatusDot column, integrated useTrafficOverview() and updated imports/dependencies.
Backend: Tag Service & APIs
crates/server/src/service/server_tag.rs, crates/server/src/router/api/server_tag.rs, crates/server/src/router/api/mod.rs, crates/server/src/service/mod.rs
Implemented tag validation/normalization (MAX_TAGS, MAX_TAG_LEN), list_tags/set_tags, added REST endpoints GET/PUT /servers/{id}/tags, and registered module in routers.
Backend: WS Full-Sync & AgentManager
crates/server/src/router/ws/browser.rs, crates/server/src/service/agent_manager.rs
Full-sync now queries tags and populates tags and cpu_cores in ServerStatus; incremental updates initialize tags/cpu_cores.
Integration Tests (backend)
crates/server/tests/integration.rs
Added end-to-end tests for tag endpoints, RBAC, validation, and WS full-sync shape asserting tags and cpu_cores.
Tests / Helpers Updated
apps/web/src/components/dashboard/widgets/stat-number.test.tsx, apps/web/src/components/server/server-card.test.tsx, apps/web/src/hooks/use-realtime-metrics.test.tsx
Test helpers extended to include disk_read_bytes_per_sec and disk_write_bytes_per_sec defaults.
Docs & QA Plans
docs/superpowers/*, tests/servers/table-row-visual-redesign.md
Added design/spec/plans and manual QA checklist for the table row visual redesign and tagging feature.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as Server Edit Dialog
    participant API as Backend API
    participant DB as Database

    User->>UI: Open dialog
    UI->>API: GET /api/servers/{id}/tags (useServerTags)
    API->>DB: SELECT tags BY server_id
    DB-->>API: tags[]
    API-->>UI: tags[]

    User->>UI: Edit tags
    UI->>UI: parseTagsInput (validate)
    User->>UI: Click Save
    UI->>API: PATCH /api/servers/{id} (details)
    API->>DB: update server
    DB-->>API: OK
    API-->>UI: OK
    UI->>API: PUT /api/servers/{id}/tags (if dirty)
    API->>API: validate & normalize
    API->>DB: delete/insert tags (transaction)
    DB-->>API: OK
    API-->>UI: normalized tags
    UI->>UI: update cache
Loading
sequenceDiagram
    participant Client
    participant WS as WebSocket Server
    participant DB

    Client->>WS: connect & request full_sync
    WS->>DB: SELECT servers
    WS->>DB: SELECT server_tags ORDER BY server_id, tag
    DB-->>WS: servers[], tags[]
    WS->>WS: group tags by server_id
    WS->>Client: full_sync payload (includes tags, cpu_cores)
    Client->>Client: merge payload (preserve static fields on empty updates)
    Client->>UI: render table with tags & status dots
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

Poem

🐰 I hopped through tags and pulsing light,
Quotas counted, pixels bright.
WebSockets hummed a syncy tune,
DB wrote tags beneath the moon.
Tables tidy — hop! — delight 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.63% 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 PR title clearly describes the main change: a visual redesign of the servers table row with tags support, combining both frontend and backend improvements into a concise, specific summary.

✏️ 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 little-rock

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: 2

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/hooks/use-servers-ws.ts (1)

103-120: ⚠️ Potential issue | 🟡 Minor

Subtle: treating [] as a static default can hide legitimate empties.

The guard on line 110-111 suppresses updates whose value is an empty array for any static field. This is the intent for tags/features on noisy per-frame updates, but it also means that a legitimate "all tags removed" signal arriving via the update path would be ignored, and clients would keep stale tags until the next full_sync. Today, tag edits appear to flow via REST + query invalidation (use-server-tags.ts), so this is latent, not actively broken — worth either a comment documenting the assumption or a stricter rule that only suppresses when the key is absent from the incoming payload rather than when it is explicitly empty.

🛡️ One stricter alternative
-      for (const [key, value] of Object.entries(server)) {
-        const isStaticDefault =
-          STATIC_FIELDS.has(key) && (value === null || value === 0 || (Array.isArray(value) && value.length === 0))
-        if (!isStaticDefault) {
-          ;(merged as Record<string, unknown>)[key] = value
-        }
-      }
+      // For static fields, only skip when the key is absent from the incoming payload.
+      // Empty arrays / zeros / nulls that are explicitly present should still overwrite.
+      const incomingKeys = new Set(Object.keys(server))
+      for (const [key, value] of Object.entries(server)) {
+        if (STATIC_FIELDS.has(key) && !incomingKeys.has(key)) continue
+        ;(merged as Record<string, unknown>)[key] = value
+      }

(Adjust to match whatever "no-update" contract the agent/server actually produces for these frames.)

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

In `@apps/web/src/hooks/use-servers-ws.ts` around lines 103 - 120, In
mergeServerUpdate, the current suppression logic treats empty arrays as a
static-default and ignores explicit "clear" updates for fields like
tags/features; update the guard so we only suppress when the incoming payload
lacks the key (e.g., use a check for incoming key absence such as server[key]
=== undefined or Object.prototype.hasOwnProperty.call(server, key) rather than
Array.isArray(value) && value.length === 0), keeping STATIC_FIELDS logic but
allowing explicit empty arrays to overwrite existing values; refer to
mergeServerUpdate and STATIC_FIELDS to locate the change.
🧹 Nitpick comments (12)
docs/superpowers/plans/2026-04-17-servers-table-row-visual-redesign.md (1)

1-2644: Plan document; implementation is reviewed in the actual code files.

One small callout: Task 19's "stub" crates/server/tests/server_tags.rs containing only a comment creates an empty integration-test binary that cargo test will still link. Prefer omitting the file entirely (as the plan itself suggests) to avoid build noise.

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

In `@docs/superpowers/plans/2026-04-17-servers-table-row-visual-redesign.md`
around lines 1 - 2644, The empty-test stub crates/server/tests/server_tags.rs
(containing only a comment) produces a linked test binary and should be removed;
delete that file (or convert it to a non-test helper module) so only the real
tags tests live in crates/server/tests/integration.rs, then git rm the stub and
commit (e.g. "test(server): remove empty server_tags.rs stub — tests live in
integration.rs"). Ensure integration.rs contains the full tags tests and re-run
cargo test to verify no extra empty test binary remains.
docs/superpowers/plans/2026-04-16-servers-table-density.md (1)

1-784: Documentation-only plan; no review comments.

This plan file predates the current PR's redesign plan (2026-04-17) and may now be partially superseded. If it's kept for historical reference, consider adding a note pointing readers to the newer plan to avoid confusion.

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

In `@docs/superpowers/plans/2026-04-16-servers-table-density.md` around lines 1 -
784, This plan file is historical and should be marked as superseded by the
newer 2026-04-17 servers-table-density plan: add a short one-paragraph banner
immediately under the "# Servers Table Density & Disk I/O Display Implementation
Plan" heading that states this document is kept for historical reference and has
been superseded by the 2026-04-17 plan (include that date/string so readers can
find the newer doc), optionally add a "Superseded" tag or front-matter line and
update the top-level summary to point readers to the newer plan, then commit
with a descriptive message like "docs: mark 2026-04-16 servers table density
plan as superseded by 2026-04-17".
apps/web/src/components/server/tag-chip.tsx (2)

12-19: Minor: Math.trunc on line 16 is redundant.

Math.imul already returns a signed 32-bit integer, and adding charCodeAt(i) (≤ 0xFFFF) keeps the result integer-valued — Math.trunc on an integer-valued number is a no-op. You can drop it without changing semantics.

♻️ Proposed simplification
 function hashTag(tag: string): number {
   let h = 0
   for (let i = 0; i < tag.length; i++) {
     h = Math.imul(h, 31) + tag.charCodeAt(i)
-    h = Math.trunc(h)
   }
   return Math.abs(h) % PALETTE.length
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/server/tag-chip.tsx` around lines 12 - 19, Remove the
redundant Math.trunc call in the hashTag function: Math.imul already produces a
signed 32-bit integer and adding tag.charCodeAt(i) preserves integer semantics,
so delete the Math.trunc(h) step inside hashTag (the function that computes an
index into PALETTE) and return Math.abs(h) % PALETTE.length as before.

32-44: key={tag} assumes unique tags per server.

If duplicate tags ever slip past backend validation, React will emit a duplicate-key warning and render only one chip. The current server_tag service should enforce uniqueness (PK on (server_id, tag)), so this is defensive only — consider either falling back to an indexed key or adding a frontend dedup as a safety net.

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

In `@apps/web/src/components/server/tag-chip.tsx` around lines 32 - 44, The tag
chips use the tag string as React key (in the tags.map callback using
key={tag}), which will break if duplicate tags appear; update the rendering to
guarantee unique keys by using a stable index or composite key (e.g.,
`${tag}-${i}`) or deduplicate tags before mapping (e.g., create a unique list
from tags) so that the span elements rendered in the tags.map loop have unique
keys; reference the tags.map callback and key={tag} in tag-chip.tsx (and
consider server_tag uniqueness in backend but implement the frontend defensive
fix here).
apps/web/src/hooks/use-servers-ws.test.ts (1)

213-249: Consider consolidating baseServer with the existing makeServer helper.

baseServer is a near-duplicate of makeServer at lines 6-37 but with a more complete field set (adds tags, features, cpu_cores, disk_read_bytes_per_sec, disk_write_bytes_per_sec). Maintaining two fixture builders risks drift as ServerMetrics evolves. If feasible, extend makeServer to cover the new fields and retire baseServer.

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

In `@apps/web/src/hooks/use-servers-ws.test.ts` around lines 213 - 249, Replace
the duplicate fixture builder by extending the existing makeServer helper to
include the extra fields present in baseServer (tags, features, cpu_cores,
disk_read_bytes_per_sec, disk_write_bytes_per_sec and any other fields from
ServerMetrics), change makeServer’s signature to accept Partial<ServerMetrics>
overrides like baseServer, update all tests that call baseServer to call
makeServer instead, and then remove the baseServer function to avoid drift
between the two builders.
crates/server/src/router/ws/browser.rs (1)

266-275: Log tag-query failures instead of silently defaulting to empty.

unwrap_or_default() here is inconsistent with how the sibling ServerService::list_servers error is handled a few lines above (explicit tracing::error! on failure). If the server_tag query ever errors transiently, every browser sees empty tags with no diagnostic breadcrumb. Suggest mirroring the existing pattern.

🪵 Proposed fix
-    let tags_rows = server_tag::Entity::find()
-        .order_by_asc(server_tag::Column::ServerId)
-        .order_by_asc(server_tag::Column::Tag)
-        .all(&state.db)
-        .await
-        .unwrap_or_default();
+    let tags_rows = match server_tag::Entity::find()
+        .order_by_asc(server_tag::Column::ServerId)
+        .order_by_asc(server_tag::Column::Tag)
+        .all(&state.db)
+        .await
+    {
+        Ok(rows) => rows,
+        Err(e) => {
+            tracing::error!("Failed to load server tags for FullSync: {e}");
+            Vec::new()
+        }
+    };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/server/src/router/ws/browser.rs` around lines 266 - 275, The tags
query currently swallows errors via unwrap_or_default() causing silent empty tag
lists; replace that with explicit error handling similar to
ServerService::list_servers: await the server_tag::Entity::find() call into a
Result, on Err call tracing::error! with context (e.g., "failed to query server
tags") and the error, then set tags_rows to an empty Vec; keep the subsequent
loop that builds tags_by_server (the HashMap<String, Vec<String>>) unchanged so
browser behavior is preserved but failures are logged.
apps/web/src/components/server/server-edit-dialog.tsx (1)

129-140: Minor: also reset tagsDirty when reverting input on save failure.

saveTags reverts tagsInput to initialTags.join(', ') on failure, but leaves tagsDirty = true. The next submit will therefore re-run the tags PUT with the (now canonical) initial list — a redundant, no-op round-trip. Setting tagsDirty = false after the revert keeps subsequent saves scoped to the main mutation only.

♻️ Proposed tweak
     } catch (err) {
       if (initialTags) {
         setTagsInput(initialTags.join(', '))
+        setTagsDirty(false)
       }
       toast.error(err instanceof Error ? err.message : t('tags_save_failed'))
       return false
     }
🤖 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 129 -
140, The saveTags handler reverts the input on error but doesn't reset the
tagsDirty flag, causing a redundant PUT on the next submit; in the saveTags
function (which calls tagsMutation.mutateAsync and setTagsInput with
initialTags.join(', ')), after reverting set tagsDirty to false (use the state
setter for tagsDirty) so the UI reflects the canonical state and prevents a
no-op tags update on subsequent saves.
apps/web/src/lib/traffic.ts (1)

17-23: LGTM — small, pure, well-tested.

One semantic note worth acknowledging in a comment but not blocking: when entry is undefined, used is the lifetime net_in_transfer + net_out_transfer, while the entry branch uses cycle-scoped bytes. Callers that render this as a "monthly quota" bar will visually saturate to 100% for long-running servers without a configured billing cycle — which matches the DEFAULT_TRAFFIC_LIMIT_BYTES fallback intent, and the tests pin this behavior down. Consider a brief comment on the fallback branch to make the semantic mismatch explicit for future readers.

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

In `@apps/web/src/lib/traffic.ts` around lines 17 - 23, Add a brief inline comment
inside computeTrafficQuota explaining that when entry is undefined the computed
used value (netInTransfer + netOutTransfer) is a lifetime total while when entry
is present used is cycle-scoped (entry.cycle_in + entry.cycle_out), and note
that this can cause long-running servers to saturate the quota bar against
DEFAULT_TRAFFIC_LIMIT_BYTES by design; update the comment near the used
computation and reference the entry, netInTransfer, netOutTransfer, and
DEFAULT_TRAFFIC_LIMIT_BYTES symbols so future readers understand the semantic
mismatch.
apps/web/src/routes/_authed/servers/index.tsx (1)

95-95: Optional: memoize a Map for traffic overview lookup instead of .find per row.

trafficOverview.find((e) => e.server_id === row.original.id) runs on every cell render (so O(N·M) per table render). A Map computed once alongside the data keeps the cell lookup O(1) and also makes the columns dependency more stable than passing the raw array.

♻️ Proposed refactor
-  const { data: trafficOverview = [] } = useTrafficOverview()
+  const { data: trafficOverview = [] } = useTrafficOverview()
+  const trafficByServer = useMemo(
+    () => new Map(trafficOverview.map((e) => [e.server_id, e])),
+    [trafficOverview]
+  )
@@
-        cell: ({ row }) => {
-          const entry = trafficOverview.find((e) => e.server_id === row.original.id)
-          return <NetworkCell entry={entry} server={row.original} />
-        },
+        cell: ({ row }) => <NetworkCell entry={trafficByServer.get(row.original.id)} server={row.original} />,
@@
-    [t, groupMap, groupOptions, statusOptions, trafficOverview]
+    [t, groupMap, groupOptions, statusOptions, trafficByServer]

Also applies to: 210-214, 266-266

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

In `@apps/web/src/routes/_authed/servers/index.tsx` at line 95, The table
currently calls trafficOverview.find(...) per-cell which is O(N·M); instead
compute a Map keyed by server_id from the useTrafficOverview() result and
memoize it (e.g., const trafficMap = useMemo(() => new Map(trafficOverview.map(e
=> [e.server_id, e])), [trafficOverview])) and then replace all per-row
trafficOverview.find(...) calls in your column cell renderers with
trafficMap.get(row.original.id) to make lookups O(1) and stabilize the columns'
dependencies; update every column/cell that references trafficOverview.find
(including the same uses in the columns definition) to use trafficMap.get
instead.
apps/web/src/hooks/use-server-tags.test.tsx (1)

34-51: Optional: assert request shape (URL/method/body) in the PUT test.

The current mock ignores _input and only round-trips the body through the response. A small assertion that the request targets PUT /api/servers/srv-1/tags with { tags: [...] } would catch regressions where a refactor accidentally changes the endpoint or payload wrapping, since the cache-patch assertions alone wouldn't notice that.

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

In `@apps/web/src/hooks/use-server-tags.test.tsx` around lines 34 - 51, In the
'PUTs tags and patches both caches on success' test for useUpdateServerTags,
enhance the fetch mock (vi.spyOn(globalThis, 'fetch').mockImplementationOnce) to
assert the request URL, method and body shape before returning the Response:
verify the request targets /api/servers/srv-1/tags, that init.method === 'PUT',
and that JSON.parse((init as RequestInit).body) yields an object { tags: [...] }
(matching the payload passed to mutateAsync); after those assertions return the
same 200 JSON response as before.
apps/web/src/routes/_authed/servers/index.cells.test.tsx (1)

108-113: Two renders in one test share the DOM.

Both render calls mount into document.body without unmounting the first, so screen.getByText sees both subtrees. It works here only because '100%' and '0%' each appear exactly once. Consider splitting into two it blocks or calling unmount()/cleanup() between renders for robustness.

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

In `@apps/web/src/routes/_authed/servers/index.cells.test.tsx` around lines 108 -
113, The test "clamps percentage to [0, 100]" calls render twice into the same
DOM so the second assertion can see both mounted trees; fix by either splitting
into two tests (e.g., two it blocks each rendering MetricBarRow with pct=150 and
pct=-5) or call react-testing-library cleanup()/unmount() between the two render
calls so each assertion only queries the intended render; update references to
MetricBarRow, render, screen.getByText accordingly.
crates/server/src/service/server_tag.rs (1)

60-67: Prefer insert_many over a per-tag insert loop.

Each iteration issues a separate round-trip inside the transaction. With up to MAX_TAGS entries this is minor, but Entity::insert_many collapses it to one statement and reads more idiomatically for SeaORM.

♻️ Proposed refactor
-    for tag in &normalized {
-        server_tag::ActiveModel {
-            server_id: Set(server_id.to_string()),
-            tag: Set(tag.clone()),
-        }
-        .insert(&txn)
-        .await?;
+    if !normalized.is_empty() {
+        let models = normalized.iter().map(|tag| server_tag::ActiveModel {
+            server_id: Set(server_id.to_string()),
+            tag: Set(tag.clone()),
+        });
+        server_tag::Entity::insert_many(models).exec(&txn).await?;
     }
Does sea-orm 1.0 Entity::insert_many accept an empty iterator without error?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/server/src/service/server_tag.rs` around lines 60 - 67, Replace the
per-tag insert loop that builds server_tag::ActiveModel for each tag and calls
.insert(&txn).await with a single server_tag::Entity::insert_many(...) call: map
normalized tags into a Vec<server_tag::ActiveModel> (setting server_id via
Set(server_id.to_string()) and tag via Set(tag.clone())), then call
insert_many(&txn).await once inside the transaction; also guard against an empty
collection by checking models.is_empty() before calling insert_many (or verify
sea-orm 1.0 accepts empty iterators and skip the guard if confirmed) to avoid a
no-op or runtime error.
🤖 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/routes/_authed/servers/index.cells.test.tsx`:
- Around line 11-17: The test's mocked Link component uses the type
React.ReactNode but React is not imported, causing typecheck failures under
strict mode; add a type-only import for React (e.g., import type React from
'react') at the top of the file so the React.ReactNode reference in the vi.mock
callback is resolved; update the file containing the vi.mock block and ensure
the import appears before the mocked Link definition.

In `@crates/server/src/service/server_tag.rs`:
- Around line 12-15: In validate_tags, the length check currently uses raw.len()
and should be performed after normalizing (trim whitespace), filtering out empty
strings, and deduplicating to match frontend behavior; so first map raw through
.trim().to_string(), filter out "".to_string(), deduplicate (e.g., using a
HashSet or unique retain to preserve order), then check that the resulting
normalized vector’s length <= MAX_TAGS and return that normalized Vec<String> or
AppError::Validation if too many. Ensure you update the code paths around
validate_tags, MAX_TAGS and the AppError::Validation message to reflect the
normalized count check.

---

Outside diff comments:
In `@apps/web/src/hooks/use-servers-ws.ts`:
- Around line 103-120: In mergeServerUpdate, the current suppression logic
treats empty arrays as a static-default and ignores explicit "clear" updates for
fields like tags/features; update the guard so we only suppress when the
incoming payload lacks the key (e.g., use a check for incoming key absence such
as server[key] === undefined or Object.prototype.hasOwnProperty.call(server,
key) rather than Array.isArray(value) && value.length === 0), keeping
STATIC_FIELDS logic but allowing explicit empty arrays to overwrite existing
values; refer to mergeServerUpdate and STATIC_FIELDS to locate the change.

---

Nitpick comments:
In `@apps/web/src/components/server/server-edit-dialog.tsx`:
- Around line 129-140: The saveTags handler reverts the input on error but
doesn't reset the tagsDirty flag, causing a redundant PUT on the next submit; in
the saveTags function (which calls tagsMutation.mutateAsync and setTagsInput
with initialTags.join(', ')), after reverting set tagsDirty to false (use the
state setter for tagsDirty) so the UI reflects the canonical state and prevents
a no-op tags update on subsequent saves.

In `@apps/web/src/components/server/tag-chip.tsx`:
- Around line 12-19: Remove the redundant Math.trunc call in the hashTag
function: Math.imul already produces a signed 32-bit integer and adding
tag.charCodeAt(i) preserves integer semantics, so delete the Math.trunc(h) step
inside hashTag (the function that computes an index into PALETTE) and return
Math.abs(h) % PALETTE.length as before.
- Around line 32-44: The tag chips use the tag string as React key (in the
tags.map callback using key={tag}), which will break if duplicate tags appear;
update the rendering to guarantee unique keys by using a stable index or
composite key (e.g., `${tag}-${i}`) or deduplicate tags before mapping (e.g.,
create a unique list from tags) so that the span elements rendered in the
tags.map loop have unique keys; reference the tags.map callback and key={tag} in
tag-chip.tsx (and consider server_tag uniqueness in backend but implement the
frontend defensive fix here).

In `@apps/web/src/hooks/use-server-tags.test.tsx`:
- Around line 34-51: In the 'PUTs tags and patches both caches on success' test
for useUpdateServerTags, enhance the fetch mock (vi.spyOn(globalThis,
'fetch').mockImplementationOnce) to assert the request URL, method and body
shape before returning the Response: verify the request targets
/api/servers/srv-1/tags, that init.method === 'PUT', and that JSON.parse((init
as RequestInit).body) yields an object { tags: [...] } (matching the payload
passed to mutateAsync); after those assertions return the same 200 JSON response
as before.

In `@apps/web/src/hooks/use-servers-ws.test.ts`:
- Around line 213-249: Replace the duplicate fixture builder by extending the
existing makeServer helper to include the extra fields present in baseServer
(tags, features, cpu_cores, disk_read_bytes_per_sec, disk_write_bytes_per_sec
and any other fields from ServerMetrics), change makeServer’s signature to
accept Partial<ServerMetrics> overrides like baseServer, update all tests that
call baseServer to call makeServer instead, and then remove the baseServer
function to avoid drift between the two builders.

In `@apps/web/src/lib/traffic.ts`:
- Around line 17-23: Add a brief inline comment inside computeTrafficQuota
explaining that when entry is undefined the computed used value (netInTransfer +
netOutTransfer) is a lifetime total while when entry is present used is
cycle-scoped (entry.cycle_in + entry.cycle_out), and note that this can cause
long-running servers to saturate the quota bar against
DEFAULT_TRAFFIC_LIMIT_BYTES by design; update the comment near the used
computation and reference the entry, netInTransfer, netOutTransfer, and
DEFAULT_TRAFFIC_LIMIT_BYTES symbols so future readers understand the semantic
mismatch.

In `@apps/web/src/routes/_authed/servers/index.cells.test.tsx`:
- Around line 108-113: The test "clamps percentage to [0, 100]" calls render
twice into the same DOM so the second assertion can see both mounted trees; fix
by either splitting into two tests (e.g., two it blocks each rendering
MetricBarRow with pct=150 and pct=-5) or call react-testing-library
cleanup()/unmount() between the two render calls so each assertion only queries
the intended render; update references to MetricBarRow, render, screen.getByText
accordingly.

In `@apps/web/src/routes/_authed/servers/index.tsx`:
- Line 95: The table currently calls trafficOverview.find(...) per-cell which is
O(N·M); instead compute a Map keyed by server_id from the useTrafficOverview()
result and memoize it (e.g., const trafficMap = useMemo(() => new
Map(trafficOverview.map(e => [e.server_id, e])), [trafficOverview])) and then
replace all per-row trafficOverview.find(...) calls in your column cell
renderers with trafficMap.get(row.original.id) to make lookups O(1) and
stabilize the columns' dependencies; update every column/cell that references
trafficOverview.find (including the same uses in the columns definition) to use
trafficMap.get instead.

In `@crates/server/src/router/ws/browser.rs`:
- Around line 266-275: The tags query currently swallows errors via
unwrap_or_default() causing silent empty tag lists; replace that with explicit
error handling similar to ServerService::list_servers: await the
server_tag::Entity::find() call into a Result, on Err call tracing::error! with
context (e.g., "failed to query server tags") and the error, then set tags_rows
to an empty Vec; keep the subsequent loop that builds tags_by_server (the
HashMap<String, Vec<String>>) unchanged so browser behavior is preserved but
failures are logged.

In `@crates/server/src/service/server_tag.rs`:
- Around line 60-67: Replace the per-tag insert loop that builds
server_tag::ActiveModel for each tag and calls .insert(&txn).await with a single
server_tag::Entity::insert_many(...) call: map normalized tags into a
Vec<server_tag::ActiveModel> (setting server_id via Set(server_id.to_string())
and tag via Set(tag.clone())), then call insert_many(&txn).await once inside the
transaction; also guard against an empty collection by checking
models.is_empty() before calling insert_many (or verify sea-orm 1.0 accepts
empty iterators and skip the guard if confirmed) to avoid a no-op or runtime
error.

In `@docs/superpowers/plans/2026-04-16-servers-table-density.md`:
- Around line 1-784: This plan file is historical and should be marked as
superseded by the newer 2026-04-17 servers-table-density plan: add a short
one-paragraph banner immediately under the "# Servers Table Density & Disk I/O
Display Implementation Plan" heading that states this document is kept for
historical reference and has been superseded by the 2026-04-17 plan (include
that date/string so readers can find the newer doc), optionally add a
"Superseded" tag or front-matter line and update the top-level summary to point
readers to the newer plan, then commit with a descriptive message like "docs:
mark 2026-04-16 servers table density plan as superseded by 2026-04-17".

In `@docs/superpowers/plans/2026-04-17-servers-table-row-visual-redesign.md`:
- Around line 1-2644: The empty-test stub crates/server/tests/server_tags.rs
(containing only a comment) produces a linked test binary and should be removed;
delete that file (or convert it to a non-test helper module) so only the real
tags tests live in crates/server/tests/integration.rs, then git rm the stub and
commit (e.g. "test(server): remove empty server_tags.rs stub — tests live in
integration.rs"). Ensure integration.rs contains the full tags tests and re-run
cargo test to verify no extra empty test binary remains.
🪄 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: fb36901e-ee03-4e52-abb6-9363e7ccfae2

📥 Commits

Reviewing files that changed from the base of the PR and between db514df and 9c84b79.

📒 Files selected for processing (31)
  • apps/web/src/components/dashboard/dashboard-switcher.tsx
  • apps/web/src/components/server/server-card.tsx
  • apps/web/src/components/server/server-edit-dialog.tsx
  • apps/web/src/components/server/status-dot.test.tsx
  • apps/web/src/components/server/status-dot.tsx
  • apps/web/src/components/server/tag-chip.test.tsx
  • apps/web/src/components/server/tag-chip.tsx
  • apps/web/src/hooks/use-server-tags.test.tsx
  • apps/web/src/hooks/use-server-tags.ts
  • apps/web/src/hooks/use-servers-ws.test.ts
  • apps/web/src/hooks/use-servers-ws.ts
  • apps/web/src/lib/traffic.test.ts
  • apps/web/src/lib/traffic.ts
  • apps/web/src/locales/en/servers.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
  • crates/common/src/types.rs
  • crates/server/src/router/api/mod.rs
  • crates/server/src/router/api/server_tag.rs
  • crates/server/src/router/ws/browser.rs
  • crates/server/src/service/agent_manager.rs
  • crates/server/src/service/mod.rs
  • crates/server/src/service/server_tag.rs
  • crates/server/tests/integration.rs
  • docs/superpowers/plans/2026-04-16-servers-table-density.md
  • docs/superpowers/plans/2026-04-17-servers-table-row-visual-redesign.md
  • docs/superpowers/specs/2026-04-15-servers-table-density-design.md
  • docs/superpowers/specs/2026-04-17-servers-table-row-visual-redesign-design.md
  • tests/servers/table-row-visual-redesign.md

Comment on lines +11 to +17
vi.mock('@tanstack/react-router', () => ({
Link: ({ children, ...props }: { children?: React.ReactNode; [k: string]: unknown }) => (
<a data-testid="server-link" href={`/servers/${props.params && (props.params as { id: string }).id}`}>
{children}
</a>
)
}))
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
# Confirm React is not imported anywhere in the test file and verify tsconfig
rg -n "from 'react'" apps/web/src/routes/_authed/servers/index.cells.test.tsx || echo "no react import"
fd -t f tsconfig.json apps/web

Repository: ZingerLittleBee/ServerBee

Length of output: 107


🏁 Script executed:

cat -n apps/web/src/routes/_authed/servers/index.cells.test.tsx | head -30

Repository: ZingerLittleBee/ServerBee

Length of output: 1452


🏁 Script executed:

cat apps/web/tsconfig.json

Repository: ZingerLittleBee/ServerBee

Length of output: 281


🏁 Script executed:

rg -A 5 "React\.ReactNode" apps/web/src/routes/_authed/servers/index.cells.test.tsx

Repository: ZingerLittleBee/ServerBee

Length of output: 301


🏁 Script executed:

cat apps/web/tsconfig.app.json

Repository: ZingerLittleBee/ServerBee

Length of output: 898


Add missing React type import for React.ReactNode reference.

Line 12 uses React.ReactNode in the type annotation, but React is never imported. Although jsx: "react-jsx" in tsconfig auto-imports React for JSX syntax, it does not auto-import for type references. With strict: true enabled, this causes a typecheck failure.

🔧 Proposed fix
 import { render, screen } from '@testing-library/react'
+import type { ReactNode } from 'react'
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
@@
 vi.mock('@tanstack/react-router', () => ({
-  Link: ({ children, ...props }: { children?: React.ReactNode; [k: string]: unknown }) => (
+  Link: ({ children, ...props }: { children?: ReactNode; [k: string]: unknown }) => (
📝 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
vi.mock('@tanstack/react-router', () => ({
Link: ({ children, ...props }: { children?: React.ReactNode; [k: string]: unknown }) => (
<a data-testid="server-link" href={`/servers/${props.params && (props.params as { id: string }).id}`}>
{children}
</a>
)
}))
import { render, screen } from '@testing-library/react'
import type { ReactNode } from 'react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('@tanstack/react-router', () => ({
Link: ({ children, ...props }: { children?: ReactNode; [k: string]: unknown }) => (
<a data-testid="server-link" href={`/servers/${props.params && (props.params as { id: string }).id}`}>
{children}
</a>
)
}))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/routes/_authed/servers/index.cells.test.tsx` around lines 11 -
17, The test's mocked Link component uses the type React.ReactNode but React is
not imported, causing typecheck failures under strict mode; add a type-only
import for React (e.g., import type React from 'react') at the top of the file
so the React.ReactNode reference in the vi.mock callback is resolved; update the
file containing the vi.mock block and ensure the import appears before the
mocked Link definition.

Comment on lines +12 to +15
pub fn validate_tags(raw: &[String]) -> Result<Vec<String>, AppError> {
if raw.len() > MAX_TAGS {
return Err(AppError::Validation(format!("at most {MAX_TAGS} tags")));
}
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

Consider trimming/filtering before the count check.

raw.len() > MAX_TAGS rejects inputs that would normalize to ≤ 8 after trimming empties and dedup (e.g., 9 entries where 2 are duplicates or whitespace-only). The frontend's parseTagsInput applies the count check after dedup, so the client and server disagree on what constitutes a "too many" error for edge inputs. Minor UX inconsistency.

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

In `@crates/server/src/service/server_tag.rs` around lines 12 - 15, In
validate_tags, the length check currently uses raw.len() and should be performed
after normalizing (trim whitespace), filtering out empty strings, and
deduplicating to match frontend behavior; so first map raw through
.trim().to_string(), filter out "".to_string(), deduplicate (e.g., using a
HashSet or unique retain to preserve order), then check that the resulting
normalized vector’s length <= MAX_TAGS and return that normalized Vec<String> or
AppError::Validation if too many. Ensure you update the code paths around
validate_tags, MAX_TAGS and the AppError::Validation message to reflect the
normalized count check.

# Conflicts:
#	apps/web/src/hooks/use-servers-ws.test.ts
#	crates/server/src/router/api/mod.rs
#	crates/server/src/router/ws/browser.rs
#	crates/server/tests/integration.rs
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.

🧹 Nitpick comments (5)
apps/web/src/hooks/use-servers-ws.test.ts (1)

216-284: LGTM — static-fields guard coverage is solid.

The new baseServer helper and mergeServerUpdate static-fields guard suite correctly exercise each branch of the STATIC_FIELDS guard in use-servers-ws.ts (empty-array for tags/features, null for cpu_cores, and the non-empty overwrite path). Using a second helper with zero/null defaults makes the static-default semantics explicit and avoids accidental interference from makeServer's populated values.

One nit you can take or leave: since baseServer in the incoming frames also carries tags: [] and features: [] by default, the guard happens to also shield those from clobbering the prev values in the cpu_cores test — which is the desired behavior, but it means that test only isolates cpu_cores because of the guard itself. If you ever loosen the guard, this test would still pass on cpu_cores alone; consider asserting result[0].tags and result[0].features alongside cpu_cores to lock in the full contract.

♻️ Optional tightening for the cpu_cores test
   it('preserves prior cpu_cores when incoming frame carries cpu_cores: null', () => {
-    const prev = [baseServer({ cpu_cores: 8 })]
-    const incoming = [baseServer({ cpu_cores: null, cpu: 5 })]
+    const prev = [baseServer({ cpu_cores: 8, tags: ['prod'], features: ['docker'] })]
+    const incoming = [baseServer({ cpu_cores: null, cpu: 5 })]
     const result = mergeServerUpdate(prev, incoming)
     expect(result[0].cpu_cores).toBe(8)
+    expect(result[0].tags).toEqual(['prod'])
+    expect(result[0].features).toEqual(['docker'])
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/hooks/use-servers-ws.test.ts` around lines 216 - 284, The
cpu_cores test can pass unintentionally because baseServer for incoming frames
defaults tags: [] and features: [], so the STATIC_FIELDS guard also protects
those fields; update the 'preserves prior cpu_cores when incoming frame carries
cpu_cores: null' test in the mergeServerUpdate suite to also assert that tags
and features were preserved (e.g., expect(result[0].tags) and
expect(result[0].features)) alongside the cpu_cores expectation so the test
locks in the full static-fields contract; locate the test by the describe name
'mergeServerUpdate static-fields guard' and the it block name, and modify only
that test assertions.
crates/server/tests/integration.rs (3)

3882-3896: Edge case: validation error status assumption.

The comment correctly notes AppError::Validation → 422. Good. One minor gap: none of the new tests assert that the 8-tag boundary is accepted (only that 9 is rejected). Consider adding a positive test with exactly MAX_TAGS entries to lock in the inclusive/exclusive boundary, which is a common regression point.

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

In `@crates/server/tests/integration.rs` around lines 3882 - 3896, Add a positive
boundary test to ensure MAX_TAGS (8) is accepted: create a new test (or extend
admin_put_rejects_too_many_tags) that builds a Vec<String> of length MAX_TAGS
(use the constant or hardcode 8 if no constant exported), sends the same PUT to
/api/servers/{server_id}/tags and asserts the response is success (e.g., 200 or
204) instead of 422; reference the existing test function
admin_put_rejects_too_many_tags and the same
register_agent/login_admin/http_client flow to reuse setup so the
inclusive/exclusive boundary is locked in.

3987-4014: Cookie parsing will break if Set-Cookie ever emits attributes in a different order.

set_cookie.split(';').next() assumes the session_token=... pair is the first segment, which is true today but is an implicit coupling to response header formatting. Since you already have http_client() with cookie_store(true), a cleaner approach is to log in via that client and then read the stored cookie via reqwest::cookie::Jar or attach the cookie jar to the WS request. Minor — the current form is acceptable for a test.

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

In `@crates/server/tests/integration.rs` around lines 3987 - 4014, Replace brittle
manual parsing of Set-Cookie by using a reqwest cookie jar and the cookie-aware
client: create a reqwest::cookie::Jar
(Arc::new(reqwest::cookie::Jar::default())), build the client with
ClientBuilder::cookie_provider(...) (or cookie_store(true) with an accessible
jar), perform the login request using that client (replace
raw_client/login_resp), then read the stored cookie from the jar via
jar.cookies(&base_url.parse().unwrap()) (or jar.cookies(&login_url)) and use
that value when calling into_client_request()/request.headers_mut().insert(...);
update references to raw_client, login_resp, set_cookie and cookie_value
accordingly.

3788-3818: register_member helper duplicates the admin+member creation pattern already open-coded in several tests.

Tests like test_member_read_only, test_file_write_requires_admin, test_dashboard_rbac_member_cannot_write, test_recovery_* all repeat this pattern. Since you've introduced a helper, a follow-up refactor to replace those open-coded sequences with register_member would reduce ~300 lines of duplication. Not blocking for this PR.

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

In `@crates/server/tests/integration.rs` around lines 3788 - 3818, The tests
duplicate the admin+member creation/login sequence that the async helper
register_member already encapsulates; replace the repeated open-coded sequences
in tests such as test_member_read_only, test_file_write_requires_admin,
test_dashboard_rbac_member_cannot_write, and the test_recovery_* cases with
calls to register_member(base_url) to obtain an authenticated reqwest::Client
for the member, removing the duplicated POST /api/users and POST /api/auth/login
blocks and adjusting assertions to use the helper's result.
crates/server/src/router/ws/browser.rs (1)

279-288: Tag query is unbounded — fetches every row in server_tag on each FullSync / resync.

server_tag::Entity::find() without a filter pulls the entire table. FullSync runs on every browser WS connect and on broadcast lag (line 152), so with many servers × tags this scales with total rows regardless of how many servers are visible. Given the per-server limits (≤8 tags, ≤16 chars), the absolute ceiling is small, but it's still worth scoping the query to just the server IDs being rendered.

♻️ Suggested scoping
-    let tags_rows = server_tag::Entity::find()
-        .order_by_asc(server_tag::Column::ServerId)
-        .order_by_asc(server_tag::Column::Tag)
-        .all(&state.db)
-        .await
-        .unwrap_or_default();
+    let server_ids: Vec<String> = servers.iter().map(|s| s.id.clone()).collect();
+    let tags_rows = server_tag::Entity::find()
+        .filter(server_tag::Column::ServerId.is_in(server_ids))
+        .order_by_asc(server_tag::Column::ServerId)
+        .order_by_asc(server_tag::Column::Tag)
+        .all(&state.db)
+        .await
+        .unwrap_or_default();

Also consider swallowing the error silently (unwrap_or_default) vs. logging it like the list_servers branch above — a persistent DB error here would result in "no tags ever" with no visibility.

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

In `@crates/server/src/router/ws/browser.rs` around lines 279 - 288, The tag query
currently uses server_tag::Entity::find() which fetches the entire table on
every FullSync/resync; change the query to filter by only the server IDs being
rendered (e.g. apply a .filter(server_tag::Column::ServerId.is_in(server_ids))
where server_ids are collected from the servers list used in the FullSync) so
tags_rows only contains relevant rows, populate tags_by_server from that result,
and replace unwrap_or_default() with proper error handling/logging consistent
with the list_servers branch so DB errors are logged instead of silently
swallowed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/web/src/hooks/use-servers-ws.test.ts`:
- Around line 216-284: The cpu_cores test can pass unintentionally because
baseServer for incoming frames defaults tags: [] and features: [], so the
STATIC_FIELDS guard also protects those fields; update the 'preserves prior
cpu_cores when incoming frame carries cpu_cores: null' test in the
mergeServerUpdate suite to also assert that tags and features were preserved
(e.g., expect(result[0].tags) and expect(result[0].features)) alongside the
cpu_cores expectation so the test locks in the full static-fields contract;
locate the test by the describe name 'mergeServerUpdate static-fields guard' and
the it block name, and modify only that test assertions.

In `@crates/server/src/router/ws/browser.rs`:
- Around line 279-288: The tag query currently uses server_tag::Entity::find()
which fetches the entire table on every FullSync/resync; change the query to
filter by only the server IDs being rendered (e.g. apply a
.filter(server_tag::Column::ServerId.is_in(server_ids)) where server_ids are
collected from the servers list used in the FullSync) so tags_rows only contains
relevant rows, populate tags_by_server from that result, and replace
unwrap_or_default() with proper error handling/logging consistent with the
list_servers branch so DB errors are logged instead of silently swallowed.

In `@crates/server/tests/integration.rs`:
- Around line 3882-3896: Add a positive boundary test to ensure MAX_TAGS (8) is
accepted: create a new test (or extend admin_put_rejects_too_many_tags) that
builds a Vec<String> of length MAX_TAGS (use the constant or hardcode 8 if no
constant exported), sends the same PUT to /api/servers/{server_id}/tags and
asserts the response is success (e.g., 200 or 204) instead of 422; reference the
existing test function admin_put_rejects_too_many_tags and the same
register_agent/login_admin/http_client flow to reuse setup so the
inclusive/exclusive boundary is locked in.
- Around line 3987-4014: Replace brittle manual parsing of Set-Cookie by using a
reqwest cookie jar and the cookie-aware client: create a reqwest::cookie::Jar
(Arc::new(reqwest::cookie::Jar::default())), build the client with
ClientBuilder::cookie_provider(...) (or cookie_store(true) with an accessible
jar), perform the login request using that client (replace
raw_client/login_resp), then read the stored cookie from the jar via
jar.cookies(&base_url.parse().unwrap()) (or jar.cookies(&login_url)) and use
that value when calling into_client_request()/request.headers_mut().insert(...);
update references to raw_client, login_resp, set_cookie and cookie_value
accordingly.
- Around line 3788-3818: The tests duplicate the admin+member creation/login
sequence that the async helper register_member already encapsulates; replace the
repeated open-coded sequences in tests such as test_member_read_only,
test_file_write_requires_admin, test_dashboard_rbac_member_cannot_write, and the
test_recovery_* cases with calls to register_member(base_url) to obtain an
authenticated reqwest::Client for the member, removing the duplicated POST
/api/users and POST /api/auth/login blocks and adjusting assertions to use the
helper's result.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cc52a25f-3f90-4cb2-8c28-d43d7391903d

📥 Commits

Reviewing files that changed from the base of the PR and between 9c84b79 and effa530.

📒 Files selected for processing (12)
  • apps/web/src/components/dashboard/widgets/stat-number.test.tsx
  • apps/web/src/components/server/server-card.test.tsx
  • apps/web/src/hooks/use-realtime-metrics.test.tsx
  • apps/web/src/hooks/use-servers-ws.test.ts
  • apps/web/src/hooks/use-servers-ws.ts
  • apps/web/src/locales/en/servers.json
  • apps/web/src/locales/zh/servers.json
  • crates/server/src/router/api/mod.rs
  • crates/server/src/router/ws/browser.rs
  • crates/server/src/service/agent_manager.rs
  • crates/server/src/service/mod.rs
  • crates/server/tests/integration.rs
✅ Files skipped from review due to trivial changes (6)
  • crates/server/src/service/mod.rs
  • apps/web/src/components/dashboard/widgets/stat-number.test.tsx
  • apps/web/src/components/server/server-card.test.tsx
  • apps/web/src/locales/en/servers.json
  • apps/web/src/hooks/use-realtime-metrics.test.tsx
  • apps/web/src/locales/zh/servers.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/server/src/router/api/mod.rs
  • apps/web/src/hooks/use-servers-ws.ts

@ZingerLittleBee ZingerLittleBee merged commit 8099fff into main Apr 17, 2026
10 checks passed
@ZingerLittleBee ZingerLittleBee deleted the little-rock branch April 17, 2026 11:24
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