Skip to content

feat(seedstats): add seed shoot capacity and health stats with live updates#2901

Open
petersutter wants to merge 9 commits into
masterfrom
enh/seed-stats-v2
Open

feat(seedstats): add seed shoot capacity and health stats with live updates#2901
petersutter wants to merge 9 commits into
masterfrom
enh/seed-stats-v2

Conversation

@petersutter
Copy link
Copy Markdown
Member

@petersutter petersutter commented Apr 16, 2026

What this PR does / why we need it:

Adds per-seed shoot health statistics and capacity indicators to the seed list and seed detail views, with real-time updates via Socket.IO.

Backend

  • Seedstats service: computes per-seed shootCount and unhealthyShoots: { total, matching }. total counts all unhealthy shoots; matching counts only those not excluded by the active filter mask.
  • Filter mask: a bitmask unhealthyFilterMask parameter controls which unhealthy shoots are excluded from matching:
    Bit Filter
    0 Hide progressing
    1 Hide no-operator-action-required
    2 Hide clusters with configured ticket labels
  • REST: GET /api/seedstats[/:name]?unhealthyFilterMask=<mask>.
  • Socket.IO: explicit subscribe/unsubscribe with filter-specific rooms (seedstats;uf=<mask>, seedstats;seed=<name>;uf=<mask>). Server emits pre-computed counts per room. Only rooms with members trigger recomputation.
  • Cache indexing: projects indexed by namespace, shoots indexed by seed name for efficient per-seed iteration.

Frontend

  • seedStat store: on-demand lifecycle: subscribes on seed list/detail mount, re-subscribes on filter change, unsubscribes when stats columns are hidden or on unmount.
  • GShootHealthDonut: SVG ring chart showing matching (red) / excluded (grey) / healthy (background) shoot distribution per seed.
  • GSeedCapacityIndicator: shows assigned vs allocatable shoot count with progress bar. Reads allocatableShoots from the seed spec.
  • useShootListFilters: shared composable for all-project filter state, reused between shoot list and seed stats.

Which issue(s) this PR fixes:
Fixes #

Special notes for your reviewer:

Release note:

The seed list now displays per-seed shoot health statistics as donut charts and shoot capacity indicators, with real-time updates. Health stats respect the active shoot list filters (hide progressing, hide no-operator-action, hide ticket-labeled).

Summary by CodeRabbit

  • New Features

    • Seed statistics API with real‑time subscribe/synchronize and a new "seedstats" socket channel; UI components for shoot health (donut) and seed capacity, plus client store/composables for live seed stats.
  • Improvements

    • New cache indices for faster project/seed/shoot lookups; event publishing now emits seedstats updates; enhanced socket room handling, authorization checks, and filter-driven unhealthy masking.
  • Tests

    • Extensive new unit and acceptance tests covering seedstats, realtime flows, cache, watches, and UI.
  • Chores

    • Updated .gitignore to ignore local Claude settings.

@petersutter petersutter added the kind/enhancement Enhancement, improvement, extension label Apr 16, 2026
@gardener-prow
Copy link
Copy Markdown

gardener-prow Bot commented Apr 16, 2026

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign grolu for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@gardener-prow gardener-prow Bot added size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. cla: yes Indicates the PR's author has signed the cla-assistant.io CLA. labels Apr 16, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a SeedStat feature: backend service, routes, cache indices, Socket.IO seedstats rooms and emissions; frontend API, store, composables, components, views, and many tests; also ticket cache indexing, startup hooks, and minor infra/.gitignore updates.

Changes

Cohort / File(s) Summary
Backend: seedstats service & routes
backend/lib/services/seedstats.js, backend/lib/routes/seedstats.js, backend/lib/services/index.js, backend/lib/routes/index.js
New SeedStat service (list/read/getByUids, mask parsing/validation, access checks) and Express routes; service wired into services export and routes map.
Backend: cache & tickets indices
backend/lib/cache/index.js, backend/lib/cache/tickets.js
Added namespace→project and seedName→shoot indices plus getShootsBySeedName and index* APIs; ticket cache gains issuesByShoot index and getIssuesForShoot.
Backend: IO, seedstats socket & helper updates
backend/lib/io/seedstats.js, backend/lib/io/dispatcher.js, backend/lib/io/index.js, backend/lib/io/helper.js
New seedstats Socket.IO module (room parsing, subscribe/unsubscribe/synchronize), dispatcher handling, async join changes, and userProfiles adds canListShoots.
Backend: watches & publishing
backend/lib/watches/shoots.js, backend/lib/watches/seeds.js, backend/lib/watches/leases.js
Watches and ticket handlers emit seedstats events to matching joined rooms; gating logic added to control emissions based on unhealthy-mask and seed resolution.
Backend: startup hooks & tests
backend/lib/hooks.js, backend/__tests__/helpers/cache.js, backend/__tests__/...
Cache initializes new indices on startup; added test helper to seed project-namespace index; many tests added/updated to cover seedstats, cache indices, and adjusted auth mock counts.
Backend: cache API surface
backend/lib/cache/index.js
Exports added: indexProjectsByNamespace, indexShootsBySeedName, getShootsBySeedName; findProjectByNamespace now uses the new index.
Frontend: seedStat store & socket integration
frontend/src/store/seedStat.js, frontend/src/store/socket/index.js, frontend/src/store/socket/helper.js
New Pinia seedStat store with fetch/subscribe/synchronize lifecycle; socket wiring for seedstats events and synchronize options; socket helper integrates seedstat synchronization.
Frontend: composables, API & sorting
frontend/src/composables/useShootListFilters.js, frontend/src/composables/useDonutChart.js, frontend/src/composables/useSeedItem/index.js, frontend/src/composables/useSocketEventHandler.js, frontend/src/composables/useSeedTableSorting.js, frontend/src/composables/useApi/api.js
Added shoot-list filters composable (bitmask), donut chart composable, useSeedStats, socket handler getSynchronizeOptions, API helpers getSeedStats/getSeedStat, and seed-table sort keys.
Frontend: components & views
frontend/src/components/GShootHealthDonut.vue, frontend/src/components/Seeds/GSeedCapacityIndicator.vue, frontend/src/components/GSeedListRow.vue, frontend/src/components/SeedDetails/GSeedInfrastructureCard.vue, frontend/src/views/GSeedList.vue, frontend/src/views/GShootList.vue
New health donut and capacity indicator components; seed list row and seed-details card show capacity and shoot health; views subscribe/unsubscribe to seedStat store based on selected columns and filters.
Frontend: store & helper changes
frontend/src/store/shoot/shoot.js, frontend/src/store/helper.js, frontend/src/store/localStorage.js
Shoot store now uses external composable for filters and new synchronize lock factory added; removed deprecated localStorage flag.
Frontend: misc utils & small edits
frontend/src/utils/index.js, frontend/src/utils/errorCodes.js, frontend/src/composables/useSeedItem/index.js
Added isTruthyValue, refactored implementation, renamed errorCodesFromArray parameter, and added useSeedStats helper integration.
Tests: many new/updated tests
backend/__tests__/*, frontend/__tests__/*, backend/__tests__/helpers/cache.js
Extensive unit and acceptance tests for seedstats, cache indices, IO, watches, composables, components, and stores; test helpers to seed indices and adjusted mock counts.
Misc / Config
.gitignore
Ignore .claude/settings.local.json.

Sequence Diagram(s)

sequenceDiagram
    participant Browser
    participant API
    participant SeedService
    participant Cache
    participant TicketsCache

    Browser->>API: GET /api/seedstats?unhealthyFilterMask=...
    API->>SeedService: list({ user, unhealthyFilterMask })
    SeedService->>Cache: getSeeds(), getShootsBySeedName(seedName)
    Cache-->>SeedService: seed & shoot data (indexed)
    SeedService->>TicketsCache: getIssuesForShoot(projectName,name) [as needed]
    TicketsCache-->>SeedService: issue labels
    SeedService-->>API: SeedStat objects
    API-->>Browser: 200 + SeedStat payloads
Loading
sequenceDiagram
    participant Watcher
    participant Cache
    participant SocketNsp as Socket.IO_Nsp
    participant Rooms
    participant Subscribers

    Watcher->>Cache: resolve seedName -> seedUid
    Cache-->>Watcher: seedUid
    Watcher->>SocketNsp: getJoinedRooms(seedName)
    SocketNsp-->>Watcher: matching seedstats rooms
    Watcher->>Rooms: emit 'seedstats' { type:'MODIFIED', uid }
    Rooms-->>Subscribers: receive seedstats update
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

approved, lgtm

Suggested reviewers

  • holgerkoser
  • klocke-io
  • grolu

Poem

🐇 I hopped through cache and rooms with cheer,
Counting shoots and masks both far and near,
Donuts spun and capacities shone bright,
Subscribed and synced through day and night,
Dashboard blooms — a rabbit's delight.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.25% 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
Title check ✅ Passed The PR title 'feat(seedstats): add seed shoot capacity and health stats with live updates' accurately describes the main feature addition—seed statistics with capacity and health indicators.
Description check ✅ Passed The PR description covers all required template sections: 'What this PR does', issue fixes, special notes, and a detailed release note explaining the user-facing feature and implementation.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch enh/seed-stats-v2

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

🧹 Nitpick comments (6)
backend/lib/io/shoots.js (1)

39-42: Log message may be misleading when joining multiple rooms.

The joinRoom function can be called with an array of rooms (line 63), but the log message uses singular "room". Consider adjusting the message or logging each room individually for clarity.

💡 Suggested fix
   const joinRoom = room => {
-    logger.debug('User %s joined room [%s]', user.id, room)
+    logger.debug('User %s joined room(s) [%s]', user.id, room)
     return socket.join(room)
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/lib/io/shoots.js` around lines 39 - 42, joinRoom's log uses singular
"room" but joinRoom(room) can receive an array; update joinRoom to detect
Array.isArray(room) and either log each room (e.g., iterate and call
logger.debug per room with user.id) or log a pluralized message with the rooms
serialized, then call socket.join(room) as before; change references in the
joinRoom function (logger.debug, socket.join, and user.id) so the log accurately
reflects single or multiple rooms.
backend/lib/cache/index.js (1)

97-99: Consider returning a consistent type from getShootsBySeedName.

The method returns a MapIterator when shoots exist but an empty array [] when none exist. While both are iterable and the current code handles this (via for...of and explicit Array.from() conversions), the inconsistent return type could cause confusion for future maintainers or callers.

💡 Suggested fix for consistent return type
   getShootsBySeedName (seedName) {
-    return this.#seedNameToShoots.get(seedName)?.values() ?? []
+    const shoots = this.#seedNameToShoots.get(seedName)
+    return shoots ? [...shoots.values()] : []
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/lib/cache/index.js` around lines 97 - 99, getShootsBySeedName
currently returns a MapIterator when entries exist and an array when none exist;
change it to always return the same type (an array) by converting the
MapIterator from this.#seedNameToShoots.get(seedName)?.values() to an array
(e.g. via Array.from or spread) and return an empty array when there is no entry
so callers of getShootsBySeedName always receive an Array of shoots.
backend/lib/watches/seeds.js (1)

25-25: Drop the unused oldObject argument in the update subscription.

handleEvent now accepts only (type, newObject), so the extra parameter in the update listener can be removed for clarity.

✂️ Small cleanup
-informer.on('update', (object, oldObject) => handleEvent('MODIFIED', object, oldObject))
+informer.on('update', object => handleEvent('MODIFIED', object))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/lib/watches/seeds.js` at line 25, The update listener is passing an
unused oldObject to handleEvent; update the informer.on('update', ...)
subscription so it calls handleEvent('MODIFIED', object) with only the new
object, removing the extra oldObject parameter; locate the informer.on('update',
(object, oldObject) => handleEvent('MODIFIED', object, oldObject)) line and
change the callback to call handleEvent with only the type and newObject to
match handleEvent(type, newObject).
frontend/src/store/seedStat.js (1)

82-98: Consider error handling in subscribe when closing the previous subscription.

If closeSubscription() throws (e.g., socket timeout), the function will propagate the error and the new subscription won't be established. Depending on desired behavior, you may want to catch and log this error, then proceed with the new subscription.

🛡️ Optional: graceful handling of close errors
   async function subscribe (options = {}) {
     const sameSubscription = isEqual(subscription.value, options)
     if (sameSubscription && list.value !== null) {
       if (!subscribed.value && socketStore.connected) {
         await openSubscription(options)
       }
       return
     }

     if (subscribed.value || subscription.value) {
-      await closeSubscription()
+      try {
+        await closeSubscription()
+      } catch (err) {
+        logger.debug('Failed to close previous subscription: %s', err.message)
+      }
     }

     subscription.value = options
     await fetchSeedStats(options)
     await openSubscription(options)
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/store/seedStat.js` around lines 82 - 98, The subscribe function
currently propagates errors from closeSubscription and can block establishing a
new subscription; wrap the await closeSubscription() call in a try/catch inside
subscribe (referencing subscribe, closeSubscription, openSubscription,
fetchSeedStats, subscription.value and subscribed.value), log or report the
caught error via your logger/console, and then continue to set
subscription.value, call fetchSeedStats(options) and openSubscription(options)
so a failing close does not prevent the new subscription from being created.
frontend/src/components/GShootHealthDonut.vue (1)

167-168: Consider clamping computed values to prevent negative segment values.

If upstream data is inconsistent (e.g., totalUnhealthyShoots > shootCount), healthyShoots could become negative, potentially causing unexpected SVG rendering behavior.

🛡️ Defensive clamping
-const hiddenUnhealthy = computed(() => totalUnhealthy.value - matchingUnhealthy.value)
-const healthyShoots = computed(() => shootCount.value - totalUnhealthy.value)
+const hiddenUnhealthy = computed(() => Math.max(0, totalUnhealthy.value - matchingUnhealthy.value))
+const healthyShoots = computed(() => Math.max(0, shootCount.value - totalUnhealthy.value))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/GShootHealthDonut.vue` around lines 167 - 168, Clamp
computed segment values to a non-negative range to avoid negative SVG segments:
in GShootHealthDonut.vue update the computed properties hiddenUnhealthy and
healthyShoots to ensure they never go below zero (use shootCount,
totalUnhealthy, matchingUnhealthy as inputs), e.g., compute hiddenUnhealthy as
max(0, totalUnhealthy - matchingUnhealthy) and healthyShoots as max(0,
shootCount - totalUnhealthy) so rendering always receives non-negative segment
sizes.
backend/__tests__/acceptance/api.seedstats.spec.js (1)

27-38: Cache index cleanup is optional and may not be necessary.

The seedShootsBySeedNameIndex function populates the cache but has no explicit cleanup. However, this same pattern exists in other acceptance tests (e.g., io.spec.js) without reported issues, and no reset function has been created for this index elsewhere in the codebase. If test isolation becomes a concern, consider adding cleanup similar to cache.cache.resetTicketCache() called in other test files, but it is not required.

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

In `@backend/__tests__/acceptance/api.seedstats.spec.js` around lines 27 - 38, The
test helper seedShootsBySeedNameIndex populates the cache via
cache.indexShootsBySeedName but does not clean up the index afterward; to
preserve test isolation add an explicit reset at the end of the helper (or
provide a paired teardown) that calls the cache reset method used elsewhere
(e.g., cache.cache.resetTicketCache or an equivalent reset function) so the
index added by seedShootsBySeedNameIndex is cleared between tests; update the
seedShootsBySeedNameIndex function to register or invoke that reset/cleanup
after adding shoots.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/__tests__/hooks.spec.js`:
- Around line 114-117: The test for beforeListen stubs/asserts
cache.indexProjectsByNamespace but misses the new derived indexer
cache.indexShootsBySeedName; update the spec to mock cache.indexShootsBySeedName
(e.g., vi.fn()) alongside cache.indexProjectsByNamespace before calling
beforeListen, and add an assertion that cache.indexShootsBySeedName was called
(or calledWith expected args) after beforeListen returns; modify both
occurrences (around the existing stubs at the top and the second block around
lines 136-138) to keep the test isolated and prevent regressions.

In `@backend/lib/io/index.js`:
- Line 55: Wrap the async io.on('connection', async socket => { ... }) handler's
awaited room-join operations (notably helper.joinPrivateRoom(socket) and
subsequent socket.join(...) calls) in a try-catch block; on catch, log/emitthe
error and fail the socket deterministically (e.g., socket.emit('error',
err.message) and socket.disconnect(true) or call the same error handler pattern
used in 'subscribe'/'unsubscribe'/'synchronize') so unhandled promise rejections
are prevented and join failures are handled cleanly.

In `@backend/lib/io/seedstats.js`:
- Around line 79-90: getJoinedRooms currently filters out per-seed rooms when no
seedName is passed because appliesToSeed is written as ({ seedName: roomSeedName
}) => !roomSeedName || roomSeedName === seedName; change the predicate so that
when the caller does not provide seedName all rooms (global and all per-seed)
are returned, and when seedName is provided only global rooms and rooms matching
that seed are returned; update appliesToSeed in getJoinedRooms to something
like: return true for !roomSeedName OR seedName is undefined/null OR
roomSeedName === seedName (i.e. use the condition !roomSeedName || seedName ==
null || roomSeedName === seedName), keeping references to isSeedStatsRoom and
parseRoomName unchanged.

In `@backend/lib/services/seedstats.js`:
- Around line 234-255: The current getTicketsForShoot repeatedly scans
cache.getTicketCache().getIssues(), causing O(shoots×issues) work; instead build
a one-time index of issues by projectName+shootName for the current seedstats
refresh and use that index in getTicketsForShoot (or add a memoized lookup keyed
by shoot). Concretely: when handling the seedstats refresh (the function that
calls getTicketsForShoot/shouldHideShoot), iterate once over
cache.getTicketCache().getIssues() and populate a Map keyed by
`${issue.metadata.projectName}/${issue.metadata.name}` (or a nested Map by
projectName then name), then change getTicketsForShoot to read from that Map
(using getProjectNameForShoot(shoot) and shoot.metadata.name) and return [] if
missing; this avoids repeated full-array filters.

In `@frontend/src/components/SeedDetails/GSeedInfrastructureCard.vue`:
- Around line 155-175: The subscribe/unsubscribe calls can race; serialize them
by introducing a shared operation chain (e.g., a local Promise/lock) used by the
watcher and onUnmounted so each operation awaits the previous one before calling
seedStatStore.subscribe or seedStatStore.unsubscribe; specifically, add a
module-scoped variable like lastOp: Promise<void> = Promise.resolve(), wrap any
subscribe/unsubscribe calls in an async function that does `await lastOp` then
performs the async store call and assigns its returned promise to lastOp (and
still catch and forward errors to appStore.setError), and use that same wrapper
from the watch callback and onUnmounted so no subscribe can complete
out-of-order after a later unsubscribe.

In `@frontend/src/composables/useSeedItem/index.js`:
- Around line 257-259: The computed getters seedShootCount,
seedTotalUnhealthyShoots and seedUnhealthyShoots currently coerce missing data
into 0 (using ?? 0), which hides loading/unsubscribed/error states; change each
computed to return undefined when seedStat.value or the nested counts are
missing (e.g. remove the "?? 0" and rely on seedStat.value?.counts?.… returning
undefined) so consumers can detect undefined and render placeholders/skeletons
instead.

In `@frontend/src/composables/useShootListFilters.js`:
- Around line 16-20: The filter label for the exclusion rule is misleading:
update the FILTER_LABELS entry with key 'noOperatorAction' to a label that
matches the actual exclusion rule (e.g., "No operator action required" or
similar) so active filter chips and the donut tooltip correctly describe
excluded shoots; modify the label string in the FILTER_LABELS array where the
object has key 'noOperatorAction' to the chosen, accurate wording.

---

Nitpick comments:
In `@backend/__tests__/acceptance/api.seedstats.spec.js`:
- Around line 27-38: The test helper seedShootsBySeedNameIndex populates the
cache via cache.indexShootsBySeedName but does not clean up the index afterward;
to preserve test isolation add an explicit reset at the end of the helper (or
provide a paired teardown) that calls the cache reset method used elsewhere
(e.g., cache.cache.resetTicketCache or an equivalent reset function) so the
index added by seedShootsBySeedNameIndex is cleared between tests; update the
seedShootsBySeedNameIndex function to register or invoke that reset/cleanup
after adding shoots.

In `@backend/lib/cache/index.js`:
- Around line 97-99: getShootsBySeedName currently returns a MapIterator when
entries exist and an array when none exist; change it to always return the same
type (an array) by converting the MapIterator from
this.#seedNameToShoots.get(seedName)?.values() to an array (e.g. via Array.from
or spread) and return an empty array when there is no entry so callers of
getShootsBySeedName always receive an Array of shoots.

In `@backend/lib/io/shoots.js`:
- Around line 39-42: joinRoom's log uses singular "room" but joinRoom(room) can
receive an array; update joinRoom to detect Array.isArray(room) and either log
each room (e.g., iterate and call logger.debug per room with user.id) or log a
pluralized message with the rooms serialized, then call socket.join(room) as
before; change references in the joinRoom function (logger.debug, socket.join,
and user.id) so the log accurately reflects single or multiple rooms.

In `@backend/lib/watches/seeds.js`:
- Line 25: The update listener is passing an unused oldObject to handleEvent;
update the informer.on('update', ...) subscription so it calls
handleEvent('MODIFIED', object) with only the new object, removing the extra
oldObject parameter; locate the informer.on('update', (object, oldObject) =>
handleEvent('MODIFIED', object, oldObject)) line and change the callback to call
handleEvent with only the type and newObject to match handleEvent(type,
newObject).

In `@frontend/src/components/GShootHealthDonut.vue`:
- Around line 167-168: Clamp computed segment values to a non-negative range to
avoid negative SVG segments: in GShootHealthDonut.vue update the computed
properties hiddenUnhealthy and healthyShoots to ensure they never go below zero
(use shootCount, totalUnhealthy, matchingUnhealthy as inputs), e.g., compute
hiddenUnhealthy as max(0, totalUnhealthy - matchingUnhealthy) and healthyShoots
as max(0, shootCount - totalUnhealthy) so rendering always receives non-negative
segment sizes.

In `@frontend/src/store/seedStat.js`:
- Around line 82-98: The subscribe function currently propagates errors from
closeSubscription and can block establishing a new subscription; wrap the await
closeSubscription() call in a try/catch inside subscribe (referencing subscribe,
closeSubscription, openSubscription, fetchSeedStats, subscription.value and
subscribed.value), log or report the caught error via your logger/console, and
then continue to set subscription.value, call fetchSeedStats(options) and
openSubscription(options) so a failing close does not prevent the new
subscription from being created.
🪄 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: 6a2b717d-8bb3-4de8-95dd-2550ae90b3d7

📥 Commits

Reviewing files that changed from the base of the PR and between 15db54c and 2ca3a8a.

📒 Files selected for processing (61)
  • .gitignore
  • backend/__tests__/acceptance/api.members.spec.js
  • backend/__tests__/acceptance/api.seedstats.spec.js
  • backend/__tests__/acceptance/api.terminals.spec.js
  • backend/__tests__/acceptance/api.tickets.spec.js
  • backend/__tests__/acceptance/io.cors.spec.js
  • backend/__tests__/acceptance/io.spec.js
  • backend/__tests__/cache.spec.js
  • backend/__tests__/helpers/cache.js
  • backend/__tests__/hooks.spec.js
  • backend/__tests__/io.seedstats.spec.js
  • backend/__tests__/services.seedstats.spec.js
  • backend/__tests__/services.terminals.spec.js
  • backend/__tests__/utils.spec.js
  • backend/__tests__/watches.spec.js
  • backend/lib/cache/index.js
  • backend/lib/hooks.js
  • backend/lib/io/dispatcher.js
  • backend/lib/io/helper.js
  • backend/lib/io/index.js
  • backend/lib/io/seedstats.js
  • backend/lib/io/shoots.js
  • backend/lib/routes/index.js
  • backend/lib/routes/seedstats.js
  • backend/lib/services/index.js
  • backend/lib/services/seedstats.js
  • backend/lib/utils/index.js
  • backend/lib/watches/leases.js
  • backend/lib/watches/seeds.js
  • backend/lib/watches/shoots.js
  • frontend/__tests__/components/GSeedInfrastructureCard.spec.js
  • frontend/__tests__/components/GShootHealthDonut.spec.js
  • frontend/__tests__/composables/useDonutChart.spec.js
  • frontend/__tests__/composables/useSeedTableSorting.spec.js
  • frontend/__tests__/composables/useShootItem.spec.js
  • frontend/__tests__/composables/useShootListFilters.spec.js
  • frontend/__tests__/stores/seedStat.spec.js
  • frontend/__tests__/stores/shoot.spec.js
  • frontend/src/components/GSeedListRow.vue
  • frontend/src/components/GShootHealthDonut.vue
  • frontend/src/components/SeedDetails/GSeedInfrastructureCard.vue
  • frontend/src/components/Seeds/GSeedCapacityIndicator.vue
  • frontend/src/composables/useApi/api.js
  • frontend/src/composables/useDonutChart.js
  • frontend/src/composables/useSeedItem/index.js
  • frontend/src/composables/useSeedTableSorting.js
  • frontend/src/composables/useShootListFilters.js
  • frontend/src/composables/useSocketEventHandler.js
  • frontend/src/router/guards.js
  • frontend/src/store/helper.js
  • frontend/src/store/localStorage.js
  • frontend/src/store/seedStat.js
  • frontend/src/store/shoot/helper.js
  • frontend/src/store/shoot/shoot.js
  • frontend/src/store/socket/helper.js
  • frontend/src/store/socket/index.js
  • frontend/src/utils/errorCodes.js
  • frontend/src/utils/index.js
  • frontend/src/views/GAdministration.vue
  • frontend/src/views/GSeedList.vue
  • frontend/src/views/GShootList.vue
💤 Files with no reviewable changes (3)
  • frontend/tests/stores/shoot.spec.js
  • frontend/src/router/guards.js
  • frontend/src/store/localStorage.js

Comment thread backend/__tests__/hooks.spec.js
Comment thread backend/lib/io/index.js
Comment thread backend/lib/io/seedstats.js Outdated
Comment thread backend/lib/services/seedstats.js Outdated
Comment thread frontend/src/components/SeedDetails/GSeedInfrastructureCard.vue Outdated
Comment thread frontend/src/composables/useSeedItem/index.js Outdated
Comment thread frontend/src/composables/useShootListFilters.js
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: 4

🧹 Nitpick comments (6)
frontend/src/components/GShootHealthDonut.vue (2)

218-227: Nit: dead branch in formatCompact.

For value in [1000, 10000), v = Math.floor(value / 100) / 10 is always < 10 (since value/100 < 100), so v < 10 ? 1 : 0 is effectively constant 1. You can drop the ternary and always use toFixed(1).

♻️ Suggested simplification
   if (value < 10000) {
-    const v = Math.floor(value / 100) / 10
-    return `${v.toFixed(v < 10 ? 1 : 0)}k`
+    const v = Math.floor(value / 100) / 10
+    return `${v.toFixed(1)}k`
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/GShootHealthDonut.vue` around lines 218 - 227, The
formatCompact function contains a dead branch: in the 1000–9999 range v =
Math.floor(value / 100) / 10 is always < 10, so the ternary v < 10 ? 1 : 0 is
redundant; update the formatCompact implementation (the function named
formatCompact) to always use toFixed(1) for that branch (and you can optionally
simplify the v calculation or the return to a single toFixed(1) call) so the
ternary is removed and the code is clearer.

167-168: Optional: guard against transient negative derived counts.

If backend updates briefly report totalUnhealthyShoots > shootCount or matchingUnhealthyShoots > totalUnhealthyShoots (e.g., during a room re-subscription race), healthyShoots / hiddenUnhealthy could become negative and feed into the donut geometry and tooltip. A Math.max(0, …) clamp would make the component resilient to such transient inconsistencies without affecting normal behavior.

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

In `@frontend/src/components/GShootHealthDonut.vue` around lines 167 - 168, Clamp
derived counts to zero to avoid transient negatives: update the computed
properties hiddenUnhealthy and healthyShoots so their returned values use
Math.max(0, ...). Specifically, change hiddenUnhealthy (which currently computes
totalUnhealthy.value - matchingUnhealthy.value) to return Math.max(0,
totalUnhealthy.value - matchingUnhealthy.value) and change healthyShoots (which
currently computes shootCount.value - totalUnhealthy.value) to return
Math.max(0, shootCount.value - totalUnhealthy.value); keep the same
reactive/computed wrappers and names.
backend/lib/services/seedstats.js (1)

164-184: Minor: empty string silently parses to mask 0.

Number('') returns 0, so an explicit empty unhealthyFilterMask query/option parses as "no filters" rather than being rejected. If you want to treat a missing value and an explicit empty value the same (both reject), consider requiring a non-empty string before the Number() conversion. If the permissive behavior is intentional, ignore.

♻️ Suggested tweak
   if (typeof unhealthyFilterMask === 'string') {
+    if (unhealthyFilterMask.trim() === '') {
+      throw invalidUnhealthyFilterMask()
+    }
     unhealthyFilterMask = Number(unhealthyFilterMask)
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/lib/services/seedstats.js` around lines 164 - 184, The
parseUnhealthyFilterMask currently treats an explicit empty string as 0 because
Number('') === 0; update parseUnhealthyFilterMask to reject empty strings before
converting (e.g., if typeof unhealthyFilterMask === 'string' &&
unhealthyFilterMask.trim() === '' then throw invalidUnhealthyFilterMask()), then
proceed to Number(...) and validate with isValidUnhealthyFilterMask; reference
functions: parseUnhealthyFilterMask, isValidUnhealthyFilterMask and the error
helper invalidUnhealthyFilterMask and constant ALL_UNHEALTHY_FILTER_FLAGS.
backend/lib/watches/shoots.js (1)

70-90: Perf: cache.getSeed() deep-clones on every shoot event.

cache.getSeed(name) (exported helper) does _.chain(...).find(...).cloneDeep().value() — an O(N) scan plus a full deep clone of the seed object — executed on every shoot add/update/delete and additionally once per affected seedName (up to 2 per update). Here only metadata.uid is needed, so the clone is pure overhead. In bulk informer resync this can be a noticeable hotspot.

Consider using the un-cloned lookup from the underlying store:

♻️ Suggested refactor
-    for (const seedName of affectedSeedNames) {
-      const seedUid = cache.getSeed(seedName)?.metadata?.uid
+    for (const seedName of affectedSeedNames) {
+      const seedUid = cache.cache.get('seeds')?.find(['metadata.name', seedName])?.metadata?.uid
       if (!seedUid) {
         continue
       }

Alternatively, add a dedicated getSeedUidByName(name) helper that avoids the clone.

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

In `@backend/lib/watches/shoots.js` around lines 70 - 90, The publishSeedStats
function is calling cache.getSeed(seedName) which deep-clones the whole seed
object (expensive) even though only metadata.uid is needed; change the lookup to
avoid the clone by either (A) adding a new helper cache.getSeedUidByName(name)
that returns seed.metadata.uid without clone and use that here, or (B) access
the underlying store directly (e.g., cache.store or index) to retrieve the seed
object reference and read .metadata.uid, then keep the rest of the loop
(getJoinedRooms, shouldEmitSeedStatsEvent, nsp.to(room).emit) unchanged so only
the uid is read without cloning the entire seed.
backend/lib/cache/index.js (1)

97-99: Inconsistent return type: iterator vs array.

getShootsBySeedName returns a MapIterator when the seed is known and a plain Array ([]) when not. for..of consumers work either way, but any caller using array methods (e.g., .length, .map, .filter) would silently break for the known-seed path. Consider normalizing to an array for a consistent API.

♻️ Suggested tweak
   getShootsBySeedName (seedName) {
-    return this.#seedNameToShoots.get(seedName)?.values() ?? []
+    const bucket = this.#seedNameToShoots.get(seedName)
+    return bucket ? Array.from(bucket.values()) : []
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/lib/cache/index.js` around lines 97 - 99, getShootsBySeedName
currently returns a Map iterator when a seed exists and an array when it
doesn't, causing inconsistent types; change it to always return an array by
materializing the iterator (for example, use Array.from on
this.#seedNameToShoots.get(seedName)?.values()) and keep the fallback as an
empty array so callers can reliably use array methods like .length/.map/.filter.
backend/__tests__/acceptance/io.spec.js (1)

260-320: Minor: 'seeds' is now listed both in defaultRooms and in individual assertions.

Since defaultRooms now includes 'seeds' (line 220), every new Set([...defaultRooms, 'seeds', ...]) in this block (and similar ones at lines ~293, ~300, ~317, ~336 below, plus ~501, ~519, ~537, ~555 in the admin block) contains a redundant 'seeds'. The Set dedupes so tests still pass, but the expectations read as if the room is asserted twice. Consider cleaning these up for clarity.

♻️ Example cleanup
     expect(getRooms(socket, nsp)).toEqual(new Set([
       ...defaultRooms,
-      'seeds',
       'shoots;garden-foo/fooShoot',
     ]))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/__tests__/acceptance/io.spec.js` around lines 260 - 320, The test
assertions redundantly include 'seeds' alongside defaultRooms (which already
contains 'seeds'); update the expectations to remove the duplicate literal so
they read new Set([...defaultRooms, 'shoots;garden-foo', ...]) etc. Locate uses
around the getRooms(socket, nsp) checks in the describe block (references:
defaultRooms, getRooms, nsp, subscribe, unsubscribe, publishEvent) and delete
the extra 'seeds' entries in each new Set([...defaultRooms, 'seeds', ...])
occurrence (including similar cases in the admin block) so the Set contents are
clearer while preserving test semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/lib/cache/index.js`:
- Around line 140-146: findProjectByNamespace now throws NotFound for unknown
namespaces and four callers don't handle that; update each caller (the calls in
terminals utils, terminals index, MemberManager, and tickets route) to wrap the
call to findProjectByNamespace(namespace) in a try/catch that specifically
catches NotFound and handles it appropriately for the context (e.g., return
null/undefined or skip the operation, or send a 404 response from the route), or
alternatively add a safe existence check in the cache (e.g., a
hasProjectForNamespace(namespace) method) and use that before calling
findProjectByNamespace to avoid the throw.

In `@frontend/src/components/GShootHealthDonut.vue`:
- Around line 243-257: The ariaLabel computed currently uses static plural nouns
and must pluralize based on counts: update the ariaLabel computed (function
ariaLabel and its usage of shootCount.value, matchingUnhealthy.value,
healthyShoots.value, hiddenUnhealthy.value, and filterDescription.value) to
choose "shoot" vs "shoots" (and "excluded" phrasing) when the numeric value ===
1; e.g., build each phrase using a small inline conditional that picks singular
when count === 1 and plural otherwise, and keep the rest of the structure
(including the hiddenUnhealthy filter desc) unchanged.

In `@frontend/src/composables/useApi/api.js`:
- Around line 261-266: The getSeedStat function currently calls
encodeURIComponent on a possibly undefined name causing requests to
/api/seedstats/undefined; update getSeedStat to validate that name is provided
and is a non-empty string (e.g., if (!name || typeof name !== 'string') throw
new Error(...)) before calling encodeURIComponent, so callers must pass a name
(use getSeedStats for the "all seeds" case); reference getSeedStat to add this
guard and keep existing encodeURIComponent and withQuery usage unchanged.

In `@frontend/src/store/helper.js`:
- Around line 21-22: The lock TTL is set to 30_000ms but socket
acknowledgementTimeout is 60_000ms, allowing a second caller to acquire() while
the first synchronize/subscribe is still in flight; update the expiresAt
calculation in the lock (where this.expiresAt is set) to use the same
acknowledgementTimeout constant from frontend/src/store/socket/index.js (or
derive TTL = acknowledgementTimeout + small buffer) so the lock outlives the
longest socket ack window and prevents concurrent acquires with identical
options.

---

Nitpick comments:
In `@backend/__tests__/acceptance/io.spec.js`:
- Around line 260-320: The test assertions redundantly include 'seeds' alongside
defaultRooms (which already contains 'seeds'); update the expectations to remove
the duplicate literal so they read new Set([...defaultRooms,
'shoots;garden-foo', ...]) etc. Locate uses around the getRooms(socket, nsp)
checks in the describe block (references: defaultRooms, getRooms, nsp,
subscribe, unsubscribe, publishEvent) and delete the extra 'seeds' entries in
each new Set([...defaultRooms, 'seeds', ...]) occurrence (including similar
cases in the admin block) so the Set contents are clearer while preserving test
semantics.

In `@backend/lib/cache/index.js`:
- Around line 97-99: getShootsBySeedName currently returns a Map iterator when a
seed exists and an array when it doesn't, causing inconsistent types; change it
to always return an array by materializing the iterator (for example, use
Array.from on this.#seedNameToShoots.get(seedName)?.values()) and keep the
fallback as an empty array so callers can reliably use array methods like
.length/.map/.filter.

In `@backend/lib/services/seedstats.js`:
- Around line 164-184: The parseUnhealthyFilterMask currently treats an explicit
empty string as 0 because Number('') === 0; update parseUnhealthyFilterMask to
reject empty strings before converting (e.g., if typeof unhealthyFilterMask ===
'string' && unhealthyFilterMask.trim() === '' then throw
invalidUnhealthyFilterMask()), then proceed to Number(...) and validate with
isValidUnhealthyFilterMask; reference functions: parseUnhealthyFilterMask,
isValidUnhealthyFilterMask and the error helper invalidUnhealthyFilterMask and
constant ALL_UNHEALTHY_FILTER_FLAGS.

In `@backend/lib/watches/shoots.js`:
- Around line 70-90: The publishSeedStats function is calling
cache.getSeed(seedName) which deep-clones the whole seed object (expensive) even
though only metadata.uid is needed; change the lookup to avoid the clone by
either (A) adding a new helper cache.getSeedUidByName(name) that returns
seed.metadata.uid without clone and use that here, or (B) access the underlying
store directly (e.g., cache.store or index) to retrieve the seed object
reference and read .metadata.uid, then keep the rest of the loop
(getJoinedRooms, shouldEmitSeedStatsEvent, nsp.to(room).emit) unchanged so only
the uid is read without cloning the entire seed.

In `@frontend/src/components/GShootHealthDonut.vue`:
- Around line 218-227: The formatCompact function contains a dead branch: in the
1000–9999 range v = Math.floor(value / 100) / 10 is always < 10, so the ternary
v < 10 ? 1 : 0 is redundant; update the formatCompact implementation (the
function named formatCompact) to always use toFixed(1) for that branch (and you
can optionally simplify the v calculation or the return to a single toFixed(1)
call) so the ternary is removed and the code is clearer.
- Around line 167-168: Clamp derived counts to zero to avoid transient
negatives: update the computed properties hiddenUnhealthy and healthyShoots so
their returned values use Math.max(0, ...). Specifically, change hiddenUnhealthy
(which currently computes totalUnhealthy.value - matchingUnhealthy.value) to
return Math.max(0, totalUnhealthy.value - matchingUnhealthy.value) and change
healthyShoots (which currently computes shootCount.value - totalUnhealthy.value)
to return Math.max(0, shootCount.value - totalUnhealthy.value); keep the same
reactive/computed wrappers and names.
🪄 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: b522b86a-a7c2-4711-98df-1ef97093209c

📥 Commits

Reviewing files that changed from the base of the PR and between 2ca3a8a and 85d3e37.

📒 Files selected for processing (61)
  • .gitignore
  • backend/__tests__/acceptance/api.members.spec.js
  • backend/__tests__/acceptance/api.seedstats.spec.js
  • backend/__tests__/acceptance/api.terminals.spec.js
  • backend/__tests__/acceptance/api.tickets.spec.js
  • backend/__tests__/acceptance/io.cors.spec.js
  • backend/__tests__/acceptance/io.spec.js
  • backend/__tests__/cache.spec.js
  • backend/__tests__/helpers/cache.js
  • backend/__tests__/hooks.spec.js
  • backend/__tests__/io.seedstats.spec.js
  • backend/__tests__/services.seedstats.spec.js
  • backend/__tests__/services.terminals.spec.js
  • backend/__tests__/utils.spec.js
  • backend/__tests__/watches.spec.js
  • backend/lib/cache/index.js
  • backend/lib/hooks.js
  • backend/lib/io/dispatcher.js
  • backend/lib/io/helper.js
  • backend/lib/io/index.js
  • backend/lib/io/seedstats.js
  • backend/lib/io/shoots.js
  • backend/lib/routes/index.js
  • backend/lib/routes/seedstats.js
  • backend/lib/services/index.js
  • backend/lib/services/seedstats.js
  • backend/lib/utils/index.js
  • backend/lib/watches/leases.js
  • backend/lib/watches/seeds.js
  • backend/lib/watches/shoots.js
  • frontend/__tests__/components/GSeedInfrastructureCard.spec.js
  • frontend/__tests__/components/GShootHealthDonut.spec.js
  • frontend/__tests__/composables/useDonutChart.spec.js
  • frontend/__tests__/composables/useSeedTableSorting.spec.js
  • frontend/__tests__/composables/useShootItem.spec.js
  • frontend/__tests__/composables/useShootListFilters.spec.js
  • frontend/__tests__/stores/seedStat.spec.js
  • frontend/__tests__/stores/shoot.spec.js
  • frontend/src/components/GSeedListRow.vue
  • frontend/src/components/GShootHealthDonut.vue
  • frontend/src/components/SeedDetails/GSeedInfrastructureCard.vue
  • frontend/src/components/Seeds/GSeedCapacityIndicator.vue
  • frontend/src/composables/useApi/api.js
  • frontend/src/composables/useDonutChart.js
  • frontend/src/composables/useSeedItem/index.js
  • frontend/src/composables/useSeedTableSorting.js
  • frontend/src/composables/useShootListFilters.js
  • frontend/src/composables/useSocketEventHandler.js
  • frontend/src/router/guards.js
  • frontend/src/store/helper.js
  • frontend/src/store/localStorage.js
  • frontend/src/store/seedStat.js
  • frontend/src/store/shoot/helper.js
  • frontend/src/store/shoot/shoot.js
  • frontend/src/store/socket/helper.js
  • frontend/src/store/socket/index.js
  • frontend/src/utils/errorCodes.js
  • frontend/src/utils/index.js
  • frontend/src/views/GAdministration.vue
  • frontend/src/views/GSeedList.vue
  • frontend/src/views/GShootList.vue
💤 Files with no reviewable changes (3)
  • frontend/tests/stores/shoot.spec.js
  • frontend/src/router/guards.js
  • frontend/src/store/localStorage.js
✅ Files skipped from review due to trivial changes (17)
  • .gitignore
  • backend/tests/acceptance/api.tickets.spec.js
  • backend/tests/acceptance/api.terminals.spec.js
  • backend/lib/io/shoots.js
  • backend/tests/services.terminals.spec.js
  • frontend/tests/composables/useSeedTableSorting.spec.js
  • frontend/src/views/GAdministration.vue
  • backend/tests/acceptance/api.members.spec.js
  • backend/lib/hooks.js
  • frontend/src/utils/index.js
  • frontend/src/utils/errorCodes.js
  • backend/lib/services/index.js
  • backend/lib/routes/seedstats.js
  • backend/tests/services.seedstats.spec.js
  • backend/tests/acceptance/api.seedstats.spec.js
  • backend/lib/io/seedstats.js
  • backend/tests/helpers/cache.js
🚧 Files skipped from review as they are similar to previous changes (21)
  • backend/lib/routes/index.js
  • frontend/src/composables/useSeedTableSorting.js
  • backend/lib/io/index.js
  • backend/tests/hooks.spec.js
  • backend/tests/utils.spec.js
  • backend/lib/watches/seeds.js
  • backend/tests/cache.spec.js
  • frontend/src/composables/useSocketEventHandler.js
  • frontend/tests/composables/useShootListFilters.spec.js
  • frontend/tests/components/GSeedInfrastructureCard.spec.js
  • backend/tests/io.seedstats.spec.js
  • backend/lib/utils/index.js
  • backend/lib/io/dispatcher.js
  • frontend/src/components/SeedDetails/GSeedInfrastructureCard.vue
  • frontend/src/components/Seeds/GSeedCapacityIndicator.vue
  • frontend/src/store/seedStat.js
  • frontend/src/views/GSeedList.vue
  • frontend/tests/stores/seedStat.spec.js
  • backend/lib/watches/leases.js
  • frontend/src/composables/useShootListFilters.js
  • frontend/src/composables/useSeedItem/index.js

Comment thread backend/lib/cache/index.js
Comment thread frontend/src/components/GShootHealthDonut.vue
Comment thread frontend/src/composables/useApi/api.js
Comment thread frontend/src/store/helper.js
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: 1

🧹 Nitpick comments (2)
backend/lib/services/seedstats.js (1)

164-178: parseUnhealthyFilterMask rejects missing values but silently accepts empty strings as 0.

Number('') evaluates to 0, which passes isValidUnhealthyFilterMask, so ?unhealthyFilterMask= (empty) yields the unfiltered view while ?unhealthyFilterMask omitted yields 422. That asymmetry can confuse API clients. Consider rejecting empty strings (or all whitespace) explicitly, or — if the parameter is conceptually required — also reject '' consistently with undefined.

♻️ Suggested tightening
   if (typeof unhealthyFilterMask === 'string') {
+    if (unhealthyFilterMask.trim() === '') {
+      throw invalidUnhealthyFilterMask()
+    }
     unhealthyFilterMask = Number(unhealthyFilterMask)
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/lib/services/seedstats.js` around lines 164 - 178,
parseUnhealthyFilterMask currently converts an empty string to 0 (via
Number('')) which passes isValidUnhealthyFilterMask; update
parseUnhealthyFilterMask to explicitly reject empty or all-whitespace strings by
checking the raw string (e.g., trim() === '') and throwing
invalidUnhealthyFilterMask() before converting to Number, so that both omitted
(undefined) and empty-string parameters are treated consistently; keep
references to parseUnhealthyFilterMask, isValidUnhealthyFilterMask, and
invalidUnhealthyFilterMask when making the change.
backend/lib/cache/tickets.js (1)

11-13: Optional: avoid JSON.stringify for hot-path key building.

shootKey is called on every cache mutation and lookup. A simple delimited string (with a separator unlikely to appear in resource names, e.g. /) would be cheaper and equally collision-free for Kubernetes-style names.

♻️ Optional refactor
-function shootKey (projectName, name) {
-  return JSON.stringify([projectName, name])
-}
+function shootKey (projectName, name) {
+  return `${projectName}/${name}`
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/lib/cache/tickets.js` around lines 11 - 13, The hot-path function
shootKey currently uses JSON.stringify([projectName, name]) which is slower;
replace it with a cheap delimited string construction in shootKey (e.g.
projectName + '/' + name) using a separator unlikely to appear in resource names
(or escape the separator if necessary) so cache lookups/mutations that call
shootKey are faster and collision-free for Kubernetes-style names.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/lib/services/seedstats.js`:
- Around line 257-269: getProjectNameForShoot is spamming logger.warn for the
same failing namespace on every refresh; change the logging to avoid repeated
warnings by adding a module-level Set (e.g., failedNamespaceWarned) and in
getProjectNameForShoot call cache.findProjectByNamespace(namespace) inside the
try/catch and only call logger.warn('Failed to resolve project for namespace %s:
%s', namespace, err.message) if failedNamespaceWarned does not contain
namespace, then add namespace to the set; also add a hook to clear
failedNamespaceWarned at the start or end of each seedstats aggregation/tick
(where the aggregation loop runs) so failures are re-attempted next tick;
alternatively, if you prefer simpler behavior, replace logger.warn with
logger.debug in getProjectNameForShoot to demote the noise.

---

Nitpick comments:
In `@backend/lib/cache/tickets.js`:
- Around line 11-13: The hot-path function shootKey currently uses
JSON.stringify([projectName, name]) which is slower; replace it with a cheap
delimited string construction in shootKey (e.g. projectName + '/' + name) using
a separator unlikely to appear in resource names (or escape the separator if
necessary) so cache lookups/mutations that call shootKey are faster and
collision-free for Kubernetes-style names.

In `@backend/lib/services/seedstats.js`:
- Around line 164-178: parseUnhealthyFilterMask currently converts an empty
string to 0 (via Number('')) which passes isValidUnhealthyFilterMask; update
parseUnhealthyFilterMask to explicitly reject empty or all-whitespace strings by
checking the raw string (e.g., trim() === '') and throwing
invalidUnhealthyFilterMask() before converting to Number, so that both omitted
(undefined) and empty-string parameters are treated consistently; keep
references to parseUnhealthyFilterMask, isValidUnhealthyFilterMask, and
invalidUnhealthyFilterMask when making the change.
🪄 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: 7501cecf-e339-4ea1-93e3-1fbcce30d761

📥 Commits

Reviewing files that changed from the base of the PR and between 85d3e37 and f8600a4.

📒 Files selected for processing (9)
  • backend/__tests__/cache.tickets.spec.js
  • backend/__tests__/hooks.spec.js
  • backend/__tests__/io.seedstats.spec.js
  • backend/__tests__/services.seedstats.spec.js
  • backend/lib/cache/tickets.js
  • backend/lib/io/seedstats.js
  • backend/lib/services/seedstats.js
  • frontend/src/composables/useApi/api.js
  • frontend/src/composables/useSeedItem/index.js
✅ Files skipped from review due to trivial changes (1)
  • backend/tests/io.seedstats.spec.js
🚧 Files skipped from review as they are similar to previous changes (4)
  • backend/tests/hooks.spec.js
  • backend/tests/services.seedstats.spec.js
  • frontend/src/composables/useSeedItem/index.js
  • backend/lib/io/seedstats.js

Comment on lines +257 to +269
function getProjectNameForShoot (shoot) {
const namespace = shoot?.metadata?.namespace
if (!namespace) {
return
}
try {
const project = cache.findProjectByNamespace(namespace)
return project?.metadata?.name
} catch (err) {
logger.warn('Failed to resolve project for namespace %s: %s', namespace, err.message)
return 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

Potential log spam from getProjectNameForShoot.

When FILTER_HIDE_TICKETS is enabled, this is invoked for every unhealthy shoot per seedstats refresh (and per Socket.IO room emission). A single namespace consistently failing project resolution will produce a warn per shoot per refresh tick. Consider rate-limiting, demoting to debug, or memoizing failures within a single aggregation pass.

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

In `@backend/lib/services/seedstats.js` around lines 257 - 269,
getProjectNameForShoot is spamming logger.warn for the same failing namespace on
every refresh; change the logging to avoid repeated warnings by adding a
module-level Set (e.g., failedNamespaceWarned) and in getProjectNameForShoot
call cache.findProjectByNamespace(namespace) inside the try/catch and only call
logger.warn('Failed to resolve project for namespace %s: %s', namespace,
err.message) if failedNamespaceWarned does not contain namespace, then add
namespace to the set; also add a hook to clear failedNamespaceWarned at the
start or end of each seedstats aggregation/tick (where the aggregation loop
runs) so failures are re-attempted next tick; alternatively, if you prefer
simpler behavior, replace logger.warn with logger.debug in
getProjectNameForShoot to demote the noise.

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/__tests__/stores/seedStat.spec.js`:
- Line 126: The Promise executor uses a one-line block that causes
lint/verification failures; update the test declaration for fetchPromise (and
the similar occurrence around line 151) so the executor body is on separate
lines: declare fetchPromise = new Promise(resolve => { then on the next line
assign fetchResolve = resolve and close the block on its own line. Ensure you
change both instances (the variable names fetchPromise and fetchResolve in this
test file) so each assignment is on its own line inside the executor.
🪄 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: 9d7ad202-c58e-419d-9080-dc8945f94417

📥 Commits

Reviewing files that changed from the base of the PR and between f8600a4 and 7acf8b4.

📒 Files selected for processing (4)
  • frontend/__tests__/stores/seedStat.spec.js
  • frontend/src/components/SeedDetails/GSeedInfrastructureCard.vue
  • frontend/src/store/seedStat.js
  • frontend/src/views/GSeedList.vue
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/store/seedStat.js

Comment thread frontend/__tests__/stores/seedStat.spec.js Outdated
@gardener-prow gardener-prow Bot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Apr 24, 2026
…watcher

The store's subscribe(), unsubscribe(), and synchronize() were async functions that could interleave across await boundaries — an older subscribe could resume and call openSubscription after a newer unsubscribe had already cleared the store.

Replace the imperative async lifecycle with a single Vue watch on subscription + refreshNonce. onCleanup cancels stale runs at each await boundary. Public API becomes synchronous; callers simplified.
@gardener-prow gardener-prow Bot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Apr 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla: yes Indicates the PR's author has signed the cla-assistant.io CLA. kind/enhancement Enhancement, improvement, extension size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant