Skip to content

Feat/group#68

Merged
jabibamman merged 57 commits intomasterfrom
feat/group
Jul 6, 2025
Merged

Feat/group#68
jabibamman merged 57 commits intomasterfrom
feat/group

Conversation

@jabibamman
Copy link
Contributor

@jabibamman jabibamman commented Jul 3, 2025

Summary by CodeRabbit

  • New Features

    • Introduced comprehensive server and VM group management with creation, editing, deletion, resource assignment, and shutdown orchestration.
    • Added numerous group-related UI components including cards, lists, flow views, modals (create, edit, details, delete confirmation), action menus, pagination, selection, shutdown preview dialogs, and a management panel.
    • Enabled priority-based cascading start/stop actions and multiple group view modes (sections, grid, list, flow).
    • Enhanced history/audit log with detailed event views, user tooltips, dynamic filtering, and a new filter panel.
    • Added paginated loading and infinite scroll for groups, servers, and UPS items.
    • Introduced UPS maintenance and power specification components for detailed device insights.
    • Added SSH terminal access with authentication modal in server details.
    • Added command palette state management with auto-focus on open.
    • Enhanced user profile with tabbed interface, activity logs, security controls, and modals for password reset and account deletion.
    • Introduced toggle switches and improved UI controls across settings and profile.
    • Added detailed changelog UI with stats and a team section.
    • Added presence indicators to user avatars.
    • Introduced resource selection and group deletion confirmation components.
    • Added group priority management with draggable server and VM cards, priority swapping, and utility functions.
    • Added new group shutdown workflow with selection, preview, and execution dialogs.
    • Added caching composable for API data to optimize fetching.
    • Added setup progress component and enhanced setup steps with read-only modes and animations.
    • Added new sidebar navigation with dynamic loading and lazy loading of nested rooms, servers, VMs, and UPSs.
    • Added new priority input component with presets and validation.
    • Added new group management panel with mode switching and unsaved changes handling.
    • Added new components for server and VM cards with priority and drag state indicators.
    • Added new comprehensive history filters component for advanced event filtering.
  • Improvements

    • Expanded and improved localization for English and French, covering new features and UI elements.
    • Centralized and streamlined state management for groups, servers, UPS items, and command palette with enhanced pagination and loading indicators.
    • Added dark mode styling support to authentication components, modals, and profile dialogs.
    • Replaced static sidebar data with dynamic paginated loading and lazy loading of nested items.
    • Refined UI formatting and accessibility across multiple components for better readability and user experience.
    • Improved history list view with detailed filters, modals, and enhanced event presentation.
    • Enhanced UPS and server list views with incremental loading and refined scroll handling.
    • Improved command palette focus behavior and state management.
    • Enhanced group management UI with improved filtering, sorting, resource mapping, and pagination.
    • Improved server details view with SSH terminal integration and authentication modal.
    • Added pagination controls for group listings.
    • Improved user dropdown and profile UI components.
    • Refined setup view with animated transitions and progress tracking.
    • Improved UPS details view by modularizing power and maintenance information.
    • Enhanced server header with terminal access button.
    • Improved group flow drag-and-drop behavior with error handling and notifications.
    • Enhanced group shutdown UI with admin confirmation and detailed preview.
    • Improved resource selection components with search, grid/list toggles, and filtering.
    • Improved login form to prevent concurrent login attempts.
    • Enhanced server store with pagination and admin fetching capabilities.
    • Improved UPS store with pagination and incremental loading.
    • Refined group store with comprehensive state and actions for group lifecycle.
    • Improved dashboard view with caching and store integration for stats.
    • Enhanced setup steps with read-only modes and improved dark mode support.
    • Improved welcome step with animated features and conditional interactivity.
  • Bug Fixes

    • Improved error handling and fallback to mock data for group fetching.
    • Fixed concurrency issues in login form to prevent multiple simultaneous login attempts.
  • Chores

    • Updated routing to display the new group management interface and added new shutdown route.
    • Added new runtime dependencies: "@headlessui/vue", "date-fns", "gsap", and "vue-draggable-plus" for UI components and animations.
    • Removed unused and deprecated components and views (e.g., HelloWorld for groups, ilos, vms, profile danger zone, and create room/UPS views).
    • Refactored API endpoints for VMs from plural to singular paths and added patch methods for servers and VMs.
    • Refined component formatting and template markup for consistency and readability across multiple files.
    • Added new Pinia store for command palette state management.
    • Removed deprecated chart component.
    • Improved component imports and modularization in UPS details view.
    • Refined command palette composable and component for centralized state and auto-focus behavior.

@jabibamman jabibamman self-assigned this Jul 3, 2025
@jabibamman jabibamman added the enhancement New feature or request label Jul 3, 2025
@coderabbitai
Copy link

coderabbitai bot commented Jul 3, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

This update introduces a comprehensive suite of features for managing groups (server and VM) in the application. It adds strongly typed group APIs, a Pinia store for group state, and multiple Vue components for group creation, listing, actions, details, and visual flow. The history and server modules are also enhanced with new types, filtering, and paginated loading. Extensive localization updates support the new UI and workflows.

Changes

File(s) Change Summary
src/features/groups/api.ts, src/features/groups/types.ts Added fully typed group API utilities, mock data generators, CRUD, resource management, and comprehensive type definitions.
src/features/groups/store.ts Introduced Pinia store for groups: state, computed, async actions for CRUD, filtering, pagination, and resource operations.
src/features/groups/components/*.vue Added Vue components for group creation modal, action menu, group card, details modal, flow visualization, list, pagination, selection, shutdown preview dialog, group edit modal, resource selector, group pagination, delete confirm modal, group management panel, simple resource selector, edit and view mode content, priority server and VM cards.
src/features/groups/views/GroupView.vue, GroupShutdownView.vue New views for managing groups with multiple modes, cascading actions, shutdown UI, modals, and context menus.
src/features/groups/views/HelloWorld.vue Removed placeholder HelloWorld component.
src/features/history/api.ts, src/features/history/store.ts Added method to fetch available entity types for filtering; exposed state and API for use in UI.
src/features/history/types.ts Extended enums and interfaces for new entities/actions; added mappings for UI styling and entity routing.
src/features/history/components/HistoryEventDetail.vue New component to display detailed, localized history event information with user, metadata, and change tracking.
src/features/history/views/HistoryListView.vue Enhanced history UI: detailed event modal, improved filters, icons, user tooltips, and richer event summaries.
src/features/servers/api.ts, src/features/servers/store.ts Added admin fetch method; refactored store for paginated loading, state, and backward compatibility.
src/features/servers/views/ServerListView.vue Refactored to use the centralized store for all server data, pagination, and error handling.
src/features/servers/components/ServerHeader.vue Added "Terminal" button and event for SSH terminal access.
src/features/servers/views/ServerDetailsView.vue Added SSH terminal and authentication modal components with related state and event handlers.
src/features/ups/store.ts, src/features/ups/views/UpsListView.vue Added paginated/infinite scroll loading, state, and admin fetch for UPS items; updated UI and store accordingly.
src/features/ups/components/MaintenanceInformation.vue, PowerSpecifications.vue, UpsCard.vue Added new UPS info components and enhanced UpsCard with server count prop.
src/features/ups/types.ts Extended UPS response type with serverCount property.
src/features/ups/views/UpsDetailsView.vue Refactored UPS details view to use new components for power specs and maintenance info.
src/locales/en.json, src/locales/fr.json Added extensive localization for groups, history, UPS, and common UI actions in English and French.
src/router/index.ts Updated /groups route to load the new GroupView.vue instead of the old placeholder; added /groups/shutdown route.
src/features/auth/components/TwoFAForm.vue, views/Enable2FAView.vue, views/TwoFAView.vue Added dark mode styling support to 2FA-related components.
src/features/groups/components/GroupPagination.vue Added new pagination component for group lists.
src/features/groups/components/GroupSelection.vue Added component for selecting server and VM groups with pagination and preview shutdown trigger.
src/features/groups/components/ShutdownPreviewDialog.vue Added modal dialog component for shutdown sequence preview and confirmation.
src/features/groups/components/DeleteConfirmModal.vue Added modal dialog for confirming group deletion with resource warnings.
src/features/groups/components/GroupEditModal.vue Added modal dialog for creating and editing groups with resource selection.
src/features/groups/components/ResourceSelector.vue Added resource selector component supporting search, select all/none, and grid/list views.
src/features/changelog/components/TeamSection.vue Added new team section component displaying team members with avatars, roles, and quotes.
src/features/changelog/views/ChangelogView.vue Redesigned changelog UI with hero, stats cards, enhanced releases list, and team section; updated scrollbar styling.
src/components/CommandPalette.vue, useCommandPalette.ts, stores/commandPalette.ts Refactored command palette state management to use Pinia store; added focus behavior on open; removed direct exposure of open method.
src/layouts/components/TreeNavbar.vue Refactored to dynamic hierarchical data loading with paginated stores and lazy VM loading; added scroll handling and navigation.
src/features/groups/stores/usePriorityStore.ts Added Pinia store for managing server and VM priorities with optimistic updates and swapping.
src/features/groups/types/priority.ts Added TypeScript types for priority management of servers and VMs.
src/features/groups/utils/priority-utils.ts Added utility functions for swapping, reordering, sorting priorities and mapping priority to colors and labels.
src/features/groups/components/priority/ServerCard.vue Added priority-aware server card component with drag and hover states.
src/features/groups/components/priority/VmCard.vue Added priority-aware VM card component with drag and hover states.
src/composables/useApiCache.ts Added generic caching composable with TTL support for API data.
src/features/auth/store.ts Enhanced current user fetching with caching and forced reload option.
src/features/dashboard/store.ts Integrated caching for dashboard stats and history data fetching.
src/features/setup/views/SetupView.vue Refactored setup view to use animated transitions, progress bar component, and read-only mode detection.
src/features/setup/components/steps/*.vue Added read-only mode support to setup steps with UI disabling and saved data loading.
src/features/profile/views/ProfileView.vue Major profile view refactor to tabbed interface with presence, modals, stats, activity list, and toast notifications.
src/features/profile/components/ProfileStat.vue, ProfileTab.vue, ProfileActivityItem.vue Added new profile UI components for stats display, tab buttons, and activity items with localization and formatting.
src/features/users/components/UserAvatar.vue Enhanced avatar with optional presence pulse indicator.
src/shared/components/ToggleSwitch.vue Added toggle switch component with accessible keyboard and mouse support.
Multiple components and views Added dark mode styling support and numerous template formatting improvements for readability without functional changes.

Sequence Diagram(s)

Group Creation and Management Flow

sequenceDiagram
    participant User
    participant GroupView
    participant CreateGroupModal
    participant GroupStore
    participant GroupAPI

    User->>GroupView: Click "Create Group"
    GroupView->>CreateGroupModal: Open modal
    CreateGroupModal->>User: User fills form & submits
    CreateGroupModal->>GroupView: Emit 'create' with payload
    GroupView->>GroupStore: addGroup(payload)
    GroupStore->>GroupAPI: createGroup(payload)
    GroupAPI-->>GroupStore: Return new group
    GroupStore-->>GroupView: Update state
    GroupView-->>User: Show success, update UI
Loading

Group List Loading with Pagination

sequenceDiagram
    participant User
    participant GroupList
    participant GroupStore
    participant GroupAPI

    User->>GroupList: Scrolls or clicks "Load More"
    GroupList->>GroupStore: loadMore()
    GroupStore->>GroupAPI: fetchGroups(params, append=true)
    GroupAPI-->>GroupStore: Return page of groups
    GroupStore-->>GroupList: Append to group list
    GroupList-->>User: Display additional groups
Loading

History Event Detail Viewing

sequenceDiagram
    participant User
    participant HistoryListView
    participant HistoryEventDetail

    User->>HistoryListView: Clicks "Details" on event
    HistoryListView->>HistoryEventDetail: Open modal with event data
    HistoryEventDetail-->>User: Show detailed event info
Loading

Poem

In the warren of code, new features bloom,
Groups now gather, dispelling the gloom.
Cards and flows, a modal or two,
History remembers what rabbits do.
With paginated hops and localizations bright,
This patch brings the garden of groups to light!
🐇✨


🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

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

🧹 Nitpick comments (15)
src/features/history/api.ts (1)

30-37: Fix incomplete JSDoc documentation.

The JSDoc example is incomplete and cuts off mid-sentence. Complete the example to show the full response structure.

Apply this diff to fix the documentation:

-  /** Get Available entity types for history filtering
-   * @returns Promise resolving with an array of entity type strings.
-   * @example
-   * {
-   * "entityTypes": [
-   *  "user",
-   * "organization",
-   */
+  /** Get Available entity types for history filtering
+   * @returns Promise resolving with an array of entity type strings.
+   * @example
+   * {
+   *   "entityTypes": [
+   *     "user",
+   *     "organization",
+   *     "server",
+   *     "network"
+   *   ]
+   * }
+   */
src/features/history/store.ts (1)

37-52: Improve JSDoc formatting consistency.

The method implementation is solid and follows the store patterns well. However, the JSDoc formatting could be improved for consistency.

Apply this diff to improve the JSDoc formatting:

-  /** Get Available entity types for history filtering by calling API
-   * @returns Promise resolving with an array of entity type strings.
-   * @example
-   * {
-   * "entityTypes": [
-   *  "user",
-   *  "organization",
-   *  "server",
-   *  "network",
-   * ]
-   */
+  /**
+   * Get Available entity types for history filtering by calling API
+   * @returns Promise resolving with an array of entity type strings.
+   * @example
+   * {
+   *   "entityTypes": [
+   *     "user",
+   *     "organization",
+   *     "server",
+   *     "network"
+   *   ]
+   * }
+   */
src/features/groups/components/GroupFlow.vue (5)

241-249: Improve type safety in getNodeColor

The type assertion on line 248 could be avoided with better type checking.

Consider using a type guard or avoiding the assertion:

 const getNodeColor = (node: Node) => {
   const priorityColors = {
   };
-  return priorityColors[node.data.priority as 1 | 2 | 3 | 4] || '#6b7280';
+  const priority = node.data.priority;
+  if (priority in priorityColors) {
+    return priorityColors[priority as keyof typeof priorityColors];
+  }
+  return '#6b7280';
 };

9-9: Simplify the arrow function call.

The arrow function wrapper is unnecessary when calling fitView without arguments.

-          @click="() => fitView()"
+          @click="fitView"

155-160: Extract magic numbers as constants for better maintainability.

The position values and spacing should be configurable constants.

+const HORIZONTAL_SPACING = 300;
+const VERTICAL_SPACING = 120;
+const NODE_WIDTH = 250;
+
 const nodes = computed<Node[]>(() => {
   const priorityPositions = {
-    1: { x: 0, y: 0 },
-    2: { x: 300, y: 0 },
-    3: { x: 600, y: 0 },
-    4: { x: 900, y: 0 },
+    1: { x: 0 * HORIZONTAL_SPACING, y: 0 },
+    2: { x: 1 * HORIZONTAL_SPACING, y: 0 },
+    3: { x: 2 * HORIZONTAL_SPACING, y: 0 },
+    4: { x: 3 * HORIZONTAL_SPACING, y: 0 },
   };

179-181: Use consistent spacing constants.

Replace the hardcoded value with the constant for consistency.

       const position = orientation.value === 'horizontal'
-        ? { x: basePos.x, y: index * 120 }
-        : { x: index * 250, y: basePos.x };
+        ? { x: basePos.x, y: index * VERTICAL_SPACING }
+        : { x: index * NODE_WIDTH, y: basePos.x };

192-195: Simplify nested ternary operators for better readability.

Extract the pluralization logic into a helper function.

+const getResourceLabel = (count: number, type: 'server' | 'vm') => {
+  if (type === 'server') {
+    return count === 1 ? 'server' : 'servers';
+  }
+  return count === 1 ? 'VM' : 'VMs';
+};
+
       nodeList.push({
         id: group.id,
         type: 'group',
         position,
         data: {
           label: group.name,
           priority: group.priority,
           type: group.type,
           cascade: group.cascade,
           resourceCount: group.type === 'server' ? group.serverIds.length : group.vmIds.length,
-          resourceLabel: group.type === 'server' 
-            ? (group.serverIds.length === 1 ? 'server' : 'servers')
-            : (group.vmIds.length === 1 ? 'VM' : 'VMs'),
+          resourceLabel: getResourceLabel(
+            group.type === 'server' ? group.serverIds.length : group.vmIds.length,
+            group.type
+          ),
           group,
         },
       });
src/features/groups/components/CreateGroupModal.vue (1)

143-156: Consider comprehensive form validation

The current validation only checks for non-empty name, but the form has other required fields (type, priority) that should be validated.

Add validation for all required fields:

 const isValid = computed(() => {
-  return formData.value.name.trim().length > 0;
+  return formData.value.name.trim().length > 0 &&
+    formData.value.type &&
+    formData.value.priority >= 1 && formData.value.priority <= 4;
 });

Also, if groups require resources at creation time, consider adding a resource selection UI or document that resources are added post-creation.

src/features/groups/api.ts (1)

12-71: Mock data uses hardcoded IDs that may not match actual resources

The mock groups reference specific server and VM IDs (e.g., '1', '4', 'vm-1') that might not exist in the actual system.

Consider either:

  1. Using placeholder IDs that are clearly mock data
  2. Fetching actual resource IDs to create realistic mock data
  3. Documenting that these are example IDs only
 export const getMockServerGroups = (): ServerGroup[] => [
   {
     id: 'group-web',
     name: 'Web Servers',
     description: 'Frontend web server cluster',
     type: 'server',
     priority: 1,
     cascade: true,
-    serverIds: ['1', '4', '5'],
+    serverIds: ['mock-server-1', 'mock-server-2', 'mock-server-3'],
     roomId: 'room-1',
   },
src/features/history/types.ts (1)

85-97: Consider using icon components instead of emoji

Using emoji characters for icons may lead to inconsistent rendering across different platforms, browsers, and operating systems.

Consider using icon identifiers that map to proper icon components:

 export const actionStyles: Record<string, { color: string; icon: string }> = {
-  CREATE: { color: 'text-green-600', icon: '➕' },
-  UPDATE: { color: 'text-blue-600', icon: '✏️' },
-  DELETE: { color: 'text-red-600', icon: '🗑️' },
+  CREATE: { color: 'text-green-600', icon: 'plus' },
+  UPDATE: { color: 'text-blue-600', icon: 'pencil' },
+  DELETE: { color: 'text-red-600', icon: 'trash' },
   // ... rest of the mappings
 };

Then use a mapping function in your components to render the appropriate icon component based on the string identifier.

src/features/groups/components/GroupList.vue (1)

1-256: Consider decomposing this large component

This component is handling multiple responsibilities and could benefit from being broken down into smaller, more focused components.

Consider extracting:

  1. The search/filter toolbar into a GroupListToolbar.vue component
  2. Each view mode (sections, grid, list) into separate components like GroupSectionView.vue, GroupGridView.vue, GroupListItemView.vue
  3. The empty state into a GroupEmptyState.vue component

This would improve maintainability, testability, and make the code easier to understand.

src/features/servers/views/ServerListView.vue (1)

317-317: Simplify the click handler.

The arrow function wrapper is unnecessary here.

-            @click="() => loadMore(pageSize)"
+            @click="loadMore(pageSize)"
src/features/history/components/HistoryEventDetail.vue (2)

22-25: Use unknown instead of any for better type safety.

Using any bypasses TypeScript's type checking. unknown is safer and forces proper type guards.

-const formatJson = (data: any) => {
+const formatJson = (data: unknown) => {
   if (!data) return null;
   return JSON.stringify(data, null, 2);
 };

377-377: Improve readability of metadata key filtering.

The long inline condition is hard to read and maintain.

Define the excluded keys as a constant:

+// At the top of the script section
+const CONTEXTUAL_METADATA_KEYS = new Set([
+  'userEmail', 'identifier', 'loginMethod', 'requires2FA', 
+  'operationType', 'targetRoleName', 'targetRoleIsAdmin', 
+  'previousRoleCount', 'newRoleCount', 'isElevationToAdmin', 
+  'isRemovalFromAdmin', 'serverType', 'operatingSystem', 
+  'assignedToGroup'
+]);

 // In the template
-            <div v-if="!['userEmail', 'identifier', 'loginMethod', 'requires2FA', 'operationType', 'targetRoleName', 'targetRoleIsAdmin', 'previousRoleCount', 'newRoleCount', 'isElevationToAdmin', 'isRemovalFromAdmin', 'serverType', 'operatingSystem', 'assignedToGroup'].includes(String(key))"
+            <div v-if="!CONTEXTUAL_METADATA_KEYS.has(String(key))"
src/features/history/views/HistoryListView.vue (1)

270-273: Consider extracting tooltip position calculation for better readability.

The inline ternary logic for tooltip positioning is complex. Consider extracting it to a computed property or method for better maintainability.

+const getTooltipPosition = (index: number, total: number) => {
+  if (index === 0 && total === 1) return 'top-full mt-8';
+  if (index === 0) return 'top-full mt-2';
+  return 'bottom-full mb-2';
+};

Then use it in the template:

-                    evIdx === 0 && events.length === 1 ? 'top-full mt-8' : evIdx === 0 ? 'top-full mt-2' : 'bottom-full mb-2'
+                    getTooltipPosition(evIdx, events.length)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 07c7b77 and 7b91186.

📒 Files selected for processing (24)
  • src/features/groups/api.ts (1 hunks)
  • src/features/groups/components/CreateGroupModal.vue (1 hunks)
  • src/features/groups/components/GroupActionMenu.vue (1 hunks)
  • src/features/groups/components/GroupCard.vue (1 hunks)
  • src/features/groups/components/GroupDetailsModal.vue (1 hunks)
  • src/features/groups/components/GroupFlow.vue (1 hunks)
  • src/features/groups/components/GroupList.vue (1 hunks)
  • src/features/groups/store.ts (1 hunks)
  • src/features/groups/types.ts (1 hunks)
  • src/features/groups/views/GroupView.vue (1 hunks)
  • src/features/groups/views/HelloWorld.vue (0 hunks)
  • src/features/history/api.ts (1 hunks)
  • src/features/history/components/HistoryEventDetail.vue (1 hunks)
  • src/features/history/store.ts (2 hunks)
  • src/features/history/types.ts (3 hunks)
  • src/features/history/views/HistoryListView.vue (3 hunks)
  • src/features/servers/api.ts (1 hunks)
  • src/features/servers/store.ts (2 hunks)
  • src/features/servers/views/ServerListView.vue (5 hunks)
  • src/features/ups/store.ts (4 hunks)
  • src/features/ups/views/UpsListView.vue (5 hunks)
  • src/locales/en.json (2 hunks)
  • src/locales/fr.json (2 hunks)
  • src/router/index.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • src/features/groups/views/HelloWorld.vue
🧰 Additional context used
🧬 Code Graph Analysis (5)
src/features/servers/api.ts (1)
src/features/servers/types.ts (1)
  • Server (6-23)
src/features/history/store.ts (2)
src/features/history/api.ts (1)
  • historyApi (16-47)
src/features/history/types.ts (1)
  • HistoryFilter (65-71)
src/features/ups/store.ts (2)
src/features/ups/types.ts (3)
  • UpsResponseDto (34-41)
  • UpsListResponse (43-48)
  • UpsUpdateDto (24-32)
src/features/ups/api.ts (1)
  • upsApi (15-80)
src/features/groups/api.ts (1)
src/features/groups/types.ts (7)
  • ServerGroup (15-18)
  • VmGroup (20-23)
  • Group (25-25)
  • GroupListParams (52-57)
  • GroupListResponse (45-50)
  • CreateGroupPayload (27-35)
  • UpdateGroupPayload (37-43)
src/features/servers/store.ts (2)
src/features/servers/types.ts (4)
  • Server (6-23)
  • ServerListParams (49-52)
  • ServerListResponse (42-47)
  • CreateServerPayload (25-40)
src/features/servers/api.ts (3)
  • fetchServers (87-92)
  • getServersAdmin (116-119)
  • createServer (111-114)
🔇 Additional comments (19)
src/features/history/api.ts (1)

38-46: LGTM! Clean implementation following existing patterns.

The method implementation is well-structured and follows the same authentication and error handling patterns as the existing getList method.

src/router/index.ts (1)

63-63: LGTM! Clean route update for groups feature.

The route update correctly replaces the placeholder HelloWorld component with the new GroupView component, aligning with the PR's group management feature implementation.

src/features/servers/api.ts (1)

116-119: No change required for authentication on getServersAdmin.

The shared api instance in src/services/api.ts automatically injects the Authorization header—first via api.defaults.headers.common when a token is present, then again in its request interceptor. All requests made through api.get (including /server/admin/all) will carry the bearer token if available.

Notably, other admin calls such as getAllRoles also omit explicit auth headers and rely on the same interceptor. You can continue using the current pattern without adding getAuthHeaders().

src/features/history/store.ts (2)

21-21: LGTM! Clean state variable addition.

The new availableEntityTypes state variable is well-placed and follows the existing reactive state pattern.


73-75: LGTM! Proper exposure of new state and method.

The new state variable and method are correctly exposed in the store's return object, maintaining consistency with the existing pattern.

src/features/groups/components/GroupActionMenu.vue (4)

1-37: LGTM! Well-structured template with consistent styling.

The template is clean and follows good Vue practices with proper event handling, consistent styling classes, and appropriate use of Heroicons for visual consistency.


39-63: LGTM! Proper TypeScript setup with clean interfaces.

The props interface and emit definitions are well-typed and the component setup follows Vue 3 composition API best practices.


64-82: LGTM! Clean action handling with proper event emission.

The action handler correctly maps different actions to their respective events and ensures the menu closes after each action.


84-110: LGTM! Excellent viewport overflow prevention and cleanup.

The positioning logic correctly handles viewport boundaries, and the component properly manages event listeners with cleanup in onUnmounted. This prevents memory leaks and ensures the menu always stays within the viewport.

src/locales/fr.json (1)

936-1031: LGTM! Comprehensive French translations

The translations are well-structured and cover all the new features. The French text is grammatically correct and follows consistent patterns.

src/features/groups/types.ts (1)

1-65: Well-structured type definitions

The type definitions are comprehensive and well-organized, using discriminated unions effectively for the different group types. Good use of TypeScript features for type safety.

src/features/servers/store.ts (1)

22-53: Well-implemented pagination logic!

The fetchServers method properly handles pagination with the append functionality for infinite scrolling, updates all necessary state variables, and includes proper error handling.

src/locales/en.json (1)

943-1111: Localization entries are well-structured.

The new localization keys for common actions, history details, and groups functionality follow consistent naming conventions and are properly organized.

src/features/ups/views/UpsListView.vue (2)

33-49: LGTM!

The computed property correctly uses .value for reactive refs and implements proper client-side filtering.


67-67: Verify server-side search support for paginated loading.

The code passes searchQuery.value to loadMore, but the filtering in filteredUps is client-side. This could cause inconsistencies if the server doesn't support search filtering or if the filtering logic differs.

Please verify that the UPS API supports search query parameters for paginated requests, or remove the search parameter from the loadMore call if filtering should remain client-side only.

src/features/history/views/HistoryListView.vue (2)

82-87: Excellent use of concurrent data fetching!

Using Promise.all to fetch history and entity types concurrently improves initial load performance.


184-195: Good enhancement for datetime precision!

Changing to datetime-local inputs allows users to filter events with time precision, which is valuable for audit logs.

src/features/groups/components/GroupFlow.vue (2)

1-250: Well-implemented flow visualization component!

The component effectively uses VueFlow to create an interactive group visualization with:

  • Clean separation of template, script, and style sections
  • Proper TypeScript typing (with minor exceptions noted)
  • Good dark mode support
  • Intuitive priority-based coloring and positioning

The suggested improvements are mostly for enhanced maintainability and user experience.


8-22: Add accessibility attributes to interactive elements.

The buttons should have proper aria-labels for screen reader users.

         <button
           @click="fitView"
           class="px-3 py-1.5 text-sm bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-md transition-colors flex items-center gap-1"
+          :aria-label="$t('groups.fitView')"
         >
           <ArrowsPointingInIcon class="w-4 h-4" />
           {{ $t('groups.fitView') }}
         </button>
         <button
           @click="toggleOrientation"
           class="px-3 py-1.5 text-sm bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-md transition-colors flex items-center gap-1"
+          :aria-label="$t('groups.toggleOrientation')"
         >

Likely an incorrect or invalid review comment.

Comment on lines 66 to 76
const createUps = async (payload: UpsCreationDto) => {
const created = await upsApi.create(payload);
await fetchUps();
await fetchUps(1, 12);
return created;
};

const updateUps = async (id: string, payload: UpsUpdateDto) => {
const updated = await upsApi.update(id, payload);
await fetchUps();
await fetchUps(1, 12);
return updated;
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider preserving current pagination state after create/update

After creating or updating a UPS item, the store resets to page 1 with a hardcoded limit of 12. This might not match the user's current view settings.

Consider preserving the current pagination state or make the reset behavior configurable:

 const createUps = async (payload: UpsCreationDto) => {
   const created = await upsApi.create(payload);
-  await fetchUps(1, 12);
+  // Option 1: Preserve current view
+  await fetchUps(currentPage.value, 12);
+  // Option 2: Make configurable
+  // await fetchUps(1, currentLimit.value || 12);
   return created;
 };

 const updateUps = async (id: string, payload: UpsUpdateDto) => {
   const updated = await upsApi.update(id, payload);
-  await fetchUps(1, 12);
+  await fetchUps(currentPage.value, 12);
   return updated;
 };
📝 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
const createUps = async (payload: UpsCreationDto) => {
const created = await upsApi.create(payload);
await fetchUps();
await fetchUps(1, 12);
return created;
};
const updateUps = async (id: string, payload: UpsUpdateDto) => {
const updated = await upsApi.update(id, payload);
await fetchUps();
await fetchUps(1, 12);
return updated;
};
const createUps = async (payload: UpsCreationDto) => {
const created = await upsApi.create(payload);
// Option 1: Preserve current view
await fetchUps(currentPage.value, 12);
// Option 2: Make configurable
// await fetchUps(1, currentLimit.value || 12);
return created;
};
const updateUps = async (id: string, payload: UpsUpdateDto) => {
const updated = await upsApi.update(id, payload);
await fetchUps(currentPage.value, 12);
return updated;
};
🤖 Prompt for AI Agents
In src/features/ups/store.ts around lines 66 to 76, the createUps and updateUps
functions reset the pagination to page 1 with a limit of 12 after each
operation, ignoring the user's current pagination state. Modify these functions
to retrieve and use the current page and limit values from the store or pass
them as parameters, so that fetchUps is called with the user's current
pagination settings instead of hardcoded values.

Comment on lines 146 to 151
export const removeResourcesFromGroup = async (id: string, resourceIds: string[]) => {
const response = await api.delete<Group>(`/groups/${id}/resources`, {
data: { resourceIds },
});
return response.data;
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

DELETE with request body might not be universally supported

Using DELETE with a request body is uncommon and might not work with all HTTP clients or proxies.

Consider using a POST endpoint for removing resources or passing IDs as query parameters:

 export const removeResourcesFromGroup = async (id: string, resourceIds: string[]) => {
-  const response = await api.delete<Group>(`/groups/${id}/resources`, {
-    data: { resourceIds },
-  });
+  // Option 1: Use POST for removal
+  const response = await api.post<Group>(`/groups/${id}/resources/remove`, { resourceIds });
+  // Option 2: Use query parameters (if few IDs)
+  // const params = new URLSearchParams();
+  // resourceIds.forEach(rid => params.append('resourceId', rid));
+  // const response = await api.delete<Group>(`/groups/${id}/resources?${params}`);
   return response.data;
 };
📝 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
export const removeResourcesFromGroup = async (id: string, resourceIds: string[]) => {
const response = await api.delete<Group>(`/groups/${id}/resources`, {
data: { resourceIds },
});
return response.data;
};
export const removeResourcesFromGroup = async (id: string, resourceIds: string[]) => {
// Option 1: Use POST for removal
const response = await api.post<Group>(`/groups/${id}/resources/remove`, { resourceIds });
// Option 2: Use query parameters (if few IDs)
// const params = new URLSearchParams();
// resourceIds.forEach(rid => params.append('resourceId', rid));
// const response = await api.delete<Group>(`/groups/${id}/resources?${params}`);
return response.data;
};
🤖 Prompt for AI Agents
In src/features/groups/api.ts around lines 146 to 151, the
removeResourcesFromGroup function uses a DELETE request with a request body,
which is not universally supported by all HTTP clients or proxies. To fix this,
change the API call to use a POST request to a dedicated endpoint for removing
resources or modify the DELETE request to pass resource IDs as query parameters
instead of in the body. Update the backend accordingly if needed to support this
change.

Comment on lines 158 to 165
const loadGroups = async () => {
try {
await groupStore.fetchGroups({ page: 1, limit: 12 });
} catch (error) {
const mockGroups = getMockGroups();
groupStore.list = mockGroups;
}
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Notify users when falling back to mock data.

The error is silently caught and mock data is used without informing the user. This could lead to confusion about whether they're viewing real or test data.

   try {
     await groupStore.fetchGroups({ page: 1, limit: 12 });
   } catch (error) {
+    toast.error(t('groups.loadError'));
+    console.error('Failed to load groups:', error);
     const mockGroups = getMockGroups();
     groupStore.list = mockGroups;
   }
📝 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
const loadGroups = async () => {
try {
await groupStore.fetchGroups({ page: 1, limit: 12 });
} catch (error) {
const mockGroups = getMockGroups();
groupStore.list = mockGroups;
}
};
const loadGroups = async () => {
try {
await groupStore.fetchGroups({ page: 1, limit: 12 });
} catch (error) {
toast.error(t('groups.loadError'));
console.error('Failed to load groups:', error);
const mockGroups = getMockGroups();
groupStore.list = mockGroups;
}
};
🤖 Prompt for AI Agents
In src/features/groups/views/GroupView.vue around lines 158 to 165, the catch
block silently falls back to mock data without notifying the user. Modify the
catch block to include a user notification, such as a toast message or alert,
informing them that real data could not be loaded and mock data is being
displayed instead. This will improve user awareness and transparency.

Comment on lines 252 to 254
const dependentGroups = groupStore.list.filter(
other => other.priority === g.priority + 1 && other.cascade
);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Handle priority gaps in cascade logic.

The current logic only finds groups with exactly priority + 1, which will miss dependent groups if there are gaps in priority values.

     const dependentGroups = groupStore.list.filter(
-      other => other.priority === g.priority + 1 && other.cascade
+      other => other.priority > g.priority && other.cascade
+    ).sort((a, b) => a.priority - b.priority);

This ensures all dependent groups with higher priority are included and processed in order.

📝 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
const dependentGroups = groupStore.list.filter(
other => other.priority === g.priority + 1 && other.cascade
);
const dependentGroups = groupStore.list.filter(
other => other.priority > g.priority && other.cascade
).sort((a, b) => a.priority - b.priority);
🤖 Prompt for AI Agents
In src/features/groups/views/GroupView.vue around lines 252 to 254, the
filtering logic only selects groups with priority exactly one greater than the
current group, missing groups with higher priorities if gaps exist. Modify the
filter condition to include all groups with priority greater than the current
group's priority and with cascade true, then sort these groups by priority
ascending to ensure correct processing order.

Comment on lines 269 to 275
const startGroupResources = async (group: Group) => {
console.log('Starting resources for group:', group.name);
};

const stopGroupResources = async (group: Group) => {
console.log('Stopping resources for group:', group.name);
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add TODO comments for placeholder implementations.

These functions are placeholders that only log to console. They should be clearly marked for future implementation.

 const startGroupResources = async (group: Group) => {
+  // TODO: Implement actual resource start logic
   console.log('Starting resources for group:', group.name);
+  // await groupApi.startGroup(group.id);
 };

 const stopGroupResources = async (group: Group) => {
+  // TODO: Implement actual resource stop logic  
   console.log('Stopping resources for group:', group.name);
+  // await groupApi.stopGroup(group.id);
 };
📝 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
const startGroupResources = async (group: Group) => {
console.log('Starting resources for group:', group.name);
};
const stopGroupResources = async (group: Group) => {
console.log('Stopping resources for group:', group.name);
};
const startGroupResources = async (group: Group) => {
// TODO: Implement actual resource start logic
console.log('Starting resources for group:', group.name);
// await groupApi.startGroup(group.id);
};
const stopGroupResources = async (group: Group) => {
// TODO: Implement actual resource stop logic
console.log('Stopping resources for group:', group.name);
// await groupApi.stopGroup(group.id);
};
🤖 Prompt for AI Agents
In src/features/groups/views/GroupView.vue around lines 269 to 275, the
functions startGroupResources and stopGroupResources currently only log messages
to the console as placeholders. Add clear TODO comments inside each function
indicating that these are placeholder implementations and need to be properly
implemented in the future.

Comment on lines 237 to 239
const toggleOrientation = () => {
orientation.value = orientation.value === 'horizontal' ? 'vertical' : 'horizontal';
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Trigger fitView after orientation change for better UX.

When toggling orientation, the view should automatically fit to show all repositioned nodes.

 const toggleOrientation = () => {
   orientation.value = orientation.value === 'horizontal' ? 'vertical' : 'horizontal';
+  // Wait for DOM update before fitting view
+  nextTick(() => {
+    fitView();
+  });
 };

Don't forget to import nextTick from Vue:

-import { ref, computed } from 'vue';
+import { ref, computed, nextTick } from 'vue';
📝 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
const toggleOrientation = () => {
orientation.value = orientation.value === 'horizontal' ? 'vertical' : 'horizontal';
};
// At the top of the file
-import { ref, computed } from 'vue';
+import { ref, computed, nextTick } from 'vue';
// …
const toggleOrientation = () => {
orientation.value = orientation.value === 'horizontal'
? 'vertical'
: 'horizontal';
// Wait for DOM update before fitting view
nextTick(() => {
fitView();
});
};
🤖 Prompt for AI Agents
In src/features/groups/components/GroupFlow.vue around lines 237 to 239, after
toggling the orientation value, call nextTick to wait for the DOM update and
then trigger the fitView method to adjust the view to show all repositioned
nodes. Import nextTick from Vue at the top of the file if not already imported.
This ensures the view updates correctly after the orientation change for better
user experience.

Comment on lines 230 to 235
const handleNodeClick = (event: any) => {
const node = event.node;
if (node?.data?.group) {
emit('group-click', node.data.group);
}
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Use proper TypeScript types instead of any.

Replace the any type with the proper VueFlow event type for better type safety.

-const handleNodeClick = (event: any) => {
+import type { NodeMouseEvent } from '@vue-flow/core';
+
+const handleNodeClick = (event: NodeMouseEvent) => {

Don't forget to add the import at the top of the script section.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/features/groups/components/GroupFlow.vue around lines 230 to 235, replace
the use of the `any` type for the event parameter in the handleNodeClick
function with the correct VueFlow event type to improve type safety. Identify
and import the appropriate event type from VueFlow at the top of the script
section, then update the function signature to use this type instead of `any`.

Copy link

@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

♻️ Duplicate comments (6)
src/features/groups/components/GroupSelection.vue (1)

251-259: Extract duplicated getPriorityClass function

src/features/groups/api.ts (1)

16-27: Inconsistent error handling across API functions

Also applies to: 29-40, 42-52, 54-64

src/features/groups/views/GroupView.vue (4)

280-286: Add TODO comments for placeholder implementations.

These functions are placeholders that only log to console. They should be clearly marked for future implementation.

 const startGroupResources = async (group: Group) => {
+  // TODO: Implement actual resource start logic
   console.log('Starting resources for group:', group.name);
+  // await groupApi.startGroup(group.id);
 };

 const stopGroupResources = async (group: Group) => {
+  // TODO: Implement actual resource stop logic  
   console.log('Stopping resources for group:', group.name);
+  // await groupApi.stopGroup(group.id);
 };

144-148: Critical: Remove Math.random() from computed property.

Using Math.random() in a computed property causes the VM state to change on every re-render, breaking Vue's reactivity and causing UI inconsistencies.

       map[group.id] = (vmGroup.vmIds || []).map(id => ({
         id,
         name: `VM ${id}`,
-        state: Math.random() > 0.5 ? 'active' : 'inactive',
+        state: 'inactive', // TODO: Fetch actual VM states from API or store
       }));

263-265: Handle priority gaps in cascade logic.

The current logic only finds groups with exactly priority + 1, which will miss dependent groups if there are gaps in priority values.

     const dependentGroups = groupStore.allGroups.filter(
-      (other: Group) => other.priority === g.priority + 1 && other.cascade
+      (other: Group) => other.priority > g.priority && other.cascade
+    ).sort((a, b) => a.priority - b.priority);

170-176: Notify users when group loading fails.

The error is silently caught without informing the user. This could lead to confusion about whether they're viewing real or fallback data.

 const loadGroups = async () => {
   try {
     await groupStore.loadAllGroups();
   } catch (error) {
+    toast.error(t('groups.loadError'));
     console.error('Failed to load groups:', error);
   }
 };
🧹 Nitpick comments (5)
src/features/groups/views/GroupShutdownView.vue (1)

26-37: Consider using a toast notification system instead of fixed positioning

The fixed positioning of the success message could overlap with other UI elements. Consider using a toast notification library or Vue portal to ensure proper stacking and positioning.

You could use a library like vue-toastification or implement a portal-based solution:

-<div
-  v-if="showSuccessMessage"
-  class="fixed bottom-4 right-4 p-4 bg-green-100 border border-green-200 rounded-lg shadow-lg dark:bg-green-900/20 dark:border-green-800"
->
+<Teleport to="body">
+  <div
+    v-if="showSuccessMessage"
+    class="fixed bottom-4 right-4 p-4 bg-green-100 border border-green-200 rounded-lg shadow-lg dark:bg-green-900/20 dark:border-green-800 z-50"
+    role="status"
+    aria-live="polite"
+  >
src/features/groups/components/ShutdownPreviewDialog.vue (1)

38-41: Add defensive checks for pagination display

The pagination display calculation could fail with edge cases.

 {{ $t('pagination.showing') }} 
-{{ ((groupStore.shutdownPreview.currentPage - 1) * 10) + 1 }}-{{ Math.min(groupStore.shutdownPreview.currentPage * 10, groupStore.shutdownPreview.totalItems) }}
+{{ groupStore.shutdownPreview.totalItems > 0 ? ((groupStore.shutdownPreview.currentPage - 1) * 10) + 1 : 0 }}-{{ Math.min(groupStore.shutdownPreview.currentPage * 10, groupStore.shutdownPreview.totalItems) }}
 {{ $t('pagination.of') }} {{ groupStore.shutdownPreview.totalItems }}
src/features/groups/api.ts (3)

20-25: Use optional chaining for cleaner code

Static analysis correctly identifies that optional chaining would be cleaner here.

 export const fetchServerGroups = async (params: ServerGroupListParams = {}) => {
   const response = await api.get<GroupServerListResponseDto>('/group/server', {
     params,
   });
-  if (response.data && response.data.items) {
-    response.data.items = response.data.items.map((item) => ({
+  if (response.data?.items) {
+    response.data.items = response.data.items.map((item) => ({
       ...item,
       type: 'server' as const,
     }));
   }
   return response;
 };

 export const fetchVmGroups = async (params: VmGroupListParams = {}) => {
   const response = await api.get<GroupVmListResponseDto>('/group/vm', {
     params,
   });
-  if (response.data && response.data.items) {
-    response.data.items = response.data.items.map((item) => ({
+  if (response.data?.items) {
+    response.data.items = response.data.items.map((item) => ({
       ...item,
       type: 'vm' as const,
     }));
   }
   return response;
 };

Also applies to: 33-38


75-77: Simplify redundant type assertions

The type assertions are unnecessary with proper typing.

-  return { ...response.data, type } as
-    | GroupServerResponseDto
-    | GroupVmResponseDto;
+  return { ...response.data, type };

TypeScript should be able to infer the correct type based on the discriminated union with the type property.

Also applies to: 93-95, 113-115


136-184: Move mock data to a separate file

Mock data generation doesn't belong in the API module. This violates separation of concerns.

Create a separate file src/features/groups/mocks/index.ts or src/features/groups/utils/mock-data.ts to house mock data generators. This keeps the API module focused on actual API interactions.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7b91186 and 24134b6.

📒 Files selected for processing (26)
  • src/features/auth/components/TwoFAForm.vue (6 hunks)
  • src/features/auth/views/Enable2FAView.vue (3 hunks)
  • src/features/auth/views/TwoFAView.vue (1 hunks)
  • src/features/groups/api.ts (1 hunks)
  • src/features/groups/components/GroupCard.vue (1 hunks)
  • src/features/groups/components/GroupDetailsModal.vue (1 hunks)
  • src/features/groups/components/GroupFlow.vue (1 hunks)
  • src/features/groups/components/GroupList.vue (1 hunks)
  • src/features/groups/components/GroupPagination.vue (1 hunks)
  • src/features/groups/components/GroupSelection.vue (1 hunks)
  • src/features/groups/components/ShutdownPreviewDialog.vue (1 hunks)
  • src/features/groups/store.ts (1 hunks)
  • src/features/groups/types.ts (1 hunks)
  • src/features/groups/views/GroupShutdownView.vue (1 hunks)
  • src/features/groups/views/GroupView.vue (1 hunks)
  • src/features/servers/store.ts (2 hunks)
  • src/features/ups/components/MaintenanceInformation.vue (1 hunks)
  • src/features/ups/components/PowerSpecifications.vue (1 hunks)
  • src/features/ups/components/UpsCard.vue (2 hunks)
  • src/features/ups/store.ts (4 hunks)
  • src/features/ups/types.ts (1 hunks)
  • src/features/ups/views/UpsDetailsView.vue (2 hunks)
  • src/features/ups/views/UpsListView.vue (5 hunks)
  • src/locales/en.json (4 hunks)
  • src/locales/fr.json (4 hunks)
  • src/router/index.ts (1 hunks)
✅ Files skipped from review due to trivial changes (5)
  • src/features/auth/views/TwoFAView.vue
  • src/features/auth/components/TwoFAForm.vue
  • src/features/auth/views/Enable2FAView.vue
  • src/features/ups/components/PowerSpecifications.vue
  • src/features/ups/components/MaintenanceInformation.vue
🚧 Files skipped from review as they are similar to previous changes (10)
  • src/router/index.ts
  • src/features/groups/components/GroupFlow.vue
  • src/features/ups/store.ts
  • src/features/groups/components/GroupList.vue
  • src/features/groups/components/GroupCard.vue
  • src/features/groups/components/GroupDetailsModal.vue
  • src/features/groups/store.ts
  • src/features/servers/store.ts
  • src/features/ups/views/UpsListView.vue
  • src/features/groups/types.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/features/groups/api.ts (1)
src/features/groups/types.ts (11)
  • ServerGroupListParams (65-70)
  • GroupServerListResponseDto (28-33)
  • VmGroupListParams (72-75)
  • GroupVmListResponseDto (35-40)
  • ShutdownRequestDto (61-63)
  • ShutdownPreviewParams (77-80)
  • ShutdownPreviewListResponseDto (52-59)
  • GroupServerResponseDto (4-14)
  • GroupVmResponseDto (16-26)
  • CreateGroupPayload (82-91)
  • UpdateGroupPayload (93-100)
🪛 Biome (1.9.4)
src/features/groups/api.ts

[error] 20-20: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 33-33: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (12)
src/features/ups/types.ts (1)

41-41: LGTM! Clean addition to support server count display.

The addition of serverCount property to the UpsResponseDto interface is well-designed and follows the existing patterns in the codebase.

src/features/ups/components/UpsCard.vue (2)

15-15: LGTM! Optional prop addition follows Vue best practices.

The optional serverCount prop is well-designed and maintains backward compatibility.


179-179: Excellent improvement replacing random data with actual prop data.

This change eliminates non-deterministic behavior and ensures consistent server count display across the application. The nullish coalescing operator provides a sensible fallback.

src/features/ups/views/UpsDetailsView.vue (2)

27-28: LGTM! Good modular component imports.

The import of dedicated components for power specifications and maintenance information improves code organization.


415-424: Verification Complete: Component Implementation Confirmed

Both PowerSpecifications.vue and MaintenanceInformation.vue are present under src/features/ups/components/ and correctly declare their props via defineProps<Props>() in the <script setup> block. No further changes are needed—approving the refactoring.

src/features/groups/api.ts (1)

125-134: Cascade toggling applies only to server groups
A search for a /group/vm/.../cascade endpoint returned no results, and there’s no VM-specific cascade support in the API. This behavior is intentional—VM groups don’t currently support cascade toggling. If VM cascade toggling is required in the future, a dedicated endpoint and client function will need to be added.

src/locales/fr.json (3)

154-157: LGTM: Consistent pagination translations added.

The pagination control translations are properly structured and follow standard French conventions.


994-1037: LGTM: Comprehensive history event localization.

The history namespace provides thorough coverage of audit events with properly structured actions, descriptions, and details. The use of placeholders like {method}, {email}, {hostname} etc. enables dynamic content insertion.


1039-1126: LGTM: Complete groups feature localization.

The groups namespace comprehensively covers all UI elements for the new group management feature including view modes, actions, form fields, status messages, and shutdown workflows. Translation quality appears consistent and professional.

src/locales/en.json (3)

1007-1050: LGTM: Well-structured history event localization.

The history namespace provides comprehensive coverage with clear action names, descriptive text with proper placeholder usage, and detailed audit information labels. The English translations are professional and consistent.


1052-1139: LGTM: Complete groups management localization.

The groups namespace thoroughly covers all aspects of the group management feature including UI labels, form fields, status messages, view modes, and shutdown workflows. The translations are clear and user-friendly.


949-959: LGTM: Essential common UI additions.

The new common UI action labels (create, edit, delete, save, etc.) provide necessary building blocks for the enhanced interface components.

Comment on lines 52 to 59
const onShutdownSuccess = () => {
showSuccessMessage.value = true;
groupStore.clearSelection();

setTimeout(() => {
showSuccessMessage.value = false;
}, 5000);
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Make success message timeout configurable and add error handling

The timeout is hardcoded and there's no error handling for the store operation.

+const SUCCESS_MESSAGE_TIMEOUT = 5000; // or import from a constants file
+
 const onShutdownSuccess = () => {
   showSuccessMessage.value = true;
-  groupStore.clearSelection();
+  try {
+    groupStore.clearSelection();
+  } catch (error) {
+    console.error('Failed to clear selection:', error);
+  }
   
   setTimeout(() => {
     showSuccessMessage.value = false;
-  }, 5000);
+  }, SUCCESS_MESSAGE_TIMEOUT);
 };
📝 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
const onShutdownSuccess = () => {
showSuccessMessage.value = true;
groupStore.clearSelection();
setTimeout(() => {
showSuccessMessage.value = false;
}, 5000);
};
// Make the success message timeout configurable
const SUCCESS_MESSAGE_TIMEOUT = 5000; // or import from a constants file
const onShutdownSuccess = () => {
showSuccessMessage.value = true;
try {
groupStore.clearSelection();
} catch (error) {
console.error('Failed to clear selection:', error);
}
setTimeout(() => {
showSuccessMessage.value = false;
}, SUCCESS_MESSAGE_TIMEOUT);
};
🤖 Prompt for AI Agents
In src/features/groups/views/GroupShutdownView.vue around lines 52 to 59, the
success message timeout is hardcoded and there is no error handling for the
groupStore.clearSelection() call. Refactor the code to make the timeout duration
configurable via a variable or prop, and wrap the groupStore.clearSelection()
call in a try-catch block to handle potential errors gracefully, logging or
displaying an error message if needed.

Comment on lines 8 to 38
<button
@click="$emit('page-change', currentPage - 1)"
:disabled="currentPage === 1"
class="px-3 py-1 border border-gray-300 rounded-md text-sm hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed dark:border-gray-600 dark:hover:bg-gray-800"
>
{{ $t('pagination.previous') }}
</button>

<div class="flex gap-1">
<button
v-for="page in visiblePages"
:key="page"
@click="$emit('page-change', page)"
class="px-3 py-1 text-sm rounded-md"
:class="[
page === currentPage
? 'bg-blue-600 text-white'
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'
]"
>
{{ page }}
</button>
</div>

<button
@click="$emit('page-change', currentPage + 1)"
:disabled="currentPage === totalPages"
class="px-3 py-1 border border-gray-300 rounded-md text-sm hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed dark:border-gray-600 dark:hover:bg-gray-800"
>
{{ $t('pagination.next') }}
</button>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve accessibility with ARIA labels

The pagination controls lack proper ARIA labels for screen reader users.

 <button
   @click="$emit('page-change', currentPage - 1)"
   :disabled="currentPage === 1"
+  :aria-label="$t('pagination.goToPreviousPage')"
   class="px-3 py-1 border border-gray-300 rounded-md text-sm hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed dark:border-gray-600 dark:hover:bg-gray-800"
 >
   {{ $t('pagination.previous') }}
 </button>

 <button
   v-for="page in visiblePages"
   :key="page"
   @click="$emit('page-change', page)"
+  :aria-label="`${$t('pagination.goToPage')} ${page}`"
+  :aria-current="page === currentPage ? 'page' : undefined"
   class="px-3 py-1 text-sm rounded-md"
   :class="[
     page === currentPage
       ? 'bg-blue-600 text-white'
       : 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'
   ]"
 >
   {{ page }}
 </button>

 <button
   @click="$emit('page-change', currentPage + 1)"
   :disabled="currentPage === totalPages"
+  :aria-label="$t('pagination.goToNextPage')"
   class="px-3 py-1 border border-gray-300 rounded-md text-sm hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed dark:border-gray-600 dark:hover:bg-gray-800"
 >
   {{ $t('pagination.next') }}
 </button>
📝 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
<button
@click="$emit('page-change', currentPage - 1)"
:disabled="currentPage === 1"
class="px-3 py-1 border border-gray-300 rounded-md text-sm hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed dark:border-gray-600 dark:hover:bg-gray-800"
>
{{ $t('pagination.previous') }}
</button>
<div class="flex gap-1">
<button
v-for="page in visiblePages"
:key="page"
@click="$emit('page-change', page)"
class="px-3 py-1 text-sm rounded-md"
:class="[
page === currentPage
? 'bg-blue-600 text-white'
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'
]"
>
{{ page }}
</button>
</div>
<button
@click="$emit('page-change', currentPage + 1)"
:disabled="currentPage === totalPages"
class="px-3 py-1 border border-gray-300 rounded-md text-sm hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed dark:border-gray-600 dark:hover:bg-gray-800"
>
{{ $t('pagination.next') }}
</button>
<button
@click="$emit('page-change', currentPage - 1)"
:disabled="currentPage === 1"
:aria-label="$t('pagination.goToPreviousPage')"
class="px-3 py-1 border border-gray-300 rounded-md text-sm hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed dark:border-gray-600 dark:hover:bg-gray-800"
>
{{ $t('pagination.previous') }}
</button>
<div class="flex gap-1">
<button
v-for="page in visiblePages"
:key="page"
@click="$emit('page-change', page)"
:aria-label="`${$t('pagination.goToPage')} ${page}`"
:aria-current="page === currentPage ? 'page' : undefined"
class="px-3 py-1 text-sm rounded-md"
:class="[
page === currentPage
? 'bg-blue-600 text-white'
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'
]"
>
{{ page }}
</button>
</div>
<button
@click="$emit('page-change', currentPage + 1)"
:disabled="currentPage === totalPages"
:aria-label="$t('pagination.goToNextPage')"
class="px-3 py-1 border border-gray-300 rounded-md text-sm hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed dark:border-gray-600 dark:hover:bg-gray-800"
>
{{ $t('pagination.next') }}
</button>
🤖 Prompt for AI Agents
In src/features/groups/components/GroupPagination.vue between lines 8 and 38,
the pagination buttons lack ARIA labels, which reduces accessibility for screen
reader users. Add appropriate aria-label attributes to the previous, next, and
page number buttons to clearly describe their function, such as "Go to previous
page", "Go to next page", and "Go to page X" respectively. This will improve
screen reader support and overall accessibility.

Comment on lines +210 to +218
const getPriorityClass = (priority: number) => {
if (priority >= 8) {
return 'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400';
} else if (priority >= 5) {
return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400';
} else {
return 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400';
}
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Extract duplicated getPriorityClass function to shared utility

This function is duplicated in GroupSelection.vue.

Create a shared utility file src/features/groups/utils/priority.ts:

export const getPriorityClass = (priority: number): string => {
  if (priority >= 8) {
    return 'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400';
  } else if (priority >= 5) {
    return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400';
  } else {
    return 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400';
  }
};

Then import and use it in both components.

🤖 Prompt for AI Agents
In src/features/groups/components/ShutdownPreviewDialog.vue around lines 210 to
218, the getPriorityClass function is duplicated in GroupSelection.vue. To fix
this, create a new shared utility file at src/features/groups/utils/priority.ts
containing the getPriorityClass function. Then, remove the local definition from
both components and import the function from the new utility file to use it in
both places.

Copy link

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

♻️ Duplicate comments (3)
src/features/groups/views/GroupView.vue (3)

310-316: Add TODO comments for placeholder implementations.

These functions are placeholders that only log to console. They should be clearly marked for future implementation.

 const startGroupResources = async (group: Group) => {
+  // TODO: Implement actual resource start logic
   console.log('Starting resources for group:', group.name);
+  // await groupApi.startGroup(group.id);
 };

 const stopGroupResources = async (group: Group) => {
+  // TODO: Implement actual resource stop logic  
   console.log('Stopping resources for group:', group.name);
+  // await groupApi.stopGroup(group.id);
 };

156-162: Remove Math.random() from computed property to prevent UI inconsistencies.

Using Math.random() in a computed property causes the VM state to change on every re-render, breaking Vue's reactivity.

       map[group.id] = (vmGroup.vmIds || []).map(id => ({
         id,
         name: `VM ${id}`,
-        state: Math.random() > 0.5 ? 'active' : 'inactive',
+        state: 'inactive', // TODO: Fetch actual VM states from API or store
       }));

293-295: Fix cascade logic to handle priority gaps.

The current logic only finds groups with exactly priority + 1, which will miss dependent groups if there are gaps in priority values.

     const dependentGroups = groupStore.allGroups.filter(
-      (other: Group) => other.priority === g.priority + 1 && other.cascade
+      (other: Group) => other.priority > g.priority && other.cascade
+    ).sort((a, b) => a.priority - b.priority);
🧹 Nitpick comments (4)
src/features/groups/components/GroupDetailsModal.vue (1)

224-228: Avoid using any type for better type safety.

The room store list should be properly typed to avoid runtime errors and improve code maintainability.

 const roomName = computed(() => {
   if (!props.group?.roomId) return null;
-  const room = roomStore.list?.find((r: any) => r.id === props.group?.roomId);
+  const room = roomStore.list?.find(r => r.id === props.group?.roomId);
   return room?.name;
 });

Ensure the roomStore.list is properly typed in the room store definition.

src/features/groups/components/GroupEditModal.vue (1)

306-308: Missing VM loading implementation.

The TODO comment indicates that VM loading functionality is not implemented yet.

Would you like me to help implement the VM loading logic or create an issue to track this missing functionality?

src/features/groups/components/ResourceSelector.vue (1)

141-141: Remove any type annotation for better type safety.

The rooms computed property should already be properly typed from the room store.

-                  {{ resource.roomId ? rooms.find((r: any) => r.id === resource.roomId)?.name : $t('groups.noRoom') }}
+                  {{ resource.roomId ? rooms.find(r => r.id === resource.roomId)?.name : $t('groups.noRoom') }}
src/features/groups/views/GroupView.vue (1)

184-189: Add user notification for group loading errors.

Errors are silently logged to console without informing the user about the failure.

 const loadGroups = async () => {
   try {
     await groupStore.loadAllGroups();
   } catch (error) {
+    toast.error(t('groups.loadError', { error: error.message }));
     console.error('Failed to load groups:', error);
   }
 };
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 24134b6 and 75f23ad.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • package.json (1 hunks)
  • src/features/groups/components/GroupDetailsModal.vue (1 hunks)
  • src/features/groups/components/GroupEditModal.vue (1 hunks)
  • src/features/groups/components/ResourceSelector.vue (1 hunks)
  • src/features/groups/views/GroupView.vue (1 hunks)
  • src/locales/en.json (5 hunks)
  • src/locales/fr.json (4 hunks)
✅ Files skipped from review due to trivial changes (1)
  • package.json
🧰 Additional context used
🪛 Biome (1.9.4)
src/locales/fr.json

[error] 1129-1130: The key servers was already declared.

This where a duplicated key was declared again.

If a key is defined multiple times, only the last definition takes effect. Previous definitions are ignored.

(lint/suspicious/noDuplicateObjectKeys)


[error] 1130-1130: The key vms was already declared.

This where a duplicated key was declared again.

If a key is defined multiple times, only the last definition takes effect. Previous definitions are ignored.

(lint/suspicious/noDuplicateObjectKeys)

🔇 Additional comments (5)
src/locales/en.json (5)

154-157: 👍 Pagination additions look correct but mind existing duplicates

Keys themselves are fine, however note that "previous" / "next" now exist in at least three namespaces (rooms, servers, pagination).
Keeping identical wording in one shared common/pagination area and referencing it from the UI prevents future drift.
[ suggest_optional_refactor ]


426-435: Potential naming collision with existing "type" / "priority" keys

"type_label" / "priority_label" introduce a second set of labels for the same concepts already exposed via "type" / "priority" a few lines above.
Double-source strings age badly when wording must be updated.

Diff-level fix:

-    "type_label": "Server Type",
-    "priority_label": "Priority Level",
+    "type_label": "@:servers.type",       // reuse existing
+    "priority_label": "@:servers.priority"

(i18n-ally style, adjust to your tooling).
If different wording is really required, rename with a clearer scope like "form_type_label".
[ request_verification ]
[ suggest_essential_refactor ]


940-953: Mixed camelCase / snake_case breaks key-style consistency

The newly added common keys ("loadMore", "view_details" …) switch to camelCase while the rest of the file uses snake_case (search_placeholder, load_more, …).

Pick one convention and stick to it. The same applies to other additions below (searchPlaceholder, createGroup, …).
[ suggest_essential_refactor ]


993-998: Minor: keep history-detail keys orthogonal

"general_info", "metadata", "changes" live under "administration.history_details", while similar concepts also appear in the top-level "history.details" object that follows.
Consider re-using the same keys (or aliasing) to avoid translators having to maintain two copies.
[ suggest_nitpick ]


1046-1155: Duplicate placeholders & inconsistent key style in groups section

  1. "namePlaceholder" / "descriptionPlaceholder" are defined twice – inside groups.form (L1089-1093) and again at root (L1136-1138).
    One of them will be unreachable at runtime. Remove or rename the root-level copies.
@@
-    "namePlaceholder": "Enter group name",
-    "descriptionPlaceholder": "Enter group description (optional)",
  1. This block introduces many camelCase keys (searchPlaceholder, startAll, stopAll …).
    The rest of the codebase predominantly uses snake_case (search_placeholder, load_more).
    Mixing both styles increases cognitive load and complicates automation (e.g. extracting missing keys).

    Prefer aligning with the existing convention, e.g.:

-    "searchPlaceholder": "Search groups...",
+    "search_placeholder": "Search groups...",
-    "startAll": "Start All",
+    "start_all": "Start All",
  1. Optional – you could move generic strings such as "start", "stop", "viewDetails" to the common namespace to avoid future duplication.

Failing to address (1) will silently override one of the definitions at JSON-parse time – hard to track down later.
[ raise_critical_issue ]

…roups

- Handle empty string serverGroupId by converting to undefined in GroupEditModal
- Prevent sending null or empty serverGroupId values in API payload
- Fix VM group creation to properly associate with parent server group
- Initialize serverGroupId as empty string instead of undefined
- Exclude serverGroupId from payload when empty to avoid sending null
- Ensure consistent handling across create and edit modes
Copy link

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

♻️ Duplicate comments (1)
src/features/groups/components/GroupEditModal.vue (1)

277-277: Fix type assertion to use proper type narrowing.

The past review comment correctly identified this type assertion issue. Instead of using any, use proper type narrowing with the imported types.

Apply this fix:

-      serverGroupId: group.type === 'vm' ? (group as any).serverGroupId : undefined,
+      serverGroupId: group.type === 'vm' ? (group as GroupVmResponseDto).serverGroupId : undefined,
🧹 Nitpick comments (2)
src/features/groups/api.ts (2)

20-26: Use optional chaining for safer property access.

The static analysis correctly suggests using optional chaining for more concise and safer code.

Apply this improvement:

-  if (response.data && response.data.items) {
-    response.data.items = response.data.items.map((item) => ({
+  response.data?.items && (response.data.items = response.data.items.map((item) => ({
       ...item,
       type: 'server' as const,
-    }));
-  }
+    })));

33-39: Use optional chaining for safer property access.

Same improvement applies here for consistency and safety.

Apply this improvement:

-  if (response.data && response.data.items) {
-    response.data.items = response.data.items.map((item) => ({
+  response.data?.items && (response.data.items = response.data.items.map((item) => ({
       ...item,
       type: 'vm' as const,
-    }));
-  }
+    })));
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 75f23ad and d2c2d40.

📒 Files selected for processing (5)
  • src/features/groups/api.ts (1 hunks)
  • src/features/groups/components/GroupEditModal.vue (1 hunks)
  • src/features/groups/components/ResourceSelector.vue (1 hunks)
  • src/features/groups/types.ts (1 hunks)
  • src/locales/fr.json (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/features/groups/types.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/features/groups/api.ts (1)
src/features/groups/types.ts (11)
  • ServerGroupListParams (65-70)
  • GroupServerListResponseDto (28-33)
  • VmGroupListParams (72-75)
  • GroupVmListResponseDto (35-40)
  • ShutdownRequestDto (61-63)
  • ShutdownPreviewParams (77-80)
  • ShutdownPreviewListResponseDto (52-59)
  • GroupServerResponseDto (4-14)
  • GroupVmResponseDto (16-26)
  • CreateGroupPayload (82-91)
  • UpdateGroupPayload (93-101)
🪛 Biome (1.9.4)
src/features/groups/api.ts

[error] 20-20: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 33-33: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (8)
src/features/groups/components/GroupEditModal.vue (1)

1-393: Well-implemented group management component.

The component demonstrates excellent Vue 3 practices with proper TypeScript typing, comprehensive form handling, internationalization, and accessibility features. The reactive state management and event handling are well-structured.

src/features/groups/components/ResourceSelector.vue (1)

1-254: Excellent resource selector implementation.

This component showcases strong Vue 3 practices with:

  • Proper TypeScript typing and interfaces
  • Comprehensive accessibility support
  • Responsive design with view mode switching
  • Efficient filtering and selection logic
  • Good internationalization integration

The component is well-structured and follows best practices.

src/features/groups/api.ts (1)

42-191: Well-structured API module with comprehensive functionality.

The API module provides excellent type safety and comprehensive group management capabilities. The endpoint design, payload transformation, and error handling are well-implemented.

src/locales/fr.json (5)

154-158: Approve new pagination translations.

The new pagination translations are properly integrated and provide good French localization for the pagination controls.


216-218: Approve new UPS efficiency and maintenance translations.

These new keys provide necessary localization for enhanced UPS monitoring features.


942-949: Approve new common action translations.

These common UI action translations enhance the internationalization support across the application.


997-1041: Comprehensive history event localization.

The new history namespace provides excellent French translations for user actions, descriptions, and details. The structured approach with actions, descriptions, and details subsections is well-organized.


1042-1151: Excellent groups feature localization.

The comprehensive groups namespace provides complete French translations for all group management features including forms, actions, shutdown sequences, and resource management. The translations are well-structured and maintain consistency with the application's terminology.

…mation modal

- Create DeleteConfirmModal component with proper UI/UX
- Add warning for groups containing resources
- Support async deletion with loading state
- Add French and English translations for modal content
- Fix issue where users with disabled alerts wouldn't see confirmation
- Load virtual machines instead of physical servers when editing VM groups
- Call loadResources() when opening edit modal to ensure proper resource filtering
- Fix resource type filtering based on group type
- Remove !isEditing condition to allow parent group selection in edit mode
- Enable users to change the parent server group of existing VM groups
Copy link

@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

🔭 Outside diff range comments (1)
src/locales/fr.json (1)

1-1342: Duplicate keys detected in JSON files – please remove collisions

The audit script uncovered duplicate property names that will silently override earlier entries:

• src/locales/fr.json: duplicate keys → action, actions, active, active_servers, activity, admin, admin_role, admin_url_label, admin_url_placeholder, all_loaded, all_rooms, apply, auth, back, brand_label, brand_other, brand_select, cancel, capacity_hint, capacity_label, capacity_placeholder, card_view, cascade, changelog, connected_servers, cooling_air, cooling_hybrid, cooling_liquid, create, create_button, create_description, create_modal_subtitle, create_title, creating, creation_error, creation_info, dashboard, date, default_room_main, default_room_setup, delete, description, descriptionPlaceholder, details, edit, edit_role, edit_title, email, entity, error, errors, filters, finish, form, general_title, grace_off, grace_off_hint, grace_off_label, grace_off_placeholder, grace_on, grace_on_hint, grace_on_label, grace_on_placeholder, grid, group, groups, headers, history, inactive, ip, ip_address, ip_hint, ip_invalid, ip_label, ip_placeholder, list, list_title, load_rooms_error, loading, loading_rooms, location_hint, location_placeholder, location_title, login, login_placeholder, login_required, login_success, logout, member_since, model_label, model_placeholder, monitoring, monitoring_coming_soon, monitoring_placeholder, name, namePlaceholder, name_hint, name_label, name_placeholder, name_required, negative_delay_error, network_title, next, noRoom, no_results, no_roles, no_room, no_servers_found, no_ups_found, offline, online, overview, page, password, password_placeholder, password_required, preferences, previous, priority, priority_hint, priority_label, profile, register_success, reset_password, results, role, role_added, roles, room, room_created, room_label, rooms, runtime_text, runtime_title, save, search_placeholder, security, selectAll, select_role, select_room, select_room_error, select_room_hint, select_ups_hint, selected, selected_id, selected_name, server_capacity_text, server_capacity_title, server_created, servers, settings, settings_extra, shutdown, start, state, stats, stop, submit, submitting, subtitle, table_view, tabs, temperature, timeline, title, total, total_servers, try_different_search, type, type_label, type_physical, type_virtual, unexpected_error, unknown, ups, ups_created, ups_label, ups_units, user, user_roles, username, username_placeholder, users, view, view_details, virtual_machines, vms, warning, welcome_title

• src/locales/en.json: same duplicate keys as above

• biome.json: duplicate keys → all, enabled, linter, parser, recommended

• tsconfig.json: duplicate key → path

Please dedupe these entries so that each JSON key is unique and earlier values aren’t inadvertently overridden at runtime.

♻️ Duplicate comments (1)
src/locales/en.json (1)

1038-1046: Repeated camelCase keys in common block – naming-style issue still unresolved

The earlier review (see past comments) already flagged loadMore, selectAll, selectNone as camelCase outliers in a snake_case file.
They are still here and will keep tripping i18n-lint / grep.

-    "loadMore": "Load More",
-    "selectAll": "Select All",
-    "selectNone": "Select None",
+    "load_more": "Load more",
+    "select_all": "Select all",
+    "select_none": "Select none",

(Apply the same rule to any future additions.)

🧹 Nitpick comments (3)
src/locales/fr.json (3)

1178-1291: Consider splitting the new “groups” block into a dedicated locale file (or deeper nesting).

The groups section just added is ~110 keys long. Keeping everything in one 1-3000-line JSON file makes duplicate-key mistakes and merge conflicts almost inevitable.
Most i18n loaders (vue-i18n, i18next, lingui, …) let you load per-namespace JSON files, e.g.

src/locales/fr/
  common.json
  groups.json   ← move this section
  history.json

No functional issue, but refactoring now will pay off when further group-related strings are added.


555-579: Key naming in settings_extra is drifting – normalise before it calcifies.

Inside settings_extra we now have
two_factor, two_factor_enabled, two_factor_disabled.

Unless the UI really needs three distinct keys, collapse to one sub-object for clarity:

"two_factor": {
  "label": "Authentification à deux facteurs",
  "enabled": "activée",
  "disabled": "désactivée"
}

Reduces mental load and avoids translators having to remember subtle variations.


172-176: French wording for pagination “showing” feels awkward.

"showing": "Affichage" is literal and will read like “Affichage 1–20 sur 53”.
"affichage" is a noun; what you need is a verb phrase, e.g.

-"showing": "Affichage"
+"showing": "Affiche"

or "Affichage de" depending on sentence construction.
Minor UX polish.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f47dae5 and d834dee.

📒 Files selected for processing (14)
  • src/components/charts/BaseChart.vue (0 hunks)
  • src/components/ui/StatCard.vue (0 hunks)
  • src/features/ilos/views/HelloWorld.vue (0 hunks)
  • src/features/profile/components/ProfileDangerZone.vue (0 hunks)
  • src/features/profile/components/ProfileHeader.vue (0 hunks)
  • src/features/profile/components/ProfileInfoCard.vue (0 hunks)
  • src/features/profile/components/ProfileSecurityCard.vue (0 hunks)
  • src/features/profile/components/ResetPasswordModal.vue (1 hunks)
  • src/features/rooms/views/CreateRoom.vue (0 hunks)
  • src/features/ups/views/CreateUps.vue (0 hunks)
  • src/features/vms/views/HelloWorld.vue (0 hunks)
  • src/locales/en.json (9 hunks)
  • src/locales/fr.json (9 hunks)
  • src/router/index.ts (1 hunks)
💤 Files with no reviewable changes (10)
  • src/features/ilos/views/HelloWorld.vue
  • src/features/vms/views/HelloWorld.vue
  • src/features/rooms/views/CreateRoom.vue
  • src/components/ui/StatCard.vue
  • src/features/profile/components/ProfileHeader.vue
  • src/features/profile/components/ProfileSecurityCard.vue
  • src/components/charts/BaseChart.vue
  • src/features/ups/views/CreateUps.vue
  • src/features/profile/components/ProfileDangerZone.vue
  • src/features/profile/components/ProfileInfoCard.vue
✅ Files skipped from review due to trivial changes (1)
  • src/features/profile/components/ResetPasswordModal.vue
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/router/index.ts

Comment on lines 1176 to 1217
"groups": {
"title": "Groups",
"description": "Manage server and VM groups with priority-based orchestration",
"searchPlaceholder": "Search groups...",
"allTypes": "All Types",
"serverGroups": "Server Groups",
"vmGroups": "VM Groups",
"allPriorities": "All Priorities",
"priority1": "Priority 1",
"priority2": "Priority 2",
"priority3": "Priority 3",
"priority4": "Priority 4",
"priorityDesc1": "Critical",
"priorityDesc2": "High",
"priorityDesc3": "Medium",
"priorityDesc4": "Low",
"createGroup": "Create Group",
"noGroups": "No groups found",
"viewDetails": "View Details",
"start": "Start",
"stop": "Stop",
"startAll": "Start All",
"stopAll": "Stop All",
"type": "Type",
"priority": "Priority",
"cascade": "Cascade",
"cascadeEnabled": "Cascade enabled",
"cascadeDisabled": "Cascade disabled",
"resources": "Resources",
"noResources": "No resources assigned",
"room": "Room",
"serverGroup": "Server Group",
"vmGroup": "VM Group",
"flowView": "Flow View",
"fitView": "Fit View",
"toggleOrientation": "Toggle Orientation",
"view": {
"grid": "Grid View",
"list": "List View",
"sections": "Sections View",
"flow": "Flow View"
},
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

groups section introduces a wave of camelCase keys – please switch to snake_case before this spreads

Almost every key in the new groups object (searchPlaceholder, allTypes, priorityDesc1, …) deviates from the established snake_case convention used in the rest of the file. Mixing styles hurts DX and guarantees duplicated keys down the road (cf. view_details / viewDetails déjà-vu).

Representative fix:

-    "searchPlaceholder": "Search groups...",
-    "allTypes": "All Types",
-    "serverGroups": "Server Groups",
-    "vmGroups": "VM Groups",
-    "allPriorities": "All Priorities",
+    "search_placeholder": "Search groups...",
+    "all_types": "All types",
+    "server_groups": "Server groups",
+    "vm_groups": "VM groups",
+    "all_priorities": "All priorities",

Before merging, run an i18n-linter (or simple script) to enforce one naming style across the whole file.

📝 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
"groups": {
"title": "Groups",
"description": "Manage server and VM groups with priority-based orchestration",
"searchPlaceholder": "Search groups...",
"allTypes": "All Types",
"serverGroups": "Server Groups",
"vmGroups": "VM Groups",
"allPriorities": "All Priorities",
"priority1": "Priority 1",
"priority2": "Priority 2",
"priority3": "Priority 3",
"priority4": "Priority 4",
"priorityDesc1": "Critical",
"priorityDesc2": "High",
"priorityDesc3": "Medium",
"priorityDesc4": "Low",
"createGroup": "Create Group",
"noGroups": "No groups found",
"viewDetails": "View Details",
"start": "Start",
"stop": "Stop",
"startAll": "Start All",
"stopAll": "Stop All",
"type": "Type",
"priority": "Priority",
"cascade": "Cascade",
"cascadeEnabled": "Cascade enabled",
"cascadeDisabled": "Cascade disabled",
"resources": "Resources",
"noResources": "No resources assigned",
"room": "Room",
"serverGroup": "Server Group",
"vmGroup": "VM Group",
"flowView": "Flow View",
"fitView": "Fit View",
"toggleOrientation": "Toggle Orientation",
"view": {
"grid": "Grid View",
"list": "List View",
"sections": "Sections View",
"flow": "Flow View"
},
"groups": {
"title": "Groups",
"description": "Manage server and VM groups with priority-based orchestration",
"search_placeholder": "Search groups...",
"all_types": "All types",
"server_groups": "Server groups",
"vm_groups": "VM groups",
"all_priorities": "All priorities",
"priority1": "Priority 1",
"priority2": "Priority 2",
"priority3": "Priority 3",
"priority4": "Priority 4",
"priorityDesc1": "Critical",
"priorityDesc2": "High",
"priorityDesc3": "Medium",
"priorityDesc4": "Low",
"createGroup": "Create Group",
"noGroups": "No groups found",
"viewDetails": "View Details",
"start": "Start",
"stop": "Stop",
"startAll": "Start All",
"stopAll": "Stop All",
"type": "Type",
"priority": "Priority",
"cascade": "Cascade",
"cascadeEnabled": "Cascade enabled",
"cascadeDisabled": "Cascade disabled",
"resources": "Resources",
"noResources": "No resources assigned",
"room": "Room",
"serverGroup": "Server Group",
"vmGroup": "VM Group",
"flowView": "Flow View",
"fitView": "Fit View",
"toggleOrientation": "Toggle Orientation",
"view": {
"grid": "Grid View",
"list": "List View",
"sections": "Sections View",
"flow": "Flow View"
}
},
🤖 Prompt for AI Agents
In src/locales/en.json between lines 1176 and 1217, the keys in the "groups"
section use camelCase naming like "searchPlaceholder" and "priorityDesc1" which
conflicts with the established snake_case convention in the file. To fix this,
rename all camelCase keys in this section to snake_case equivalents (e.g.,
"search_placeholder", "priority_desc1") ensuring consistency. After renaming,
run an i18n-linter or a script to verify that the entire file uniformly uses
snake_case keys to prevent duplication and maintain developer experience.

jabibamman added 10 commits July 4, 2025 17:54
- Add Props interface with isReadOnly property to CreateServerStep, VmDiscoveryStep, and CompleteStep components
- Disable all form inputs when isReadOnly is true using :disabled prop binding
- Replace submit buttons with read-only messages when in read-only mode
- Load saved data from store when components mount in read-only mode
- Update VmTable component to hide edit buttons in read-only mode
- Save VM discovery data to store for later retrieval
- Add i18n translations for read-only messages in English locale

This allows users to view but not modify data when navigating back to previous setup steps
Copy link

@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

🔭 Outside diff range comments (1)
src/features/setup/components/steps/VmDiscoveryStep.vue (1)

118-136: Move mock data loading inside the component lifecycle

The mock VM loading logic is executing at the module level, which causes timing issues. The setTimeout calls are registered immediately when the module loads, before the component is mounted and props are available. This means the mock data will be added even when isReadOnly is true.

Move the mock data loading inside onMounted to ensure it respects the isReadOnly prop:

-const MOCK_VMS = [
-  { id: 'vm-1', name: 'PROD-WEB-01', ip: '192.168.1.201', state: 'active' },
-  { id: 'vm-2', name: 'DB-INT-02', ip: '192.168.1.202', state: 'inactive' },
-  { id: 'vm-3', name: 'APP-BACK-03', ip: '192.168.1.203', state: 'active' },
-];
-
-// Only load mock data if not in read-only mode
-if (!props.isReadOnly) {
-  setTimeout(() => {
-    vms.value.push(MOCK_VMS[0]);
-  }, 1000);
-  setTimeout(() => {
-    vms.value.push(MOCK_VMS[1]);
-  }, 1700);
-  setTimeout(() => {
-    vms.value.push(MOCK_VMS[2]);
-    isLoading.value = false;
-  }, 2300);
-}

 onMounted(async () => {
   if (!setupStore.setupStatus) {
     await setupStore.checkSetupStatus();
   }

   const steps = await setupStore.getSetupProgress();
   const serverStep = steps.find(
     (step: any) => step.step === SetupStep.CREATE_SERVER,
   );

   serverId.value = serverStep?.metadata?.id ?? null;
   if (!serverId.value) {
     isLoading.value = false;
     toast.error(t('vm_discovery.no_server_error'));
     router.push('/setup/create-server');
     return;
   }

   // Load saved data if in read-only mode
   if (props.isReadOnly) {
     const vmData = setupStore.getStepData(SetupStep.VM_DISCOVERY);
     if (vmData && vmData.vms) {
       vms.value = vmData.vms;
       isLoading.value = false;
       return;
     }
   }
+  
+  // Only load mock data if not in read-only mode
+  if (!props.isReadOnly) {
+    const MOCK_VMS = [
+      { id: 'vm-1', name: 'PROD-WEB-01', ip: '192.168.1.201', state: 'active' },
+      { id: 'vm-2', name: 'DB-INT-02', ip: '192.168.1.202', state: 'inactive' },
+      { id: 'vm-3', name: 'APP-BACK-03', ip: '192.168.1.203', state: 'active' },
+    ];
+    
+    setTimeout(() => {
+      vms.value.push(MOCK_VMS[0]);
+    }, 1000);
+    setTimeout(() => {
+      vms.value.push(MOCK_VMS[1]);
+    }, 1700);
+    setTimeout(() => {
+      vms.value.push(MOCK_VMS[2]);
+      isLoading.value = false;
+    }, 2300);
+  }
 });
♻️ Duplicate comments (3)
src/locales/en.json (2)

1066-1079: Inconsistent key naming style persists in common block

The mixing of camelCase and snake_case conventions was already flagged in a previous review. This inconsistency still needs to be addressed.


1209-1250: groups section still uses camelCase keys inconsistently

The camelCase naming convention in the groups namespace was already flagged in a previous review. This needs to be converted to snake_case for consistency.

src/locales/fr.json (1)

1307-1308: Critical: Duplicate JSON keys will cause translation failures

The keys "servers" and "vms" are defined multiple times within the groups namespace. Only the last definition will be used, causing translation issues.

Either remove these duplicate keys if they're redundant, or rename them to more specific keys:

-    "servers": "Serveurs",
-    "vms": "Machines virtuelles",
+    "resourceServers": "Serveurs",
+    "resourceVms": "Machines virtuelles",
🧹 Nitpick comments (3)
src/features/setup/components/steps/CreateUpsStep.vue (1)

469-507: Resolve TODO comments before merging

There are several TODO comments that need to be addressed:

  1. Whether to include brand, model, and capacity fields in the creation DTO (lines 477-480)
  2. Which fields to send to the backend (line 489)
  3. Navigation handling after UPS creation (line 496)

These implementation details should be clarified with the backend team and product requirements.

Would you like me to help implement the proper field handling based on the backend API requirements?

src/features/setup/components/steps/CreateServerStep.vue (1)

614-616: Replace console.error with proper error logging

Using console.error directly is not recommended for production code. Consider using a centralized logging service or error tracking solution.

Replace console.error with a logging service:

   } catch (error) {
-    console.error('Erreur lors du chargement des salles:', error);
+    // Use your logging service
+    // logger.error('Failed to load rooms', { error });
     if (roomData.id) {

And:

   } catch (error) {
-    console.error('Erreur lors du chargement des UPS:', error);
+    // Use your logging service
+    // logger.error('Failed to load UPS', { error });
     if (upsData.id) {

Also applies to: 639-641

src/features/setup/components/SetupProgress.vue (1)

205-214: Consider allowing navigation to all previous steps

The current implementation only allows navigation back to the welcome step. Since previous steps are displayed in read-only mode, users should be able to review all their previous configurations.

Allow navigation to any previous step:

 const handleStepClick = (stepKey: string, index: number) => {
   // Allow navigation to current step
   if (index === currentStepIndex.value) {
     emit('go-to-step', stepKey);
   }
-  // Allow navigation to welcome step only among previous steps
-  else if (index < currentStepIndex.value && stepKey === 'welcome') {
+  // Allow navigation to any previous step
+  else if (index < currentStepIndex.value) {
     emit('go-to-step', stepKey);
   }
 };
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d834dee and 282b833.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (16)
  • package.json (2 hunks)
  • src/features/auth/components/AuthInput.vue (3 hunks)
  • src/features/profile/components/ResetPasswordModal.vue (1 hunks)
  • src/features/rooms/views/RoomListView.vue (8 hunks)
  • src/features/setup/components/SetupProgress.vue (1 hunks)
  • src/features/setup/components/steps/CompleteStep.vue (14 hunks)
  • src/features/setup/components/steps/CreateRoomStep.vue (7 hunks)
  • src/features/setup/components/steps/CreateServerStep.vue (24 hunks)
  • src/features/setup/components/steps/CreateUpsStep.vue (23 hunks)
  • src/features/setup/components/steps/VmDiscoveryStep.vue (8 hunks)
  • src/features/setup/components/steps/VmTable.vue (4 hunks)
  • src/features/setup/components/steps/WelcomeStep.vue (1 hunks)
  • src/features/setup/router/router.ts (0 hunks)
  • src/features/setup/views/SetupView.vue (1 hunks)
  • src/locales/en.json (17 hunks)
  • src/locales/fr.json (14 hunks)
💤 Files with no reviewable changes (1)
  • src/features/setup/router/router.ts
✅ Files skipped from review due to trivial changes (2)
  • src/features/rooms/views/RoomListView.vue
  • src/features/auth/components/AuthInput.vue
🚧 Files skipped from review as they are similar to previous changes (5)
  • package.json
  • src/features/setup/components/steps/VmTable.vue
  • src/features/setup/components/steps/CompleteStep.vue
  • src/features/setup/components/steps/CreateRoomStep.vue
  • src/features/profile/components/ResetPasswordModal.vue
🔇 Additional comments (3)
src/features/setup/components/steps/WelcomeStep.vue (3)

1-119: Well-structured template with excellent UX enhancements!

The refactor from static cards to dynamic feature rendering is clean and maintainable. Good use of animations, gradients, and conditional rendering based on the isReadOnly prop.


121-217: Clean script implementation with proper TypeScript usage!

The utility functions are well-implemented, and the component properly handles the setup flow navigation. Good use of Vue 3 Composition API patterns.


219-265: Smooth animations enhance the user experience!

The CSS animations are well-crafted with appropriate timing and transforms. The scoped styles ensure no conflicts with other components.

Comment on lines +5 to +7
<div
class="fixed inset-0 bg-[url('/grid.svg')] bg-center opacity-[0.02] dark:opacity-[0.04] pointer-events-none"
></div>
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

Verify the grid.svg asset exists

The background references a hardcoded /grid.svg path. Ensure this asset exists in the public directory to avoid 404 errors.


🏁 Script executed:

#!/bin/bash
# Description: Check if grid.svg exists in common public asset locations

# Check for grid.svg in public directory
fd -t f "grid.svg" public/

# Check for grid.svg in src/assets
fd -t f "grid.svg" src/assets/

# Check for any svg files that might be the grid pattern
fd -e svg -t f | rg -i "grid"

Length of output: 87


grid.svg asset is missing

I ran searches against public/ and src/assets/ and didn’t find any grid.svg file, which will lead to 404s at runtime. Please ensure the asset exists or update the reference:

• Add grid.svg under public/ (so it’s served at "/grid.svg")
• Or place it in src/assets/ and update the URL (e.g. background-image: url('@/assets/grid.svg'))
• If the file lives elsewhere, adjust the path accordingly

🤖 Prompt for AI Agents
In src/features/setup/views/SetupView.vue around lines 5 to 7, the background
image references a missing grid.svg asset, causing 404 errors at runtime. Fix
this by either adding the grid.svg file to the public/ directory so it is
accessible at "/grid.svg", or move the file to src/assets/ and update the URL in
the background-image style to use the correct relative path such as
"@/assets/grid.svg". Ensure the path matches the actual location of the asset.

Copy link

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

🧹 Nitpick comments (1)
src/features/profile/views/ProfileView.vue (1)

1-624: Consider breaking down this large component.

This component is quite large (600+ lines) and handles multiple responsibilities:

  • User profile display
  • Tab navigation
  • Multiple modals
  • Activity management
  • Stats computation

Consider extracting some functionality into smaller, focused components or composables to improve maintainability.

Potential extractions:

  • useProfileTabs composable for tab management
  • ProfileOverviewTab, ProfileSecurityTab, etc. as separate components
  • useUserStats composable for stats computation
  • useUserActivities composable for activity management
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 282b833 and 4d17d4e.

📒 Files selected for processing (5)
  • src/composables/useApiCache.ts (1 hunks)
  • src/features/auth/store.ts (2 hunks)
  • src/features/dashboard/store.ts (1 hunks)
  • src/features/dashboard/views/DashboardView.vue (5 hunks)
  • src/features/profile/views/ProfileView.vue (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/features/dashboard/views/DashboardView.vue
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/features/dashboard/store.ts (3)
src/features/dashboard/types.ts (1)
  • FullDashboardStatsDto (1-15)
src/composables/useApiCache.ts (1)
  • useApiCache (6-84)
src/features/dashboard/api.ts (1)
  • dashboardApi (20-85)
🔇 Additional comments (8)
src/features/auth/store.ts (2)

82-91: LGTM! Caching implementation improves performance.

The addition of the force parameter and caching logic prevents redundant API calls when the user data is already available. The implementation correctly handles the token validation and maintains backward compatibility.


203-203: Good refactoring to reuse cached user fetching logic.

The update to use fetchCurrentUser() instead of directly calling getMe() ensures consistent user data handling and leverages the new caching mechanism.

src/composables/useApiCache.ts (1)

1-84: Excellent caching composable implementation.

This is a well-designed generic caching utility with the following strengths:

  • Clean API with proper TypeScript generics
  • Automatic TTL-based expiration with cleanup
  • Convenient withCache method for cache-or-fetch patterns
  • Configurable TTL per operation
  • Memory-efficient with automatic cleanup of expired entries

The 5-minute default TTL is appropriate for most API data scenarios.

src/features/dashboard/store.ts (3)

5-5: Good integration of the caching composable.

The import and usage of the API cache composable aligns well with the overall caching strategy introduced in this PR.


12-26: Effective caching implementation for dashboard stats.

The 2-minute cache TTL is appropriate for dashboard statistics, balancing freshness with performance. The force parameter provides flexibility for cache invalidation when needed.


34-42: Smart dynamic cache key strategy for history data.

The cache key construction dashboard-history-${entity}-${months} ensures proper cache isolation for different entity types and time periods. The 5-minute TTL is reasonable for historical data.

src/features/profile/views/ProfileView.vue (2)

5-5: Good integration of presence store.

The presence store integration provides real-time online/offline status for users, enhancing the profile view functionality.


138-149: Good presence integration in onMounted.

The presence fetching logic is well-implemented with proper error handling and loading state management.

Comment on lines +110 to +136
const stats = computed(() => {
if (!user.value) return [];

const createdDate = new Date(user.value.createdAt);
const daysSinceCreation = Math.floor(
(Date.now() - createdDate.getTime()) / (1000 * 60 * 60 * 24),
);

return [
{
label: t('profile.stats.member_since'),
value: daysSinceCreation + ' ' + t('profile.stats.days'),
},
{
label: t('profile.stats.total_logins'),
value: '157',
},
{
label: t('profile.stats.last_login'),
value: t('profile.stats.today'),
},
{
label: t('profile.stats.security_score'),
value: user.value.isTwoFactorEnabled ? '95%' : '60%',
},
];
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider extracting stats computation logic.

The stats computation logic is complex and contains hardcoded values like '157' for total logins. Consider extracting this into a separate composable and making the data dynamic.

-const stats = computed(() => {
-  if (!user.value) return [];
-  // ... complex computation with hardcoded values
-});
+const { stats, loading: statsLoading } = useUserStats(user);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/features/profile/views/ProfileView.vue around lines 110 to 136, the stats
computation logic is complex and includes hardcoded values such as '157' for
total logins. Extract this logic into a separate composable function to improve
code organization and reusability. Modify the composable to accept dynamic data
inputs for total logins and other stats instead of hardcoded values, ensuring
the stats are computed based on actual user data.

Comment on lines 163 to 172
const handleToggle2FA = async () => {
if (!user.value) return;
if (user.value.isTwoFactorEnabled) {
if (await auth.disable2FAUser()) user.value.isTwoFactorEnabled = false;
if (await auth.disable2FAUser()) {
toast.success(t('toast.twofa_disabled'));
}
} else {
router.push({ name: 'Enable2FA' });
}
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Handle potential errors in 2FA toggle.

The 2FA toggle logic should have error handling for the case where disable2FAUser() fails.

const handleToggle2FA = async () => {
  if (!user.value) return;
-  if (user.value.isTwoFactorEnabled) {
-    if (await auth.disable2FAUser()) {
-      toast.success(t('toast.twofa_disabled'));
-    }
-  } else {
-    router.push({ name: 'Enable2FA' });
-  }
+  try {
+    if (user.value.isTwoFactorEnabled) {
+      if (await auth.disable2FAUser()) {
+        toast.success(t('toast.twofa_disabled'));
+      }
+    } else {
+      router.push({ name: 'Enable2FA' });
+    }
+  } catch (error: any) {
+    toast.error(error.message || t('toast.twofa_error'));
+  }
};
📝 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
const handleToggle2FA = async () => {
if (!user.value) return;
if (user.value.isTwoFactorEnabled) {
if (await auth.disable2FAUser()) user.value.isTwoFactorEnabled = false;
if (await auth.disable2FAUser()) {
toast.success(t('toast.twofa_disabled'));
}
} else {
router.push({ name: 'Enable2FA' });
}
};
const handleToggle2FA = async () => {
if (!user.value) return;
try {
if (user.value.isTwoFactorEnabled) {
if (await auth.disable2FAUser()) {
toast.success(t('toast.twofa_disabled'));
}
} else {
router.push({ name: 'Enable2FA' });
}
} catch (error: any) {
toast.error(error.message || t('toast.twofa_error'));
}
};
🤖 Prompt for AI Agents
In src/features/profile/views/ProfileView.vue around lines 163 to 172, the
handleToggle2FA function lacks error handling for the async call to
disable2FAUser(). Update the function to catch any errors thrown by
disable2FAUser(), and handle them appropriately, such as showing an error toast
message or logging the error, to ensure the UI responds correctly if disabling
2FA fails.

Comment on lines +72 to +108
const mockActivities = ref([
{
id: '1',
type: 'login' as const,
title: t('profile.activity.login'),
description: t('profile.activity.login_desc'),
timestamp: new Date(Date.now() - 1000 * 60 * 30),
metadata: {
ip: '192.168.1.100',
browser: 'Chrome 120.0',
},
},
{
id: '2',
type: 'profile_update' as const,
title: t('profile.activity.profile_update'),
description: t('profile.activity.profile_update_desc'),
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 2),
},
{
id: '3',
type: 'password_change' as const,
title: t('profile.activity.password_change'),
description: t('profile.activity.password_change_desc'),
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24),
},
{
id: '4',
type: 'failed_login' as const,
title: t('profile.activity.failed_login'),
description: t('profile.activity.failed_login_desc'),
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 48),
metadata: {
ip: '10.0.0.50',
},
},
]);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Replace hardcoded mock activity data with dynamic data.

The mock activities are hardcoded with static data, which is not suitable for production. This should be replaced with actual user activity data from an API.

Consider creating a user activity API endpoint and composable:

-const mockActivities = ref([
-  {
-    id: '1',
-    type: 'login' as const,
-    title: t('profile.activity.login'),
-    description: t('profile.activity.login_desc'),
-    timestamp: new Date(Date.now() - 1000 * 60 * 30),
-    metadata: {
-      ip: '192.168.1.100',
-      browser: 'Chrome 120.0',
-    },
-  },
-  // ... other hardcoded activities
-]);
+const { activities, loading: activitiesLoading, fetchActivities } = useUserActivities();
+
+onMounted(async () => {
+  if (user.value?.id) {
+    await fetchActivities(user.value.id);
+  }
+});

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/features/profile/views/ProfileView.vue between lines 72 and 108, replace
the hardcoded mockActivities array with dynamic data fetched from an API. Create
a user activity API endpoint to retrieve real user activity data and implement a
composable function to call this API and manage the state. Update the component
to use this composable to load and display actual user activities instead of
static mock data.

Copy link

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

♻️ Duplicate comments (7)
src/features/groups/components/panel/ViewModeContent.vue (1)

344-348: Replace browser confirm() with proper confirmation handling.

Using the browser's native confirm() dialog provides poor UX and doesn't match the application's design system. This was also flagged in previous reviews for other components.

Consider emitting a confirmation request event instead:

 const confirmDelete = () => {
-  if (confirm(t('groups.deleteConfirmMessage', { name: props.group.name }))) {
-    emit('delete', props.group);
-  }
+  emit('delete', props.group);
 };

The parent component should handle the confirmation with a proper modal dialog to ensure consistent UX across the application.

src/features/groups/components/GroupCard.vue (1)

151-157: Fix hardcoded strings for proper internationalization.

The resourceLabel computed property still uses hardcoded English strings ("server", "servers", "VM", "VMs"), which was flagged in previous reviews but appears unresolved.

Import the i18n composable and use proper translation keys:

+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();

 const resourceLabel = computed(() => {
   const count = resourceCount.value;
   if (props.group.type === 'SERVER') {
-    return count === 1 ? 'server' : 'servers';
+    return t('servers.label', count);
   }
-  return count === 1 ? 'VM' : 'VMs';
+  return t('vms.label', count);
 });

Ensure the translation keys support pluralization in your i18n configuration.

src/features/groups/components/GroupManagementPanel.vue (1)

318-331: Replace browser confirm() with proper confirmation modal.

Using the browser's native confirm() dialog provides poor UX and doesn't match the application's design system. This pattern should be consistent across the application.

Consider using a proper confirmation modal component:

 const setMode = async (newMode: PanelMode) => {
   if (newMode === 'view' && hasChanges.value) {
-    if (!confirm(t('common.unsavedChangesConfirm'))) {
-      return;
-    }
+    // Emit event for parent to handle confirmation
+    emit('confirm-mode-change', { newMode, hasChanges: true });
+    return;
   }
   mode.value = newMode;
   // ... rest of logic
 };
src/features/groups/views/GroupView.vue (2)

289-295: Add TODO comments for placeholder implementations.

These functions are placeholders that only log to console. They should be clearly marked for future implementation as suggested in previous reviews.

 const startGroupResources = async (group: GroupResponseDto) => {
+  // TODO: Implement actual resource start logic
   console.log('Starting resources for group:', group.name);
+  // await groupApi.startGroup(group.id);
 };

 const stopGroupResources = async (group: GroupResponseDto) => {
+  // TODO: Implement actual resource stop logic  
   console.log('Stopping resources for group:', group.name);
+  // await groupApi.stopGroup(group.id);
 };

182-188: Improve error handling with user feedback.

The error is silently logged to console without informing the user. Consider adding a toast notification to improve user experience.

 const loadGroups = async () => {
   try {
     await groupStore.fetchAllGroups();
   } catch (error) {
     console.error('Failed to load groups:', error);
+    toast.error(t('groups.loadError'));
   }
 };
src/locales/en.json (2)

1216-1349: Fix camelCase keys to maintain consistency.

The new groups section introduces many camelCase keys (searchPlaceholder, allTypes, serverGroups, priorityDesc1, etc.) which conflicts with the established snake_case convention used throughout the file. This was flagged in previous reviews but hasn't been addressed.

Representative fix for the most common violations:

-    "searchPlaceholder": "Search groups...",
-    "allTypes": "All Types",
-    "serverGroups": "Server Groups", 
-    "vmGroups": "VM Groups",
-    "allPriorities": "All Priorities",
-    "priorityDesc1": "Critical",
-    "createGroup": "Create Group",
-    "noGroups": "No groups found",
-    "viewDetails": "View Details",
-    "startAll": "Start All",
-    "stopAll": "Stop All",
+    "search_placeholder": "Search groups...",
+    "all_types": "All types",
+    "server_groups": "Server groups",
+    "vm_groups": "VM groups", 
+    "all_priorities": "All priorities",
+    "priority_desc1": "Critical",
+    "create_group": "Create group",
+    "no_groups": "No groups found",
+    "view_details": "View details",
+    "start_all": "Start all",
+    "stop_all": "Stop all",

This pattern should be applied to all camelCase keys in this section to maintain consistency.


1072-1086: Fix remaining camelCase keys in common section.

The common section still contains camelCase keys that violate the established snake_case convention, as noted in previous reviews.

-    "loadMore": "Load More",
-    "selectAll": "Select All", 
-    "deselectAll": "Deselect All",
-    "selectNone": "Select None",
-    "lastUpdated": "Last updated",
-    "unsavedChanges": "You have unsaved changes",
-    "unsavedChangesConfirm": "You have unsaved changes. Are you sure you want to discard them?"
+    "load_more": "Load more",
+    "select_all": "Select all",
+    "deselect_all": "Deselect all", 
+    "select_none": "Select none",
+    "last_updated": "Last updated",
+    "unsaved_changes": "You have unsaved changes",
+    "unsaved_changes_confirm": "You have unsaved changes. Are you sure you want to discard them?"
🧹 Nitpick comments (7)
src/features/groups/components/GroupFlow.vue (1)

213-215: Dynamic node coloring not implemented

The getNodeColor function always returns green regardless of the node's properties.

Implement dynamic coloring based on group priority or type:

-const getNodeColor = () => {
-  return '#22c55e'; // green
+const getNodeColor = (node: Node) => {
+  const priorityClass = node.data?.priorityClass;
+  switch (priorityClass) {
+    case 'high': return '#ef4444'; // red
+    case 'medium': return '#eab308'; // yellow
+    default: return '#22c55e'; // green
+  }
 };
src/features/groups/components/SimpleResourceSelector.vue (1)

149-149: Use English for code comments

The comment is in French. Please use English for consistency.

-  excludeGroupId?: string; // Pour exclure le groupe en cours d'édition
+  excludeGroupId?: string; // To exclude the group being edited
src/features/groups/components/panel/EditModeContent.vue (1)

271-271: Remove unused code

The empty destructuring from useI18n() is not needed since $t is used directly in the template.

-const {} = useI18n();
src/features/groups/store.ts (1)

135-150: Consider optimizing group list refresh after creation.

While calling fetchAllGroups() after creating a group ensures data consistency, it might be more efficient to append the new group to the existing list instead of refetching all groups, especially for large datasets.

Consider this optimization if performance becomes an issue:

 const addGroup = async (payload: CreateGroupDto) => {
   isCreating.value = true;
   error.value = null;

   try {
     const newGroup = await createGroup(payload);
-    await fetchAllGroups();
+    if (groups.value) {
+      groups.value.data.unshift(newGroup);
+      groups.value.total++;
+    } else {
+      await fetchAllGroups();
+    }
     return newGroup;
   } catch (err: any) {
     error.value =
       err.response?.data?.message ?? err.message ?? 'Error creating group';
     throw err;
   } finally {
     isCreating.value = false;
   }
 };
src/features/groups/views/GroupView.vue (2)

190-192: Implement placeholder function.

This function is a placeholder that only logs to console and needs proper implementation.

 const loadMoreGroups = async () => {
-  console.log('Load more groups');
+  // TODO: Implement pagination logic
+  try {
+    await groupStore.fetchMoreGroups();
+  } catch (error) {
+    console.error('Failed to load more groups:', error);
+    toast.error(t('groups.loadMoreError'));
+  }
 };

266-284: Reduce toast notification spam.

Showing multiple toast messages in sequence for a single action can overwhelm users. Consider showing only the final result.

 const handleGroupStart = async (group: GroupResponseDto) => {
   try {
     await startGroupResources(group);
-    toast.success(t('groups.startingGroup', { name: group.name }));
     toast.success(t('groups.startSuccess'));
   } catch (error) {
     toast.error(t('groups.startError'));
   }
 };

 const handleGroupStop = async (group: GroupResponseDto) => {
   try {
     await stopGroupResources(group);
-    toast.success(t('groups.stoppingGroup', { name: group.name }));
     toast.success(t('groups.stopSuccess'));
   } catch (error) {
     toast.error(t('groups.stopError'));
   }
 };
src/locales/fr.json (1)

1-1392: Monolithic locale file is reaching maintenance limits

fr.json is now >1 400 lines. Merging, reviewing and spotting duplicates already proved tricky (#1129/1130 earlier).
Most modern Vue i18n setups support loading locale fragments per-feature (e.g. src/locales/fr/groups.json, .../history.json, …).

Advantages:
• Avoids duplicate keys at compile-time
• Minimises merge conflicts between parallel feature branches
• Keeps PRs and reviews focused

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4d17d4e and 20cce2f.

📒 Files selected for processing (21)
  • src/features/groups/api.ts (1 hunks)
  • src/features/groups/components/CreateGroupModal.vue (1 hunks)
  • src/features/groups/components/DeleteConfirmModal.vue (1 hunks)
  • src/features/groups/components/GroupCard.vue (1 hunks)
  • src/features/groups/components/GroupDetailsModal.vue (1 hunks)
  • src/features/groups/components/GroupEditModal.vue (1 hunks)
  • src/features/groups/components/GroupFlow.vue (1 hunks)
  • src/features/groups/components/GroupList.vue (1 hunks)
  • src/features/groups/components/GroupManagementPanel.vue (1 hunks)
  • src/features/groups/components/GroupSelection.vue (1 hunks)
  • src/features/groups/components/ShutdownPreviewDialog.vue (1 hunks)
  • src/features/groups/components/SimpleResourceSelector.vue (1 hunks)
  • src/features/groups/components/panel/EditModeContent.vue (1 hunks)
  • src/features/groups/components/panel/ViewModeContent.vue (1 hunks)
  • src/features/groups/store.ts (1 hunks)
  • src/features/groups/types.ts (1 hunks)
  • src/features/groups/views/GroupView.vue (1 hunks)
  • src/features/servers/api.ts (1 hunks)
  • src/features/vms/api.ts (1 hunks)
  • src/locales/en.json (17 hunks)
  • src/locales/fr.json (14 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/features/servers/api.ts
  • src/features/groups/components/GroupSelection.vue
  • src/features/groups/components/ShutdownPreviewDialog.vue
  • src/features/groups/components/DeleteConfirmModal.vue
  • src/features/groups/components/GroupEditModal.vue
  • src/features/groups/api.ts
  • src/features/groups/components/GroupList.vue
  • src/features/groups/types.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/features/groups/store.ts (2)
src/features/groups/types.ts (8)
  • PaginatedGroupResponseDto (13-19)
  • PreviewShutdownResponseDto (29-36)
  • GroupResponseDto (3-11)
  • GroupQueryDto (43-48)
  • GroupShutdownDto (38-41)
  • CreateGroupDto (50-54)
  • UpdateGroupDto (56-59)
  • GroupType (1-1)
src/features/groups/api.ts (7)
  • fetchGroups (12-17)
  • previewGroupShutdown (39-44)
  • executeGroupShutdown (46-52)
  • fetchGroupById (19-22)
  • createGroup (24-27)
  • updateGroup (29-32)
  • deleteGroup (34-37)
🔇 Additional comments (11)
src/features/groups/components/GroupFlow.vue (1)

197-199: Incomplete edge implementation

The edges array is always empty, suggesting the flow visualization doesn't show relationships between groups.

If group relationships are planned, consider implementing edge generation. Otherwise, document why edges are not needed in this visualization.

src/features/groups/components/CreateGroupModal.vue (1)

336-414: Well-implemented resource assignment with error handling

The two-step process of creating a group and then assigning resources with proper error handling for partial failures is a robust approach.

src/features/groups/store.ts (2)

55-78: LGTM! Clean delegation pattern for type-specific fetching.

The fetchServerGroups and fetchVmGroups methods now properly delegate to fetchAllGroups with type filtering, which is much cleaner than the previous implementation and addresses the pagination state consistency issues mentioned in past reviews.


172-192: Excellent improvement: Proper data refresh after deletion.

The removeGroup method now calls fetchAllGroups() instead of manually decrementing totalItems, which correctly addresses the pagination consistency issue mentioned in past reviews. This ensures accurate counts when filters or pagination are active.

src/features/groups/components/GroupDetailsModal.vue (2)

390-427: Good error handling and resource loading logic.

The loadGroupResources method properly handles both server and VM types, includes error handling with console logging, and resets the resource list on errors. The filtering logic for group-specific resources is correct.


441-445: Smart optimization for room data loading.

The component efficiently checks if rooms are already loaded before making an unnecessary API call. This prevents redundant requests and improves performance.

src/features/groups/components/panel/ViewModeContent.vue (1)

110-143: Excellent use of SVG for progress visualization.

The circular progress indicator using SVG is well-implemented with smooth transitions and proper percentage calculations. The dynamic stroke-dasharray based on active resources ratio creates an effective visual representation.

src/features/groups/components/GroupCard.vue (1)

164-172: Nice hover effect with smooth transitions.

The SCSS hover transform effect adds a pleasant interactive feel to the cards without being overwhelming. The use of CSS custom properties for timing is a good practice.

src/features/groups/components/GroupManagementPanel.vue (2)

372-383: Smart resource filtering logic for group editing.

The resource filtering logic correctly handles the difference between edit mode (allowing resources from current group) and create mode (only unassigned resources). This prevents conflicts and provides a good user experience.


120-159: Excellent use of transitions for smooth mode switching.

The TransitionGroup with slide-fade animations provides a polished user experience when switching between view and edit modes. The absolute positioning ensures smooth transitions without layout shifts.

src/locales/fr.json (1)

1064-1083: Verification passed: no duplicate translation keys in src/locales/fr.json

I ran a duplicate-key detection script against src/locales/fr.json and found no repeated keys. The newly added entries under common will not silently override any existing values.

Comment on lines 11 to 17
export const patchVm = async (
id: string,
data: { groupId?: string | null },
) => {
const response = await api.patch(`/vm/${id}`, data);
return response.data;
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Maintain consistency with existing API patterns

The patchVm function uses async/await while other functions in this file return promises directly. Also, the endpoint uses singular /vm/{id} while others use plural /vms.

Apply this diff to maintain consistency:

-export const patchVm = async (
-  id: string,
-  data: { groupId?: string | null },
-) => {
-  const response = await api.patch(`/vm/${id}`, data);
-  return response.data;
+export const patchVm = (id: string, data: { groupId?: string | null }) => {
+  return api.patch(`/vms/${id}`, data);
 };

Also consider adding error handling consistent with how it's handled in other parts of the codebase.

📝 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
export const patchVm = async (
id: string,
data: { groupId?: string | null },
) => {
const response = await api.patch(`/vm/${id}`, data);
return response.data;
};
export const patchVm = (id: string, data: { groupId?: string | null }) => {
return api.patch(`/vms/${id}`, data);
};
🤖 Prompt for AI Agents
In src/features/vms/api.ts around lines 11 to 17, refactor the patchVm function
to return the promise directly instead of using async/await to maintain
consistency with other API functions. Change the endpoint from singular
`/vm/${id}` to plural `/vms/${id}` to match the existing pattern. Additionally,
add error handling consistent with other functions in this file, such as
catching errors and handling or propagating them appropriately.

Comment on lines 179 to 191
nodeList.push({
id: group.id,
type: 'group',
position,
data: {
label: group.name,
priorityClass: 'low',
type: group.type,
resourceCount: 0,
resourceLabel: group.type === 'SERVER' ? 'servers' : 'VMs',
group,
},
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Use actual group data instead of hardcoded values

The node data uses hardcoded values for priorityClass and resourceCount instead of deriving them from the actual group data.

Consider calculating these values from the group object:

 nodeList.push({
   id: group.id,
   type: 'group',
   position,
   data: {
     label: group.name,
-    priorityClass: 'low',
+    priorityClass: getPriorityClass(group.priority),
     type: group.type,
-    resourceCount: 0,
+    resourceCount: group.servers?.length || group.vms?.length || 0,
     resourceLabel: group.type === 'SERVER' ? 'servers' : 'VMs',
     group,
   },
 });

Add a helper function to map priority values to classes:

const getPriorityClass = (priority?: number): string => {
  if (!priority) return 'low';
  if (priority >= 8) return 'high';
  if (priority >= 5) return 'medium';
  return 'low';
};
🤖 Prompt for AI Agents
In src/features/groups/components/GroupFlow.vue between lines 179 and 191, the
node data currently uses hardcoded values for priorityClass and resourceCount.
Replace these with dynamic values derived from the group object by adding a
helper function that maps the group's priority number to a priority class string
(e.g., 'low', 'medium', 'high') and use the actual resource count from the group
data instead of zero. Update the node data construction to call this helper for
priorityClass and set resourceCount based on the group's real data.

Comment on lines +224 to +241
} catch (error) {
console.error('Error loading servers:', error);
} finally {
loadingServers.value = false;
}
};

const loadVms = async () => {
if (props.groupType !== 'VM') return;

loadingVms.value = true;
try {
const response = await fetchUvms();
vms.value = Array.isArray(response.data) ? response.data : [];
} catch (error) {
console.error('Error loading VMs:', error);
vms.value = [];
} finally {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve error handling with user feedback

Errors are only logged to console without notifying the user.

Consider showing error messages to users:

 } catch (error) {
   console.error('Error loading servers:', error);
+  // Consider using a toast notification or error state
+  // toast.error($t('groups.form.errorLoadingServers'));
 } finally {
 } catch (error) {
   console.error('Error loading VMs:', error);
   vms.value = [];
+  // Consider using a toast notification or error state
+  // toast.error($t('groups.form.errorLoadingVms'));
 } finally {
📝 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
} catch (error) {
console.error('Error loading servers:', error);
} finally {
loadingServers.value = false;
}
};
const loadVms = async () => {
if (props.groupType !== 'VM') return;
loadingVms.value = true;
try {
const response = await fetchUvms();
vms.value = Array.isArray(response.data) ? response.data : [];
} catch (error) {
console.error('Error loading VMs:', error);
vms.value = [];
} finally {
} catch (error) {
console.error('Error loading servers:', error);
// Consider using a toast notification or error state
// toast.error($t('groups.form.errorLoadingServers'));
} finally {
loadingServers.value = false;
}
};
const loadVms = async () => {
if (props.groupType !== 'VM') return;
loadingVms.value = true;
try {
const response = await fetchUvms();
vms.value = Array.isArray(response.data) ? response.data : [];
} catch (error) {
console.error('Error loading VMs:', error);
vms.value = [];
// Consider using a toast notification or error state
// toast.error($t('groups.form.errorLoadingVms'));
} finally {
🤖 Prompt for AI Agents
In src/features/groups/components/SimpleResourceSelector.vue around lines 224 to
241, error handling currently only logs errors to the console without informing
the user. To fix this, add user-visible error notifications inside the catch
blocks for both server and VM loading functions. Use a UI notification method or
component to display a clear error message so users are aware of the failure,
while keeping the existing console.error for debugging.

const selectedServerIds = ref<string[]>([]);
const selectedVmIds = ref<string[]>([]);
const isCreating = ref(false);
const availableResources = ref<any[]>([]);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Define proper TypeScript interface for resources

The availableResources uses any[] type which reduces type safety.

Define a proper interface:

interface Resource {
  id: string;
  name: string;
  state: string;
  roomId?: string;
  type: 'server' | 'vm';
}

const availableResources = ref<Resource[]>([]);
🤖 Prompt for AI Agents
In src/features/groups/components/CreateGroupModal.vue at line 271, the
availableResources variable is typed as any[], which reduces type safety. Define
a TypeScript interface named Resource with properties id (string), name
(string), state (string), optional roomId (string), and type ('server' | 'vm').
Then update the availableResources ref to use this Resource[] type instead of
any[] to improve type safety.

Comment on lines +431 to +530
const handleSave = async () => {
if (!isValid.value || isSaving.value) return;

isSaving.value = true;

try {
let group: GroupResponseDto;

if (mode.value === 'edit' && currentGroup.value) {
// Update existing group
const updatePayload: UpdateGroupDto = {
name: formData.value.name.trim(),
description: formData.value.description?.trim() || undefined,
};
group = await updateGroup(currentGroup.value.id, updatePayload);

// Handle resource changes
const currentServerIds =
formData.value.type === 'SERVER' ? selectedServerIds.value : [];
const currentVmIds =
formData.value.type === 'VM' ? selectedVmIds.value : [];

// Calculate changes
const serversToAdd = currentServerIds.filter(
(id) => !originalServerIds.value.includes(id),
);
const serversToRemove = originalServerIds.value.filter(
(id) => !currentServerIds.includes(id),
);
const vmsToAdd = currentVmIds.filter(
(id) => !originalVmIds.value.includes(id),
);
const vmsToRemove = originalVmIds.value.filter(
(id) => !currentVmIds.includes(id),
);

// Apply changes
const promises = [];
for (const serverId of serversToAdd) {
promises.push(patchServer(serverId, { groupId: group.id }));
}
for (const serverId of serversToRemove) {
promises.push(patchServer(serverId, { groupId: null }));
}
for (const vmId of vmsToAdd) {
promises.push(patchVm(vmId, { groupId: group.id }));
}
for (const vmId of vmsToRemove) {
promises.push(patchVm(vmId, { groupId: null }));
}

await Promise.allSettled(promises);
toast.success(t('groups.updateSuccess'));
} else {
// Create new group
const createPayload: CreateGroupDto = {
name: formData.value.name.trim(),
description: formData.value.description?.trim() || undefined,
type: formData.value.type,
};
group = await createGroup(createPayload);

// Assign selected resources
const resourceIds =
formData.value.type === 'SERVER'
? selectedServerIds.value
: selectedVmIds.value;
if (resourceIds.length > 0) {
const promises = resourceIds.map((id) =>
formData.value.type === 'SERVER'
? patchServer(id, { groupId: group.id })
: patchVm(id, { groupId: group.id }),
);
await Promise.allSettled(promises);
}

toast.success(t('groups.createSuccess'));
}

// Refresh server store to get updated groupIds
await serverStore.fetchServers();

emit('success', group);
if (mode.value === 'create') {
emit('close');
} else {
// Update the current group data
currentGroup.value = group;
mode.value = 'view';
await loadGroupResources();
}
} catch (error) {
console.error('Error saving group:', error);
toast.error(
mode.value === 'edit' ? t('groups.updateError') : t('groups.createError'),
);
} finally {
isSaving.value = false;
}
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Refactor the lengthy handleSave method.

The handleSave method is quite long (100 lines) and handles multiple responsibilities. Consider breaking it down into smaller, focused methods for better maintainability.

Consider extracting methods like:

 const handleSave = async () => {
   if (!isValid.value || isSaving.value) return;
   isSaving.value = true;

   try {
     let group: GroupResponseDto;
     
     if (mode.value === 'edit' && currentGroup.value) {
-      group = await updateGroup(currentGroup.value.id, updatePayload);
-      // Handle resource changes...
+      group = await handleGroupUpdate();
     } else {
-      group = await createGroup(createPayload);
-      // Assign selected resources...
+      group = await handleGroupCreation();
     }

     await serverStore.fetchServers();
     emit('success', group);
     // ... rest of success handling
   } catch (error) {
     // ... error handling
   } finally {
     isSaving.value = false;
   }
 };

+const handleGroupUpdate = async () => {
+  // Update logic here
+};

+const handleGroupCreation = async () => {
+  // Creation logic here
+};

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/features/groups/components/GroupManagementPanel.vue between lines 431 and
530, the handleSave method is too long and handles multiple tasks. Refactor by
extracting smaller helper functions such as updateGroupResources for handling
resource additions/removals, createNewGroup for creating groups and assigning
resources, and a separate function for updating the current group state after
save. Then call these helper functions from handleSave to improve readability
and maintainability.

Comment on lines +1213 to +1341
"groups": {
"title": "Groupes",
"description": "Gérer les groupes de serveurs et VMs avec orchestration par priorité",
"searchPlaceholder": "Rechercher des groupes...",
"allTypes": "Tous Types",
"serverGroups": "Groupes Serveurs",
"vmGroups": "Groupes VMs",
"allPriorities": "Toutes Priorités",
"priority1": "Priorité 1",
"priority2": "Priorité 2",
"priority3": "Priorité 3",
"priority4": "Priorité 4",
"priorityDesc1": "Critique",
"priorityDesc2": "Haute",
"priorityDesc3": "Moyenne",
"priorityDesc4": "Basse",
"createGroup": "Créer un Groupe",
"noGroups": "Aucun groupe trouvé",
"viewDetails": "Voir Détails",
"start": "Démarrer",
"stop": "Arrêter",
"startAll": "Tout Démarrer",
"stopAll": "Tout Arrêter",
"type": "Type",
"priority": "Priorité",
"cascade": "Cascade",
"cascadeEnabled": "Cascade activée",
"cascadeDisabled": "Cascade désactivée",
"resources": "Ressources",
"noResources": "Aucune ressource assignée",
"room": "Salle",
"serverGroup": "Groupe Serveur",
"vmGroup": "Groupe VM",
"flowView": "Vue Flux",
"fitView": "Ajuster la Vue",
"toggleOrientation": "Basculer l'Orientation",
"totalServers": "Total serveurs",
"totalVms": "Total VMs",
"activeResources": "Actives",
"inactiveResources": "Inactives",
"assignedServers": "Serveurs assignés",
"assignedVms": "VMs assignées",
"addResources": "Ajouter des ressources",
"view": {
"grid": "Vue Grille",
"list": "Vue Liste",
"sections": "Vue Sections",
"flow": "Vue Flux"
},
"form": {
"name": "Nom",
"namePlaceholder": "Entrer le nom du groupe",
"description": "Description",
"descriptionPlaceholder": "Entrer la description du groupe",
"type": "Type",
"priority": "Priorité",
"cascade": "Activer cascade",
"cascadeHelp": "Démarrer/arrêter automatiquement les groupes dépendants",
"room": "Salle",
"noRoom": "Aucune salle spécifique",
"selectedResources": "Ressources sélectionnées",
"selectServers": "Sélectionner des serveurs",
"selectVms": "Sélectionner des VMs",
"noServersAvailable": "Aucun serveur disponible",
"noVmsAvailable": "Aucune VM disponible",
"loadingServers": "Chargement des serveurs...",
"loadingVms": "Chargement des VMs...",
"serversSelected": "{count} serveurs sélectionnés",
"vmsSelected": "{count} VMs sélectionnées",
"alreadyAssigned": "Déjà assigné",
"partialAssignmentError": "Assignation partielle : {assigned} réussies, {failed} échouées",
"allResourcesAssigned": "Les {count} ressources ont été assignées avec succès",
"selectResourcesHelp": "Sélectionnez les ressources que vous souhaitez assigner à ce groupe"
},
"createSuccess": "Groupe créé avec succès",
"createError": "Erreur lors de la création du groupe",
"deleteConfirm": "Êtes-vous sûr de vouloir supprimer le groupe '{name}' ?",
"deleteConfirmTitle": "Confirmer la suppression",
"deleteConfirmMessage": "Êtes-vous sûr de vouloir supprimer le groupe '{name}' ? Cette action est irréversible.",
"deleteWarning": "Attention : Ce groupe contient des ressources",
"deleteWarningDetails": "Ce groupe contient {count} ressource(s) qui seront dissociées lors de la suppression.",
"deleteSuccess": "Groupe supprimé avec succès",
"deleteError": "Erreur lors de la suppression du groupe",
"startSuccess": "Groupe démarré avec succès",
"startError": "Erreur lors du démarrage du groupe",
"stopSuccess": "Groupe arrêté avec succès",
"stopError": "Erreur lors de l'arrêt du groupe",
"startingGroup": "Démarrage de {name}...",
"stoppingGroup": "Arrêt de {name}...",
"groupShutdown": "Arrêt de Groupes",
"groupShutdownDescription": "Prévisualiser et exécuter des séquences d'arrêt contrôlées pour les groupes de serveurs et VMs",
"selectGroups": "Sélectionner des Groupes",
"selectAll": "Tout Sélectionner",
"clearAll": "Tout Effacer",
"total": "total",
"serversCount": "serveurs",
"vmsCount": "VMs",
"groupsSelected": "groupes sélectionnés",
"previewShutdown": "Prévisualiser l'Arrêt",
"shutdownPreview": "Aperçu de l'Arrêt",
"shutdownWarning": "Ceci va arrêter les groupes sélectionnés dans l'ordre de priorité. Les VMs seront arrêtées avant les serveurs.",
"shutdownOrder": "Ordre",
"name": "Nom",
"group": "Groupe",
"loadingPreview": "Chargement de l'aperçu...",
"confirmationRequired": "Confirmation Requise",
"shutdownConfirmationText": "Cette action va arrêter des serveurs et VMs. Assurez-vous que tous les services sont préparés pour l'arrêt.",
"confirmUnderstandConsequences": "Je comprends les conséquences de cet arrêt",
"executeShutdown": "Exécuter l'Arrêt",
"executing": "Exécution...",
"shutdownExecutedSuccessfully": "Arrêt exécuté avec succès",
"namePlaceholder": "Entrez le nom du groupe",
"descriptionPlaceholder": "Entrez la description du groupe (optionnel)",
"priorityHelp": "Valeurs faibles = priorité élevée",
"noRoom": "Aucune salle spécifique",
"parentServerGroup": "Groupe serveur parent",
"selectServerGroup": "Sélectionnez un groupe serveur",
"servers": "Serveurs",
"vms": "Machines virtuelles",
"selected": "sélectionné(s)",
"searchResources": "Rechercher des ressources...",
"noResourcesFound": "Aucune ressource trouvée",
"selectedCount": "{count} sur {total} sélectionné(s)",
"serverCount": "Serveurs",
"vmCount": "VMs",
"highPriority": "Priorité Élevée",
"mediumPriority": "Priorité Moyenne",
"lowPriority": "Priorité Faible"
},
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Inconsistent casing & duplicate-looking identifiers inside groups namespace

  1. Key style abruptly switches to mixed camelCase (searchPlaceholder, allTypes, …) while most other top-level namespaces use snake_case.
    A consistent naming convention reduces cognitive overhead and makes grepping easier.

  2. Top-level keys "name", "noRoom", "selected" duplicate identifiers already present inside groups.form.
    Although they live in different sub-objects and won’t collide at runtime, they are error-prone during refactors ($t('groups.name') vs $t('groups.form.name')). Consider prefixing or keeping all resource-specific keys inside the form object only.

Example refactor:

-    "searchPlaceholder": "Rechercher des groupes...",
+    "search_placeholder": "Rechercher des groupes...",
@@
-    "name": "Nom",
+    "group_name": "Nom",
🤖 Prompt for AI Agents
In src/locales/fr.json between lines 1213 and 1341, the keys inside the "groups"
namespace use inconsistent casing, mixing camelCase with snake_case, and some
top-level keys like "name", "noRoom", and "selected" duplicate keys inside the
"form" object. To fix this, standardize all keys in "groups" to use snake_case
for consistency and move or prefix duplicated keys so that resource-specific
keys remain only inside the "form" object, avoiding ambiguity and reducing
refactor errors.

Copy link

@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

♻️ Duplicate comments (5)
src/features/vms/api.ts (1)

11-17: Address previous review feedback regarding consistency.

The patchVm function still uses async/await while other functions in this file return promises directly, creating an inconsistency in the API pattern.

src/features/groups/components/GroupFlow.vue (2)

98-116: Use proper TypeScript types for drag events.

The onServerDragEnd function still uses any type for the event parameter, which was flagged in previous reviews for type safety concerns.


134-163: Use proper TypeScript types for drag events.

The onVMDragEnd function also uses any type for the event parameter, reducing type safety.

src/features/groups/stores/usePriorityStore.ts (2)

141-150: Handle partial failures in concurrent requests.

Similar to swapServerPriorities, this function needs to handle partial failures.


153-155: Loading state conflicts in concurrent fetches.

When both fetchServers() and fetchVMs() run concurrently, the shared loading ref will cause incorrect loading states. This issue will be resolved if you implement separate loading states as suggested earlier.

🧹 Nitpick comments (3)
src/features/groups/stores/usePriorityStore.ts (3)

29-36: Use optional chaining for cleaner code.

The static analysis correctly identifies an opportunity to use optional chaining.

Apply this change:

-if (response.data && response.data.items) {
-  servers.value = response.data.items || [];
-} else {
-  servers.value = [];
-}
+servers.value = response.data?.items || [];

49-56: Use optional chaining for cleaner code.

Similar to the fetchServers function, use optional chaining here.

Apply this change:

-if (response.data && response.data.items) {
-  vms.value = response.data.items || [];
-} else {
-  vms.value = [];
-}
+vms.value = response.data?.items || [];

29-31: TODO: Future endpoint migration.

The TODO indicates that dedicated priority endpoints will be created. Would you like me to help create a migration plan or abstraction layer to make this transition easier when the new endpoints are available?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 61f63df and 4d9054f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • package.json (2 hunks)
  • src/features/groups/components/GroupFlow.vue (1 hunks)
  • src/features/groups/components/priority/ServerCard.vue (1 hunks)
  • src/features/groups/components/priority/VmCard.vue (1 hunks)
  • src/features/groups/stores/usePriorityStore.ts (1 hunks)
  • src/features/groups/types/priority.ts (1 hunks)
  • src/features/groups/utils/priority-utils.ts (1 hunks)
  • src/features/vms/api.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/features/groups/types/priority.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/features/groups/utils/priority-utils.ts (1)
src/features/groups/types/priority.ts (1)
  • Priority (1-1)
src/features/groups/stores/usePriorityStore.ts (3)
src/features/groups/types/priority.ts (3)
  • ServerWithPriority (3-9)
  • VMWithPriority (11-17)
  • Priority (1-1)
src/features/groups/utils/priority-utils.ts (2)
  • sortByPriority (71-78)
  • swapPriorities (3-23)
src/features/servers/api.ts (1)
  • fetchServers (87-92)
🪛 Biome (1.9.4)
src/features/groups/stores/usePriorityStore.ts

[error] 32-32: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 52-53: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (5)
src/features/groups/components/priority/VmCard.vue (1)

1-60: Well-implemented Vue component with proper TypeScript support.

The component follows Vue best practices with:

  • Proper TypeScript prop definitions
  • Dynamic styling based on state
  • Appropriate use of utility functions for priority colors
  • Clean template structure with slots for extensibility
src/features/groups/components/priority/ServerCard.vue (1)

1-85: Clean server card implementation with comprehensive display logic.

The component demonstrates good Vue practices with:

  • Proper TypeScript interface definitions
  • Conditional rendering for optional data (IP address, VM count)
  • Consistent priority color/label utilities usage
  • Well-structured template with appropriate slots
src/features/groups/utils/priority-utils.ts (3)

3-23: Well-implemented priority swap utility.

The swapPriorities function correctly handles edge cases and maintains immutability with proper type constraints.


45-69: Consistent priority color and label mapping.

The getPriorityColor and getPriorityLabel functions provide consistent UI mapping with proper dark mode support and clear priority semantics.


25-43: Fix logic error in reorderByPosition function.

The function finds draggedIndex after sorting the items, but it should find the dragged item in the original array before sorting. This could lead to incorrect reordering.

Apply this diff to fix the logic:

 export function reorderByPosition<T extends { id: string; priority: Priority }>(
   items: T[],
   draggedId: string,
   targetPosition: number,
 ): T[] {
-  const draggedIndex = items.findIndex((item) => item.id === draggedId);
-  if (draggedIndex === -1) return items;
-
   const sortedItems = [...items].sort((a, b) => a.priority - b.priority);
-  const draggedItem = sortedItems[draggedIndex];
+  const draggedIndex = sortedItems.findIndex((item) => item.id === draggedId);
+  if (draggedIndex === -1) return items;
+  
+  const draggedItem = sortedItems[draggedIndex];
 
   sortedItems.splice(draggedIndex, 1);
   sortedItems.splice(targetPosition, 0, draggedItem);

Likely an incorrect or invalid review comment.

Comment on lines +14 to +19
const loading = ref(false);
const error = ref<string | null>(null);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider using separate loading and error states for different operations.

The shared loading and error refs across all operations can lead to race conditions and confusing UX. For example, if fetchServers() and fetchVMs() are called simultaneously, the loading state will be incorrect.

Consider using a more granular approach:

-const loading = ref(false);
-const error = ref<string | null>(null);
+const loadingStates = ref({
+  servers: false,
+  vms: false,
+  updateServer: false,
+  updateVm: false,
+});
+const errors = ref({
+  servers: null as string | null,
+  vms: null as string | null,
+  lastOperation: null as string | null,
+});
📝 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
const loading = ref(false);
const error = ref<string | null>(null);
const loadingStates = ref({
servers: false,
vms: false,
updateServer: false,
updateVm: false,
});
const errors = ref({
servers: null as string | null,
vms: null as string | null,
lastOperation: null as string | null,
});
🤖 Prompt for AI Agents
In src/features/groups/stores/usePriorityStore.ts around lines 14 to 15, the
current single loading and error refs are shared across all operations, which
can cause race conditions and inaccurate UI states when multiple operations run
concurrently. Refactor the code to use separate loading and error states for
each asynchronous operation, such as distinct refs for fetchServers and
fetchVMs, to ensure each operation's status is tracked independently and the UI
reflects the correct state.

Comment on lines 113 to 131
try {
await Promise.all([
api.patch(`/server/${server1Id}`, {
priority: server1.priority,
}),
api.patch(`/server/${server2Id}`, {
priority: server2.priority,
}),
]);
} catch (err) {
servers.value = originalServers;
error.value = 'Failed to swap server priorities';
throw err;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle partial failures in concurrent requests.

The current implementation doesn't handle the case where one PATCH request succeeds and another fails, leading to inconsistent state between frontend and backend.

Consider using Promise.allSettled to handle partial failures:

try {
-  await Promise.all([
+  const results = await Promise.allSettled([
    api.patch(`/server/${server1Id}`, {
      priority: server1.priority,
    }),
    api.patch(`/server/${server2Id}`, {
      priority: server2.priority,
    }),
  ]);
+  
+  const failures = results.filter(r => r.status === 'rejected');
+  if (failures.length > 0) {
+    // Fetch fresh data to ensure consistency
+    await fetchServers();
+    error.value = 'Failed to swap server priorities';
+    throw new Error('Partial failure in swap operation');
+  }
} catch (err) {
  servers.value = originalServers;
  error.value = 'Failed to swap server priorities';
  throw err;
}
📝 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
try {
await Promise.all([
api.patch(`/server/${server1Id}`, {
priority: server1.priority,
}),
api.patch(`/server/${server2Id}`, {
priority: server2.priority,
}),
]);
} catch (err) {
servers.value = originalServers;
error.value = 'Failed to swap server priorities';
throw err;
}
try {
const results = await Promise.allSettled([
api.patch(`/server/${server1Id}`, {
priority: server1.priority,
}),
api.patch(`/server/${server2Id}`, {
priority: server2.priority,
}),
]);
const failures = results.filter(r => r.status === 'rejected');
if (failures.length > 0) {
// Fetch fresh data to ensure consistency
await fetchServers();
error.value = 'Failed to swap server priorities';
throw new Error('Partial failure in swap operation');
}
} catch (err) {
servers.value = originalServers;
error.value = 'Failed to swap server priorities';
throw err;
}
🤖 Prompt for AI Agents
In src/features/groups/stores/usePriorityStore.ts around lines 113 to 126, the
current code uses Promise.all for concurrent PATCH requests, which does not
handle partial failures properly. Replace Promise.all with Promise.allSettled to
wait for all requests to complete regardless of success or failure. Then, check
the results to detect any failed requests; if any fail, revert servers.value to
originalServers, set the error message, and throw an error to maintain
consistent frontend and backend state.

Copy link

@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

♻️ Duplicate comments (3)
src/features/groups/components/GroupFlow.vue (2)

104-104: Use proper TypeScript types instead of any.

Replace the any type with the proper VueDraggable event type for better type safety.

-const onServerDragEnd = async (event: any) => {
+import type { SortableEvent } from 'sortablejs';
+
+const onServerDragEnd = async (event: SortableEvent) => {

141-141: Use proper TypeScript types instead of any.

Replace the any type with the proper VueDraggable event type for better type safety.

-const onVMDragEnd = async (event: any) => {
+const onVMDragEnd = async (event: SortableEvent) => {
src/features/groups/stores/usePriorityStore.ts (1)

18-19: Consider using separate loading and error states for different operations.

The shared loading and error refs across all operations can lead to race conditions and confusing UX. For example, if fetchServers() and fetchVMs() are called simultaneously, the loading state will be incorrect.

Consider using a more granular approach:

-const loading = ref(false);
-const error = ref<string | null>(null);
+const loadingStates = ref({
+  servers: false,
+  vms: false,
+  updateServer: false,
+  updateVm: false,
+});
+const errors = ref({
+  servers: null as string | null,
+  vms: null as string | null,
+  lastOperation: null as string | null,
+});
🧹 Nitpick comments (2)
src/features/groups/stores/usePriorityStore.ts (2)

43-46: Consider preserving existing data on fetch errors.

When fetch operations fail, the current implementation clears the data by setting it to empty arrays. This might cause a jarring UX where data disappears on transient network errors.

Consider keeping existing data on error:

 } catch (err) {
   error.value = 'Failed to fetch servers';
   console.error('Error fetching servers:', err);
-  servers.value = [];
+  // Keep existing data on error
 }

 // Same for VMs:
 } catch (err) {
   error.value = 'Failed to fetch VMs';
   console.error('Error fetching VMs:', err);
-  vms.value = [];
+  // Keep existing data on error
 }

Also applies to: 65-68


119-119: Use consistent language in code comments.

For better team collaboration, use English for code comments to maintain consistency.

-      // Mettre à jour le state local avec la réponse
+      // Update local state with the response

Also applies to: 146-146

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4d9054f and 22369e3.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • package.json (2 hunks)
  • src/features/groups/components/GroupFlow.vue (1 hunks)
  • src/features/groups/components/priority/ServerCard.vue (1 hunks)
  • src/features/groups/components/priority/VmCard.vue (1 hunks)
  • src/features/groups/stores/usePriorityStore.ts (1 hunks)
  • src/features/groups/types/priority.ts (1 hunks)
  • src/features/groups/utils/priority-utils.ts (1 hunks)
  • src/features/vms/api.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • package.json
  • src/features/vms/api.ts
  • src/features/groups/components/priority/VmCard.vue
  • src/features/groups/components/priority/ServerCard.vue
  • src/features/groups/utils/priority-utils.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/features/groups/stores/usePriorityStore.ts (2)
src/features/groups/types/priority.ts (7)
  • ServerWithPriority (3-9)
  • VMWithPriority (11-17)
  • ServerPriorityResponse (28-34)
  • VmPriorityResponse (36-42)
  • Priority (1-1)
  • SwapServerPriorityResponse (44-47)
  • SwapVmPriorityResponse (49-52)
src/features/groups/utils/priority-utils.ts (1)
  • sortByPriority (71-78)
🔇 Additional comments (2)
src/features/groups/components/GroupFlow.vue (1)

34-44: No action needed: data-server-id is already bound

The ServerCard component in

  • src/features/groups/components/priority/ServerCard.vue (line 8)
    already declares
:data-server-id="server.id"

on its root element, so the DOM queries in GroupFlow.vue will find the attribute as expected.

Likely an incorrect or invalid review comment.

src/features/groups/types/priority.ts (1)

1-52: Well-structured type definitions!

The type definitions are comprehensive and properly typed. The use of union types for Priority and state values provides good type safety.

Comment on lines +125 to +139
const updateServerVMs = (serverId: string, vms: VMWithPriority[]) => {
const allVMs = [...priorityStore.vms];

allVMs.forEach((vm) => {
if (vm.serverId === serverId) {
const newVm = vms.find((v) => v.id === vm.id);
if (newVm) {
const index = allVMs.findIndex((v) => v.id === vm.id);
allVMs[index] = { ...vm, priority: newVm.priority };
}
}
});

priorityStore.vms = allVMs;
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Avoid directly modifying store state outside of actions.

The function directly assigns to priorityStore.vms which violates Pinia best practices. Store state should only be modified through actions.

Consider adding an action in the store to handle this update:

// In usePriorityStore.ts, add:
+function updateVMsForServer(serverId: string, updatedVMs: VMWithPriority[]) {
+  vms.value = vms.value.map(vm => {
+    if (vm.serverId === serverId) {
+      const updated = updatedVMs.find(v => v.id === vm.id);
+      return updated || vm;
+    }
+    return vm;
+  });
+}

// In GroupFlow.vue:
-const updateServerVMs = (serverId: string, vms: VMWithPriority[]) => {
-  const allVMs = [...priorityStore.vms];
-  // ... current logic
-  priorityStore.vms = allVMs;
-};
+const updateServerVMs = (serverId: string, vms: VMWithPriority[]) => {
+  priorityStore.updateVMsForServer(serverId, vms);
+};
📝 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
const updateServerVMs = (serverId: string, vms: VMWithPriority[]) => {
const allVMs = [...priorityStore.vms];
allVMs.forEach((vm) => {
if (vm.serverId === serverId) {
const newVm = vms.find((v) => v.id === vm.id);
if (newVm) {
const index = allVMs.findIndex((v) => v.id === vm.id);
allVMs[index] = { ...vm, priority: newVm.priority };
}
}
});
priorityStore.vms = allVMs;
};
// In GroupFlow.vue, replace the entire updateServerVMs function with:
const updateServerVMs = (serverId: string, vms: VMWithPriority[]) => {
priorityStore.updateVMsForServer(serverId, vms);
};
🤖 Prompt for AI Agents
In src/features/groups/components/GroupFlow.vue around lines 125 to 139, the
function updateServerVMs directly modifies the Pinia store state by assigning to
priorityStore.vms, which violates best practices. To fix this, move the logic
that updates the VMs into a dedicated action inside the priorityStore. Then,
call that action from this component instead of directly assigning to the state.
This ensures state mutations happen only within store actions.

Comment on lines +146 to +154
const fromServerId = from
.closest('.server-column')
?.querySelector('[data-server-id]')
?.getAttribute('data-server-id');
const toServerId = to
.closest('.server-column')
?.querySelector('[data-server-id]')
?.getAttribute('data-server-id');

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Replace fragile DOM queries with a more robust approach.

The current implementation relies on DOM structure which is fragile and could break with template changes.

Consider using VueDraggable's data attributes or item properties:

// In the template, add data attributes to VueDraggable:
 <VueDraggable
   :model-value="getVMsByServerId(server.id)"
+  :data-server-id="server.id"
   @update:model-value="(vms: VMWithPriority[]) => updateServerVMs(server.id, vms)"
   // ... other props
 >

// In onVMDragEnd:
-const fromServerId = from
-  .closest('.server-column')
-  ?.querySelector('[data-server-id]')
-  ?.getAttribute('data-server-id');
-const toServerId = to
-  .closest('.server-column')
-  ?.querySelector('[data-server-id]')
-  ?.getAttribute('data-server-id');
+const fromServerId = from.getAttribute('data-server-id');
+const toServerId = to.getAttribute('data-server-id');
📝 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
const fromServerId = from
.closest('.server-column')
?.querySelector('[data-server-id]')
?.getAttribute('data-server-id');
const toServerId = to
.closest('.server-column')
?.querySelector('[data-server-id]')
?.getAttribute('data-server-id');
<!-- Template: add a data attribute for the server ID -->
<VueDraggable
:model-value="getVMsByServerId(server.id)"
:data-server-id="server.id"
@update:model-value="(vms: VMWithPriority[]) => updateServerVMs(server.id, vms)"
<!-- ... other props ... -->
>
<!-- ... -->
</VueDraggable>
<!-- Script: simplify the drag-end lookup to read the attribute directly -->
const onVMDragEnd = (event: SortableEvent) => {
const { from, to } = event;
const fromServerId = from.getAttribute('data-server-id');
const toServerId = to.getAttribute('data-server-id');
// ... rest of your logic ...
};
🤖 Prompt for AI Agents
In src/features/groups/components/GroupFlow.vue around lines 146 to 154, the
code uses fragile DOM queries to get server IDs by traversing the DOM, which can
break if the template changes. Instead, refactor to use VueDraggable's provided
data attributes or item properties directly to access the server IDs, avoiding
reliance on DOM structure. Update the logic to retrieve these IDs from the
draggable event or item context rather than querying the DOM.

Copy link

@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

♻️ Duplicate comments (7)
src/features/servers/views/ServerDetailsView.vue (2)

204-223: Implement proper credential cleanup based on remember preference.

The current implementation doesn't respect the user's choice when they uncheck "Remember for this session". This is the same issue identified in previous reviews.


387-394: Add error handling for SSH connection failures.

The SshTerminal component should emit connection errors, and the parent should handle them to provide user feedback. This mirrors the previous review comment.

src/features/servers/store.ts (1)

81-81: Extract magic number to a constant.

The page size of 12 is still hardcoded here and in other places (lines 28, 71, 81). This creates a maintenance burden as identified in previous reviews.

src/locales/en.json (2)

1063-1090: Still mixing camelCase & snake_case in common keys

Keys like loadMore, view_details, selectAll, … remain camelCase while the rest of the file is snake_case – the same concern raised previously. Keeping two styles side-by-side hurts grep-ability and will lead to duplicated keys (view_details already exists elsewhere in snake_case).

Convert the new camelCase keys to snake_case before they spread further.


1219-1352: groups namespace still introduces a wave of camelCase keys

Nearly every key (searchPlaceholder, allTypes, priorityDesc1, …) deviates from the established snake_case convention in this repository. Please normalise to snake_case for consistency and maintainability.

src/locales/fr.json (2)

1061-1087: Mélange persistant camelCase / snake_case dans common

Les clés loadMore, view_details, selectAll, … sont toujours en camelCase alors que le reste du fichier utilise snake_case. Même remarque que lors des précédentes revues : standardisez en snake_case.


1216-1344: Le namespace groups reste en camelCase

searchPlaceholder, allTypes, priorityDesc1, etc. ne respectent pas le snake_case utilisé ailleurs. Merci d’harmoniser avant que ces clés ne se dupliquent dans le code.

🧹 Nitpick comments (1)
src/features/groups/components/PriorityInput.vue (1)

74-103: Consider making preset ranges configurable.

The preset ranges are hardcoded, which might not be flexible enough for different use cases. Consider extracting these to props or a configuration object to allow customization.

+interface PresetConfig {
+  label: string;
+  value: number;
+  min: number;
+  max: number;
+  color: string;
+}

 interface Props {
   modelValue: number;
   inputId?: string;
   inputClass?: string;
   required?: boolean;
   disabled?: boolean;
   showPresets?: boolean;
   hint?: string;
+  presets?: PresetConfig[];
 }

 const props = withDefaults(defineProps<Props>(), {
   // ... existing defaults
+  presets: () => [
+    {
+      label: 'Critical',
+      value: 1,
+      min: 1,
+      max: 99,
+      color: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
+    },
+    // ... other presets
+  ],
 });

-const presets = [
-  // ... hardcoded presets
-];
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 22369e3 and 83c3dcb.

📒 Files selected for processing (13)
  • src/features/groups/components/PriorityInput.vue (1 hunks)
  • src/features/groups/types/priority.ts (1 hunks)
  • src/features/groups/utils/priority-utils.ts (1 hunks)
  • src/features/servers/api.ts (1 hunks)
  • src/features/servers/components/CreateServer.vue (7 hunks)
  • src/features/servers/components/ServerEditModal.vue (4 hunks)
  • src/features/servers/store.ts (2 hunks)
  • src/features/servers/views/ServerDetailsView.vue (8 hunks)
  • src/features/setup/components/steps/CreateServerStep.vue (24 hunks)
  • src/layouts/views/DefaultLayout.vue (3 hunks)
  • src/locales/en.json (18 hunks)
  • src/locales/fr.json (15 hunks)
  • src/router/index.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/router/index.ts
  • src/layouts/views/DefaultLayout.vue
  • src/features/servers/components/CreateServer.vue
  • src/features/groups/utils/priority-utils.ts
  • src/features/setup/components/steps/CreateServerStep.vue
  • src/features/groups/types/priority.ts
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/features/servers/api.ts (1)
src/features/servers/types.ts (1)
  • Server (6-23)
src/features/servers/store.ts (2)
src/features/servers/types.ts (4)
  • Server (6-23)
  • ServerListParams (49-52)
  • ServerListResponse (42-47)
  • CreateServerPayload (25-40)
src/features/servers/api.ts (5)
  • fetchServers (87-92)
  • getServersAdmin (115-118)
  • createServer (110-113)
  • fetchServerById (96-108)
  • updateServer (128-134)
🔇 Additional comments (12)
src/features/servers/api.ts (3)

115-118: LGTM! Clean admin API implementation.

The function follows the established patterns and provides proper type safety for admin-level server retrieval.


120-126: LGTM! Focused patch function for group assignment.

The function is well-designed for its specific use case of updating server group assignments. The limited scope of the data parameter (groupId?: string | null) ensures this function stays focused on group management operations.


128-134: LGTM! Comprehensive server update function.

The function provides full server update capability with proper type safety using Partial<Server>, allowing flexible updates while maintaining type checking.

src/features/groups/components/PriorityInput.vue (1)

109-113: LGTM! Proper input validation and bounds checking.

The bounds enforcement ensures data integrity while providing a good user experience by automatically correcting out-of-range values.

src/features/servers/components/ServerEditModal.vue (2)

41-58: LGTM! Robust form state management.

The watcher properly handles synchronization between the server prop and form state, including nested iLO properties. The immediate flag ensures the form is initialized correctly.


150-156: LGTM! Proper integration of PriorityInput component.

The component is well-integrated with appropriate props and styling that matches the form's design system.

src/features/servers/views/ServerDetailsView.vue (2)

126-133: LGTM! Proper store integration.

The refactoring to use the server store improves consistency and leverages centralized state management effectively.


135-153: LGTM! Clean edit workflow implementation.

The edit functionality properly uses the store's methods and provides good user feedback via toast notifications.

src/features/servers/store.ts (4)

28-55: LGTM! Excellent pagination implementation.

The fetchServers method properly handles pagination with append functionality, updating all relevant state variables. The error handling and loading states are well-managed.


71-74: LGTM! Proper load more implementation.

The method correctly checks for loading state and hasMore flag to prevent duplicate requests and unnecessary API calls.


109-133: LGTM! Comprehensive server editing with state synchronization.

The editServer method properly updates both the list and current server, ensuring UI consistency across components.


152-155: LGTM! Thoughtful backward compatibility.

The legacy aliases ensure existing code continues to work while the store is being migrated, providing a smooth transition path.

Comment on lines +60 to +79
const handleSave = () => {
if (props.server) {
const updatedData = {
...props.server,
name: form.name,
ip: form.ip,
type: form.type,
priority: form.priority,
adminUrl: form.adminUrl,
login: form.login,
grace_period_on: form.grace_period_on,
grace_period_off: form.grace_period_off,
ilo: form.ilo.name || form.ilo.ip || form.ilo.login ? form.ilo : null,
};

// Update the parent's server data
Object.assign(props.server, updatedData);
emit('save');
}
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider avoiding direct prop mutation.

The current implementation mutates the props.server object directly via Object.assign. This violates Vue's principle of props being read-only and could cause reactivity issues.

 const handleSave = () => {
   if (props.server) {
     const updatedData = {
       ...props.server,
       name: form.name,
       ip: form.ip,
       type: form.type,
       priority: form.priority,
       adminUrl: form.adminUrl,
       login: form.login,
       grace_period_on: form.grace_period_on,
       grace_period_off: form.grace_period_off,
       ilo: form.ilo.name || form.ilo.ip || form.ilo.login ? form.ilo : null,
     };
     
-    // Update the parent's server data
-    Object.assign(props.server, updatedData);
-    emit('save');
+    emit('save', updatedData);
   }
 };

Update the emit interface:

 interface Emits {
   (e: 'close'): void;
-  (e: 'save'): void;
+  (e: 'save', server: Server): void;
 }
📝 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
const handleSave = () => {
if (props.server) {
const updatedData = {
...props.server,
name: form.name,
ip: form.ip,
type: form.type,
priority: form.priority,
adminUrl: form.adminUrl,
login: form.login,
grace_period_on: form.grace_period_on,
grace_period_off: form.grace_period_off,
ilo: form.ilo.name || form.ilo.ip || form.ilo.login ? form.ilo : null,
};
// Update the parent's server data
Object.assign(props.server, updatedData);
emit('save');
}
};
const handleSave = () => {
if (props.server) {
const updatedData = {
...props.server,
name: form.name,
ip: form.ip,
type: form.type,
priority: form.priority,
adminUrl: form.adminUrl,
login: form.login,
grace_period_on: form.grace_period_on,
grace_period_off: form.grace_period_off,
ilo: form.ilo.name || form.ilo.ip || form.ilo.login ? form.ilo : null,
};
emit('save', updatedData);
}
};
Suggested change
const handleSave = () => {
if (props.server) {
const updatedData = {
...props.server,
name: form.name,
ip: form.ip,
type: form.type,
priority: form.priority,
adminUrl: form.adminUrl,
login: form.login,
grace_period_on: form.grace_period_on,
grace_period_off: form.grace_period_off,
ilo: form.ilo.name || form.ilo.ip || form.ilo.login ? form.ilo : null,
};
// Update the parent's server data
Object.assign(props.server, updatedData);
emit('save');
}
};
interface Emits {
(e: 'close'): void;
(e: 'save', server: Server): void;
}
🤖 Prompt for AI Agents
In src/features/servers/components/ServerEditModal.vue around lines 60 to 79,
the code directly mutates the props.server object using Object.assign, which
violates Vue's rule that props are read-only and can cause reactivity problems.
To fix this, avoid mutating props.server directly; instead, create a new updated
server object with the form data and emit it via the 'save' event so the parent
component can update its state accordingly. Adjust the emit call to pass the
updated server data as a parameter.

@jabibamman jabibamman merged commit 9e14624 into master Jul 6, 2025
0 of 2 checks passed
@jabibamman jabibamman deleted the feat/group branch July 6, 2025 00:45
@coderabbitai coderabbitai bot mentioned this pull request Jul 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant