Skip to content

feat: dashboard quick actions#77

Merged
kmendell merged 11 commits intomainfrom
feat/quick-actions
Apr 28, 2025
Merged

feat: dashboard quick actions#77
kmendell merged 11 commits intomainfrom
feat/quick-actions

Conversation

@kmendell
Copy link
Member

@kmendell kmendell commented Apr 28, 2025

Fixes: #6

Summary by CodeRabbit

  • New Features
    • Added buttons to start all stopped containers, stop all running containers, and prune unused Docker resources directly from the dashboard.
    • Introduced a confirmation dialog for system pruning with selectable resource types and warnings for destructive actions.
    • Added visual loading indicators and disabled states for these actions to provide clear feedback during operations.
    • Enhanced dashboard with improved state management and real-time updates after performing bulk actions.
    • Integrated application settings into the dashboard, enabling pruning mode configuration.
    • Added detailed system prune operation with selective resource pruning and disk usage info retrieval.
  • Bug Fixes
    • Improved error handling and user notifications for bulk container and system operations.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Apr 28, 2025

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

"""

Walkthrough

This set of changes introduces backend and frontend support for quick action buttons on the dashboard, enabling users to start all stopped Docker containers, stop all running containers, and prune the Docker system. New API endpoints are implemented to handle these operations, each with appropriate error handling and logging. The frontend page is updated to manage local state for these actions, provide user feedback via loading indicators and toast notifications, and handle errors gracefully. The core Docker service is extended with a function to perform system pruning, aggregating results from multiple prune operations.

Changes

File(s) Change Summary
src/lib/services/docker/system-service.ts Added pruneSystem async function to prune unused Docker containers, images, networks, and volumes with error handling and result aggregation.
src/lib/components/dialogs/prune-confirmation-dialog.svelte Added new Svelte component for prune confirmation dialog with selectable resource types, pruning state handling, and callbacks for confirm and cancel actions.
src/routes/+page.svelte Enhanced dashboard with new local state management, async handlers (refreshData, handleStartAll, handleStopAll, confirmPrune), UI updates for quick action buttons, loading states, prune confirmation dialog integration, and toast notifications.
src/routes/+page.server.ts Extended load function to fetch application settings including pruneMode alongside existing Docker data, with robust error handling and fallback values.
src/routes/api/containers/start-all/+server.ts
src/routes/api/containers/stop-all/+server.ts
Added new POST API handlers to start all stopped containers and stop all running containers, respectively, with concurrent execution, logging, and error handling.
src/routes/api/system/prune/+server.ts Added new POST API handler to perform Docker system prune by invoking the service, aggregating results, and returning total reclaimed space, with error handling.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant DashboardPage
    participant API
    participant DockerService

    User->>DashboardPage: Clicks "Start All Stopped" / "Stop All Running" / "Prune System"
    DashboardPage->>API: POST /api/containers/start-all or /stop-all or /system/prune
    API->>DockerService: startAllStoppedContainers() / stopAllRunningContainers() / pruneSystem()
    DockerService-->>API: Operation result (success or error)
    API-->>DashboardPage: JSON response
    DashboardPage-->>User: Show toast, update UI, refresh data
Loading

Assessment against linked issues

Objective Addressed Explanation
Implement quick action buttons for starting all stopped containers, stopping all running containers, and pruning the system on the home page (#6)
"""

🪧 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.
    • Generate unit testing code for this file.
    • 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 generate unit testing code for this file.
    • @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 generate unit testing code.
    • @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.

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
Contributor

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

♻️ Duplicate comments (1)
src/routes/+page.svelte (1)

74-93: Duplicate concerns for Stop-All

Same endpoint-path discrepancy and JSON-parsing caveat apply here.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between e99ec10 and 2cf6333.

📒 Files selected for processing (5)
  • src/lib/services/docker/system-service.ts (1 hunks)
  • src/routes/+page.svelte (2 hunks)
  • src/routes/api/containers/start-all/+server.ts (1 hunks)
  • src/routes/api/containers/stop-all/+server.ts (1 hunks)
  • src/routes/api/system/prune/+server.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/routes/api/containers/stop-all/+server.ts (2)
src/routes/api/containers/start-all/+server.ts (1)
  • POST (5-20)
src/lib/services/docker/container-service.ts (2)
  • listContainers (18-40)
  • stopContainer (102-111)
src/routes/api/containers/start-all/+server.ts (2)
src/routes/api/containers/stop-all/+server.ts (1)
  • POST (5-20)
src/lib/services/docker/container-service.ts (2)
  • listContainers (18-40)
  • startContainer (85-94)
src/lib/services/docker/system-service.ts (2)
src/lib/services/docker/core.ts (1)
  • getDockerClient (24-32)
src/lib/types/errors.ts (1)
  • DockerApiError (55-65)

@kmendell
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Apr 28, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@kmendell kmendell added this to the 0.4.0 milestone Apr 28, 2025
Copy link
Contributor

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

♻️ Duplicate comments (4)
src/lib/services/docker/system-service.ts (1)

14-17: Return type is still any

Previous reviews already suggested introducing a PruneResult interface to avoid any. Consider adopting that recommendation for stronger type-safety.

src/routes/+page.svelte (3)

34-43: Debug logging & refresh-flag reset repeat a previously-noted smell

Both the noisy console.log and the conditional isRefreshing reset were surfaced in an earlier review but remain unchanged. Consider removing the log in production and unconditionally clearing the flag in all code paths (see next comment).


61-67: Robust JSON parsing & status handling – issue previously raised

response.json() throws on non-JSON bodies (HTML 500 pages, 204 No Content, text errors).
A safer pattern is:

- const result = await response.json();
+ const text = await response.text();
+ let result: any = {};
+ try {
+   result = text ? JSON.parse(text) : {};
+ } catch {
+   result = { message: text };
+ }

Apply the same change to handleStopAll() and confirmPrune() to avoid unhandled exceptions on server errors.

Also applies to: 83-87, 113-118


44-55: ⚠️ Potential issue

isRefreshing isn’t cleared on the success path → UI can stay locked

If invalidateAll() resolves without changing the data object, the $effect above won’t fire and the flag never resets.

 async function refreshData() {
   if (isRefreshing) return;
-  console.log('Refreshing dashboard data...');
   isRefreshing = true;
   try {
     await invalidateAll();
-  } catch (err) {
+  } catch (err) {
     console.error('Error during dashboard refresh:', err);
     error = 'Failed to refresh dashboard data.';
-    isRefreshing = false;
+  } finally {
+    // Always release the loading state
+    isRefreshing = false;
   }
 }
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 2cf6333 and a7c964a.

📒 Files selected for processing (5)
  • src/lib/components/dialogs/prune-confirmation-dialog.svelte (1 hunks)
  • src/lib/services/docker/system-service.ts (1 hunks)
  • src/routes/+page.server.ts (2 hunks)
  • src/routes/+page.svelte (3 hunks)
  • src/routes/api/system/prune/+server.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/routes/api/system/prune/+server.ts (2)
src/lib/services/docker/system-service.ts (1)
  • pruneSystem (14-87)
src/lib/utils.ts (1)
  • formatBytes (22-29)
src/lib/services/docker/system-service.ts (3)
src/lib/services/docker/core.ts (1)
  • getDockerClient (24-32)
src/lib/services/settings-service.ts (1)
  • getSettings (128-181)
src/lib/types/errors.ts (1)
  • DockerApiError (55-65)
🪛 Biome (1.9.4)
src/routes/api/system/prune/+server.ts

[error] 33-33: This let declares a variable that is only assigned once.

'errorMessages' is never reassigned.

Safe fix: Use const instead.

(lint/style/useConst)

src/lib/services/docker/system-service.ts

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

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (2)
src/lib/services/docker/system-service.ts (1)

82-86: error.statusCode may be undefined

Most errors thrown by dockerode are NodeJS.ErrnoExceptions without statusCode. Pass a default or guard:

-throw new DockerApiError(`Failed to initiate system prune: ${error.message || 'Unknown Docker error'}`, error.statusCode);
+throw new DockerApiError(
+  `Failed to initiate system prune: ${error?.message ?? 'Unknown Docker error'}`,
+  (error as any)?.statusCode
+);
src/routes/api/system/prune/+server.ts (1)

50-58:

Details

❓ Verification inconclusive

HTTP 200 with success:false may confuse callers

For partial failures you currently return status 200 but success:false.
Consider:

  1. HTTP 207 (Multi-Status) – conveys partial success.
  2. HTTP 500/502 with a payload detailing which parts failed.
  3. Keep 200 but set a distinct field (partialSuccess: true).

Whichever you choose, document it in the API contract so frontend and third-party consumers can handle it predictably.


HTTP status for partial failures
Returning HTTP 200 with success: false can be ambiguous for clients. Please choose one clear pattern and document it in your API contract:

• HTTP 207 Multi-Status (RFC 4918) for partial failures
• A 4xx/5xx status with a detailed payload of successes/failures
• Keep 200 but introduce a distinct flag (e.g. partialSuccess: true)

File to update:

  • src/routes/api/system/prune/+server.ts (around the return json(…, { status: 200 }) on partial errors)

Ensure your OpenAPI spec or docs reflect whichever approach you adopt so frontend and third-party consumers can handle it predictably.

Comment on lines +22 to 42
// Fetch all data concurrently, including settings
const [dockerInfo, containers, images, settings] = await Promise.all([
getDockerInfo().catch((e) => {
console.error('Dashboard: Failed to get Docker info:', e.message);
return null; // Return null on error for this specific piece
return null;
}),
listContainers(true).catch((e) => {
// Ensure options object if needed
console.error('Dashboard: Failed to list containers:', e.message);
return []; // Return empty array on error
return [];
}),
listImages().catch((e) => {
console.error('Dashboard: Failed to list images:', e.message);
return []; // Return empty array on error
return [];
}),
getSettings().catch((e) => {
// Fetch settings
console.error('Dashboard: Failed to get settings:', e.message);
return null; // Handle settings fetch error
})
]);
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Consider early-bailing on Docker unavailability to save 3 failing calls

Promise.all fires off all four requests even when the Docker daemon is down.
listContainers, listImages, and even getSettings (if it runs through the same transport) are then guaranteed to fail, costing ~3 network round-trips and polluting the logs.

A small sequential pre-check avoids the noise:

- const [dockerInfo, containers, images, settings] = await Promise.all([
-   getDockerInfo() ...
-   listContainers(true) ...
-   listImages() ...
-   getSettings() ...
- ]);
+ const dockerInfo = await getDockerInfo().catch((e) => {
+   console.error('Dashboard: Failed to get Docker info:', e.message);
+   return null;
+ });
+
+ if (!dockerInfo) {
+   // still fetch settings so the UI can render prune-mode
+   const settings = await getSettings().catch(() => null);
+   return {
+     dockerInfo: null,
+     containers: [],
+     images: [],
+     settings: settings ? { pruneMode: settings.pruneMode } : null,
+     error:
+       'Failed to connect to Docker Engine. Please check settings and ensure Docker is running.'
+   };
+ }
+
+ // Docker is up – now fetch the heavier lists in parallel
+ const [containers, images, settings] = await Promise.all([
+   listContainers(true).catch(() => []),
+   listImages().catch(() => []),
+   getSettings().catch(() => null)
+ ]);

This keeps the fallback behaviour intact while sparing unnecessary work and noisy stack-traces.

📝 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
// Fetch all data concurrently, including settings
const [dockerInfo, containers, images, settings] = await Promise.all([
getDockerInfo().catch((e) => {
console.error('Dashboard: Failed to get Docker info:', e.message);
return null; // Return null on error for this specific piece
return null;
}),
listContainers(true).catch((e) => {
// Ensure options object if needed
console.error('Dashboard: Failed to list containers:', e.message);
return []; // Return empty array on error
return [];
}),
listImages().catch((e) => {
console.error('Dashboard: Failed to list images:', e.message);
return []; // Return empty array on error
return [];
}),
getSettings().catch((e) => {
// Fetch settings
console.error('Dashboard: Failed to get settings:', e.message);
return null; // Handle settings fetch error
})
]);
// Fetch all data concurrently, including settings
const dockerInfo = await getDockerInfo().catch((e) => {
console.error('Dashboard: Failed to get Docker info:', e.message);
return null;
});
if (!dockerInfo) {
// still fetch settings so the UI can render prune-mode
const settings = await getSettings().catch(() => null);
return {
dockerInfo: null,
containers: [],
images: [],
settings: settings ? { pruneMode: settings.pruneMode } : null,
error:
'Failed to connect to Docker Engine. Please check settings and ensure Docker is running.'
};
}
// Docker is up – now fetch the heavier lists in parallel
const [containers, images, settings] = await Promise.all([
listContainers(true).catch(() => []),
listImages().catch(() => []),
getSettings().catch(() => null)
]);

@kmendell
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Apr 28, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

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

♻️ Duplicate comments (10)
src/routes/api/containers/stop-all/+server.ts (1)

1-20: Implementation looks good, but consider implementing more robust error handling.

The endpoint effectively filters and stops running containers in parallel, with proper basic error handling. However, stopping containers in parallel might have edge cases worth addressing.

Consider enhancing the error handling to capture specific Docker errors and provide more detailed feedback to the client:

 await Promise.all(running.map((c) => stopContainer(c.id)));
+
+// Alternative implementation with better error handling:
+// const results = await Promise.allSettled(running.map((c) => stopContainer(c.id)));
+// const succeeded = results.filter(r => r.status === 'fulfilled').length;
+// const failed = results.filter(r => r.status === 'rejected');
+// 
+// if (failed.length > 0) {
+//   console.error(`API: Failed to stop ${failed.length} containers:`, 
+//     failed.map(f => (f as PromiseRejectedResult).reason));
+// }
+// 
+// return json({ 
+//   success: succeeded > 0, 
+//   count: succeeded, 
+//   failedCount: failed.length,
+//   message: `Successfully stopped ${succeeded} container(s). Failed to stop ${failed.length} container(s).` 
+// });
src/routes/api/containers/start-all/+server.ts (1)

1-20: Implementation looks good, consider improving error handling.

The function effectively starts all stopped containers in parallel with proper error handling, but could benefit from more robust error handling similar to the stop-all endpoint.

Consider using Promise.allSettled to handle individual container start failures:

 await Promise.all(stopped.map((c) => startContainer(c.id)));
+
+// Alternative implementation with better error handling:
+// const results = await Promise.allSettled(stopped.map((c) => startContainer(c.id)));
+// const succeeded = results.filter(r => r.status === 'fulfilled').length;
+// const failed = results.filter(r => r.status === 'rejected');
+// 
+// if (failed.length > 0) {
+//   console.error(`API: Failed to start ${failed.length} containers:`, 
+//     failed.map(f => (f as PromiseRejectedResult).reason));
+// }
+// 
+// return json({ 
+//   success: succeeded > 0, 
+//   count: succeeded, 
+//   failedCount: failed.length,
+//   message: `Successfully started ${succeeded} container(s). Failed to start ${failed.length} container(s).` 
+// });
src/routes/api/system/prune/+server.ts (2)

33-33: 🧹 Nitpick (assertive)

Use const for errorMessages.

errorMessages is never reassigned, only mutated. Using const would be more appropriate and follows best practices.

-		let hasErrors = false;
-		const errorMessages: string[] = [];
+		let hasErrors = false;
+		const errorMessages: string[] = [];

10-26: 💡 Verification agent

🧩 Analysis chain

Add confirmation parameter to prevent accidental pruning.

Pruning Docker resources can result in permanent data loss. Consider adding a confirmation parameter to prevent accidental triggering of this destructive operation.

-export const POST: RequestHandler = async ({ url }) => {
+export const POST: RequestHandler = async ({ url, request }) => {
+	// Require explicit confirmation
+	try {
+		const { confirm } = await request.json().catch(() => ({}));
+		if (confirm !== true) {
+			return json(
+				{ success: false, message: 'Confirmation required for system prune.' },
+				{ status: 400 }
+			);
+		}
+	} catch (error) {
+		throw serverError(400, 'Invalid request body, confirmation required for system prune.');
+	}
+
 	// Get types from query parameters, e.g., /api/docker/system/prune?types=containers,images

🏁 Script executed:

#!/bin/bash
# Check if there's a confirmation mechanism in place (in frontend)
rg -n "prune.*confirm|confirm.*prune" --glob "*.svelte"

Length of output: 401


To confirm whether the frontend already includes a confirm flag in its request payload, let’s inspect the confirmPrune implementation:


🏁 Script executed:

#!/bin/bash
# Show the lines around the confirmPrune function in +page.svelte
rg -n "confirmPrune" -C 10 src/routes/+page.svelte

Length of output: 1165


Require explicit confirmation for system prune

To prevent accidental or unauthorized API calls (e.g., direct POST requests), enforce an explicit confirm=true flag in the prune handler and update the UI to send it.

• src/routes/api/system/prune/+server.ts
– Read and validate confirm from url.searchParams before parsing types.
– Return a 400 error if confirm !== 'true'.

• src/routes/+page.svelte
– In confirmPrune, append &confirm=true to the fetch URL (or send { confirm: true } in the POST body).

Suggested diffs:

--- a/src/routes/api/system/prune/+server.ts
+++ b/src/routes/api/system/prune/+server.ts
@@ export const POST: RequestHandler = async ({ url }) => {
-	// Get types from query parameters, e.g., /api/docker/system/prune?types=containers,images
+	// Require explicit confirmation to proceed
+	const confirmParam = url.searchParams.get('confirm');
+	if (confirmParam !== 'true') {
+		throw serverError(400, 'Confirmation required for system prune.');
+	}
+	// Get types from query parameters, e.g., /api/docker/system/prune?types=containers,images&confirm=true
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ async function confirmPrune(selectedTypes: string[]) {
-		const apiUrl = `/api/system/prune?types=${encodeURIComponent(selectedTypes.join(','))}`;
+		const apiUrl =
+			`/api/system/prune?types=${encodeURIComponent(selectedTypes.join(','))}&confirm=true`;
src/lib/services/docker/system-service.ts (1)

37-37: 💡 Verification agent

🧩 Analysis chain

Add an explicit warning about volume pruning and data loss.

Volume pruning can cause permanent data loss. The current comment is only in a comment, but should be emphasized more clearly.

-					result = await docker.pruneVolumes();
+					// WARNING: Volume pruning permanently deletes all unused volumes and their data!
+					result = await docker.pruneVolumes();

Let's verify the Docker documentation about volume pruning risks:


🌐 Web query:

What are the risks of Docker volume pruning?

💡 Result:

Docker volume pruning is a useful operation for reclaiming disk space, but it comes with several significant risks that users should be aware of before execution.

Data Loss Risks

The primary risk of Docker volume pruning is permanent data loss. When you run docker volume prune, it removes all volumes that are not currently attached to any containers[5]. However, there's an important nuance here - the command removes volumes that aren't referenced by any running containers[5]. This means if your containers are stopped at the time of pruning, Docker will consider their volumes as "unused" and remove them, even if those volumes contain data you need[5].

This distinction between "referenced by containers" and "referenced by running containers" is particularly dangerous for beginners who might not fully understand the implications[5]. Many users have lost all their persistent data by pruning volumes while containers were stopped[5].

Production Environment Concerns

Running pruning commands in production environments requires extra caution. While some forum discussions suggest that docker volume prune is generally safe[2], other commands like docker system prune are explicitly noted as not safe for production use[1]. The system prune command may clean up and reclaim space but carries the risk that one or more containers could die as a result[1].

Sentry-Specific Concerns

For specific applications like Sentry, pruning can be particularly destructive. According to forum discussions, running docker system prune on an active Sentry installation can cause significant problems[4]. When asked if any Docker resources (images, volumes, or containers) could be safely pruned in a Sentry environment, the response was "Nope, you need everything"[4].

Mitigation Strategies

To safely use Docker pruning commands:

  1. Always check what will be removed before pruning by using commands like docker ps -a, docker images, and docker volume ls[3]

  2. Be particularly careful with the -a and -f flags, which remove everything forcefully and increase the risk of data loss[3]

  3. For targeted cleaning, use specific commands like docker image prune (which only deletes dangling images) rather than the broader system prune[3]

  4. Most importantly, ensure you have proper backups before running any pruning operations, especially in production environments

  5. If possible, run pruning operations when containers that need their volumes are actually running, to prevent accidental deletion of important data

The key takeaway is that while Docker's pruning commands are powerful tools for maintenance, they should be used with a complete understanding of their scope and potential impact on your data and running applications.

Citations:


Add explicit warning about irreversible data loss when pruning volumes

Volume pruning permanently deletes all unused volumes (including those attached to stopped containers), along with all their data. Please ensure you’ve backed up any important volume data and double-check which volumes are in use before running this command.

  • File: src/lib/services/docker/system-service.ts, line 37
-   result = await docker.pruneVolumes();
+   // WARNING: Volume pruning permanently deletes all unused volumes 
+   // (including those attached to stopped containers) and their data! 
+   // Be sure to back up critical data and verify volumes in use before running.
+   result = await docker.pruneVolumes();
src/routes/+page.server.ts (1)

23-42: 🧹 Nitpick (assertive)

Consider adding early-return for Docker unavailability to avoid unnecessary API calls.

The current implementation makes all API calls in parallel even if Docker is unavailable, leading to multiple failures and error logs.

-		// Fetch all data concurrently, including settings
-		const [dockerInfo, containers, images, settings] = await Promise.all([
-			getDockerInfo().catch((e) => {
-				console.error('Dashboard: Failed to get Docker info:', e.message);
-				return null;
-			}),
-			listContainers(true).catch((e) => {
-				// Ensure options object if needed
-				console.error('Dashboard: Failed to list containers:', e.message);
-				return [];
-			}),
-			listImages().catch((e) => {
-				console.error('Dashboard: Failed to list images:', e.message);
-				return [];
-			}),
-			getSettings().catch((e) => {
-				// Fetch settings
-				console.error('Dashboard: Failed to get settings:', e.message);
-				return null; // Handle settings fetch error
-			})
-		]);
+		// First check if Docker is available
+		const dockerInfo = await getDockerInfo().catch((e) => {
+			console.error('Dashboard: Failed to get Docker info:', e.message);
+			return null;
+		});
+		
+		// Always fetch settings regardless of Docker availability
+		const settings = await getSettings().catch((e) => {
+			console.error('Dashboard: Failed to get settings:', e.message);
+			return null;
+		});
+		
+		// Early return if Docker is unavailable
+		if (!dockerInfo) {
+			return {
+				dockerInfo: null,
+				containers: [],
+				images: [],
+				settings: settings ? { pruneMode: settings.pruneMode } : null,
+				error: 'Failed to connect to Docker Engine. Please check settings and ensure Docker is running.'
+			};
+		}
+		
+		// Docker is available, fetch the remaining data
+		const [containers, images] = await Promise.all([
+			listContainers(true).catch((e) => {
+				console.error('Dashboard: Failed to list containers:', e.message);
+				return [];
+			}),
+			listImages().catch((e) => {
+				console.error('Dashboard: Failed to list images:', e.message);
+				return [];
+			})
+		]);
src/routes/+page.svelte (4)

82-82: ⚠️ Potential issue

Apply the same JSON parsing fix to handleStopAll.

The same issue exists in the stop all containers handler.

-			const result = await response.json();
+			const text = await response.text();
+			let result;
+			try {
+				result = text ? JSON.parse(text) : {};
+			} catch (err) {
+				result = { message: text || 'Invalid response from server' };
+			}

113-113: ⚠️ Potential issue

Apply the same JSON parsing fix to confirmPrune.

The same issue exists in the prune system handler.

-			const result = await response.json();
+			const text = await response.text();
+			let result;
+			try {
+				result = text ? JSON.parse(text) : {};
+			} catch (err) {
+				result = { message: text || 'Invalid response from server' };
+			}

111-111: 🛠️ Refactor suggestion

URL-encode the types parameter to avoid malformed requests.

Using encodeURIComponent ensures the query parameter is properly formatted, especially if it contains special characters.

-			const apiUrl = `/api/system/prune?types=${selectedTypes.join(',')}`;
+			const apiUrl = `/api/system/prune?types=${encodeURIComponent(selectedTypes.join(','))}`;

60-61: ⚠️ Potential issue

Improve error handling for API responses.

The current implementation assumes the response will always be valid JSON. If the server returns a non-JSON response (like an HTML error page), this will throw an unhandled exception.

-			const response = await fetch('/api/containers/start-all', { method: 'POST' });
-			const result = await response.json();
+			const response = await fetch('/api/containers/start-all', { method: 'POST' });
+			const text = await response.text();
+			let result;
+			try {
+				result = text ? JSON.parse(text) : {};
+			} catch (err) {
+				result = { message: text || 'Invalid response from server' };
+			}
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between a7c964a and 83e80bb.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (7)
  • src/lib/services/docker/system-service.ts (1 hunks)
  • src/lib/types/docker/prune.type.ts (1 hunks)
  • src/routes/+page.server.ts (2 hunks)
  • src/routes/+page.svelte (3 hunks)
  • src/routes/api/containers/start-all/+server.ts (1 hunks)
  • src/routes/api/containers/stop-all/+server.ts (1 hunks)
  • src/routes/api/system/prune/+server.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/routes/api/containers/stop-all/+server.ts (2)
src/routes/api/containers/start-all/+server.ts (1)
  • POST (5-20)
src/lib/services/docker/container-service.ts (2)
  • listContainers (18-40)
  • stopContainer (102-111)
src/routes/api/containers/start-all/+server.ts (2)
src/routes/api/containers/stop-all/+server.ts (1)
  • POST (5-20)
src/lib/services/docker/container-service.ts (2)
  • listContainers (18-40)
  • startContainer (85-94)
src/lib/services/docker/system-service.ts (2)
src/lib/types/docker/prune.type.ts (1)
  • PruneResult (4-10)
src/lib/services/docker/core.ts (1)
  • getDockerClient (24-32)
🔇 Additional comments (2)
src/lib/types/docker/prune.type.ts (1)

1-10: LGTM! The PruneResult interface covers all Docker prune operation outputs.

The interface accurately represents the structure of Docker prune operation results, with all fields correctly marked as optional since different prune operations may return different subsets of fields.

src/routes/api/system/prune/+server.ts (1)

49-52: Great job with human-readable space reclamation reporting!

The use of formatBytes to convert raw bytes to human-readable format is a thoughtful touch for the response message.

Comment on lines +6 to +7
ImagesDeleted?: any[];
NetworksDeleted?: string[];
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Consider using a more specific type for ImagesDeleted.

The current type for ImagesDeleted is any[] which is quite permissive and loses type safety. Since Docker's API returns image deletion objects with specific properties, a more specific type would be beneficial.

-	ImagesDeleted?: any[];
+	ImagesDeleted?: Array<{
+		Untagged?: string;
+		Deleted?: string;
+	}>;
📝 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
ImagesDeleted?: any[];
NetworksDeleted?: string[];
ImagesDeleted?: Array<{
Untagged?: string;
Deleted?: string;
}>;
NetworksDeleted?: string[];
🤖 Prompt for AI Agents (early access)
In src/lib/types/docker/prune.type.ts at lines 6 to 7, the ImagesDeleted property is typed as any[], which lacks type safety. Replace any[] with a more specific type that matches the structure of Docker's image deletion objects, such as an array of objects with defined properties like Id and Deleted or Untagged. Define an interface or type alias for these objects and use it for ImagesDeleted to improve type safety and clarity.

Comment on lines +35 to +47
if (Array.isArray(results)) {
results.forEach((res) => {
if (res) {
if (typeof res.SpaceReclaimed === 'number') {
totalSpaceReclaimed += res.SpaceReclaimed;
}
if (res.error) {
hasErrors = true;
errorMessages.push(`${res.type || 'Unknown'}: ${res.error}`);
}
}
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Simplify the error and space collection logic.

The current implementation uses nested if statements which can be simplified for better readability.

		if (Array.isArray(results)) {
-			results.forEach((res) => {
-				if (res) {
-					if (typeof res.SpaceReclaimed === 'number') {
-						totalSpaceReclaimed += res.SpaceReclaimed;
-					}
-					if (res.error) {
-						hasErrors = true;
-						errorMessages.push(`${res.type || 'Unknown'}: ${res.error}`);
-					}
-				}
-			});
+			results.forEach((res) => {
+				if (!res) return;
+				
+				// Add space reclaimed
+				if (typeof res.SpaceReclaimed === 'number') {
+					totalSpaceReclaimed += res.SpaceReclaimed;
+				}
+				
+				// Collect errors
+				if (res.error) {
+					hasErrors = true;
+					errorMessages.push(`${res.type || 'Unknown'}: ${res.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
if (Array.isArray(results)) {
results.forEach((res) => {
if (res) {
if (typeof res.SpaceReclaimed === 'number') {
totalSpaceReclaimed += res.SpaceReclaimed;
}
if (res.error) {
hasErrors = true;
errorMessages.push(`${res.type || 'Unknown'}: ${res.error}`);
}
}
});
}
if (Array.isArray(results)) {
results.forEach((res) => {
if (!res) return;
// Add space reclaimed
if (typeof res.SpaceReclaimed === 'number') {
totalSpaceReclaimed += res.SpaceReclaimed;
}
// Collect errors
if (res.error) {
hasErrors = true;
errorMessages.push(`${res.type || 'Unknown'}: ${res.error}`);
}
});
}
🤖 Prompt for AI Agents (early access)
In src/routes/api/system/prune/+server.ts around lines 35 to 47, simplify the nested if statements inside the results.forEach loop by combining conditions where possible and using early returns or logical operators to reduce nesting. This will improve readability while maintaining the logic that accumulates totalSpaceReclaimed and collects error messages.

Comment on lines +56 to +58
console.warn('Prune completed with errors:', errorMessages);
// Return 207 Multi-Status might be appropriate, but 200 with error details is often simpler
return json({ success: false, results, spaceReclaimed: totalSpaceReclaimed, message }, { status: 200 }); // Indicate partial failure in payload
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Consider using a true HTTP error status for failures.

Currently, the API returns 200 OK status even when errors occur, relying on the success: false property in the JSON response. Standard practice is to use appropriate HTTP status codes to indicate issues.

			console.warn('Prune completed with errors:', errorMessages);
			// Return 207 Multi-Status might be appropriate, but 200 with error details is often simpler
-			return json({ success: false, results, spaceReclaimed: totalSpaceReclaimed, message }, { status: 200 }); // Indicate partial failure in payload
+			return json({ success: false, results, spaceReclaimed: totalSpaceReclaimed, message }, { status: 207 }); // Multi-Status is more appropriate for partial success
📝 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
console.warn('Prune completed with errors:', errorMessages);
// Return 207 Multi-Status might be appropriate, but 200 with error details is often simpler
return json({ success: false, results, spaceReclaimed: totalSpaceReclaimed, message }, { status: 200 }); // Indicate partial failure in payload
console.warn('Prune completed with errors:', errorMessages);
// Return 207 Multi-Status might be appropriate, but 200 with error details is often simpler
- return json({ success: false, results, spaceReclaimed: totalSpaceReclaimed, message }, { status: 200 }); // Indicate partial failure in payload
+ return json({ success: false, results, spaceReclaimed: totalSpaceReclaimed, message }, { status: 207 }); // Multi-Status is more appropriate for partial success
🤖 Prompt for AI Agents (early access)
In src/routes/api/system/prune/+server.ts around lines 56 to 58, the code returns a 200 OK status even when errors occur, which is not standard practice. Update the response to use a more appropriate HTTP status code such as 207 Multi-Status or another relevant error status to indicate partial or complete failure, while still including the error details in the JSON payload. This change will align the API with standard HTTP conventions for error handling.

}
}

console.log('Docker System Prune results:', results);
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Consider logging at debug level for production environments.

The detailed results logging may be excessive in a production environment.

-	console.log('Docker System Prune results:', results);
+	// In production, consider using a more sophisticated logging system with levels
+	// For example: logger.debug('Docker System Prune results:', results);
+	console.log('Docker System Prune results:', results);
📝 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
console.log('Docker System Prune results:', results);
// In production, consider using a more sophisticated logging system with levels
// For example: logger.debug('Docker System Prune results:', results);
console.log('Docker System Prune results:', results);
🤖 Prompt for AI Agents (early access)
In src/lib/services/docker/system-service.ts at line 65, replace the console.log statement with a debug-level log using the appropriate logger for the project. This change ensures detailed Docker System Prune results are logged only in debug mode, reducing noise in production logs. Use the existing logging framework's debug method instead of console.log.

// Add filters like dangling=true if needed: { filters: '{"dangling": ["true"]}' }
// Note: Pruning all images might take a long time and remove needed images.
// Consider adding filters based on settings (e.g., pruneMode)
result = await docker.pruneImages({ filters: '{"dangling": ["true"]}' }); // Example: Only prune dangling images
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix the filters parameter format for pruneImages.

The filters parameter is incorrectly passed as a JSON string, but the dockerode API expects an object structure.

-					result = await docker.pruneImages({ filters: '{"dangling": ["true"]}' }); // Example: Only prune dangling images
+					result = await docker.pruneImages({ filters: { dangling: ['true'] } }); // Example: Only prune dangling images
📝 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
result = await docker.pruneImages({ filters: '{"dangling": ["true"]}' }); // Example: Only prune dangling images
result = await docker.pruneImages({ filters: { dangling: ['true'] } }); // Example: Only prune dangling images
🤖 Prompt for AI Agents (early access)
In src/lib/services/docker/system-service.ts at line 29, the filters parameter for pruneImages is incorrectly passed as a JSON string. Change the filters argument to be an object with the dangling property set to an array containing the string "true" instead of a JSON string. This aligns with the dockerode API expectation for the filters parameter.

dockerInfo: null,
containers: [],
images: [],
settings: settings ? { pruneMode: settings.pruneMode } : null, // Still return settings if available
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Extract common settings extraction logic to avoid duplication.

The same code for extracting pruneMode from settings is duplicated in two places.

+		// Helper function to extract pruneMode from settings
+		const extractPruneMode = (settings: SettingsType | null) => 
+			settings ? { pruneMode: settings.pruneMode } : null;
+
 		// Check if the primary connection failed
 		if (!dockerInfo) {
 			return {
 				dockerInfo: null,
 				containers: [],
 				images: [],
-				settings: settings ? { pruneMode: settings.pruneMode } : null, // Still return settings if available
+				settings: extractPruneMode(settings), // Still return settings if available
 				error: 'Failed to connect to Docker Engine. Please check settings and ensure Docker is running.'
 			};
 		}
 
 		return {
 			dockerInfo,
 			containers,
 			images,
-			settings: settings ? { pruneMode: settings.pruneMode } : null // Pass only pruneMode
+			settings: extractPruneMode(settings) // Pass only pruneMode
 		};

Also applies to: 59-59

🤖 Prompt for AI Agents (early access)
In src/routes/+page.server.ts at lines 50 and 59, the extraction of pruneMode from settings is duplicated. Refactor by creating a helper function or variable that extracts pruneMode from settings once, then reuse this extraction in both places to avoid code duplication and improve maintainability.

Comment on lines +40 to +41
isRefreshing = false;
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Ensure isRefreshing is properly reset in all cases.

The isRefreshing flag is reset in the effect and the catch block of refreshData, but it should be reset in a finally block to ensure it's always cleared regardless of the outcome.

 	$effect(() => {
 		dockerInfo = data.dockerInfo;
 		containers = data.containers;
 		images = data.images;
 		settings = data.settings;
 		error = data.error;
-		isRefreshing = false;
+		// Don't reset isRefreshing here, as it could interfere with manual refresh
 	});

 	async function refreshData() {
 		if (isRefreshing) return;
 		isRefreshing = true;
 		try {
 			await invalidateAll();
 		} catch (err) {
 			console.error('Error during dashboard refresh:', err);
 			error = 'Failed to refresh dashboard data.';
-		} finally {
-			isRefreshing = false;
 		}
+		finally {
+			isRefreshing = false;
+		}
 	}

Also applies to: 52-52

🤖 Prompt for AI Agents (early access)
In src/routes/+page.svelte around lines 40-41 and line 52, the isRefreshing flag is reset only in the effect and catch blocks of refreshData, which risks it not being cleared in some cases. Refactor the refreshData function to reset isRefreshing inside a finally block so that it is always cleared regardless of success or failure, ensuring consistent state management.

Comment on lines +119 to +123
if (!result.success && result.message) {
toast.warning(result.message);
} else {
toast.success(result.message || 'System prune completed.');
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Simplify success/failure handling logic.

The logic for handling success and failure messages is unnecessarily complex. If result.success is false, it's treated as a warning, otherwise it's a success.

-			if (!result.success && result.message) {
-				toast.warning(result.message);
-			} else {
-				toast.success(result.message || 'System prune completed.');
-			}
+			if (result.success) {
+				toast.success(result.message || 'System prune completed.');
+			} else if (result.message) {
+				toast.warning(result.message);
+			}
📝 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
if (!result.success && result.message) {
toast.warning(result.message);
} else {
toast.success(result.message || 'System prune completed.');
}
if (result.success) {
toast.success(result.message || 'System prune completed.');
} else if (result.message) {
toast.warning(result.message);
}
🤖 Prompt for AI Agents (early access)
In src/routes/+page.svelte around lines 119 to 123, simplify the conditional logic by directly checking result.success to decide whether to show a warning or success toast. If result.success is false, show a warning with result.message; otherwise, show a success toast with result.message or a default message. Remove redundant checks to make the code clearer and more concise.

async function confirmPrune(selectedTypes: string[]) {
if (isPruning || selectedTypes.length === 0) return;

console.log(`Attempting to prune types: ${selectedTypes.join(', ')}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Consider removing or conditionally enabling console.log in production.

Debug logging should typically be removed from production code or made conditional.

-		console.log(`Attempting to prune types: ${selectedTypes.join(', ')}`);
+		// For development only, remove in production or use a proper logger
+		if (import.meta.env.DEV) {
+			console.log(`Attempting to prune types: ${selectedTypes.join(', ')}`);
+		}
📝 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
console.log(`Attempting to prune types: ${selectedTypes.join(', ')}`);
// For development only, remove in production or use a proper logger
if (import.meta.env.DEV) {
console.log(`Attempting to prune types: ${selectedTypes.join(', ')}`);
}
🤖 Prompt for AI Agents (early access)
In src/routes/+page.svelte at line 106, the console.log statement used for debugging should be removed or wrapped in a condition that checks if the environment is development. Implement a check using an environment variable or a similar mechanism to ensure this log only appears during development and is disabled in production builds.

Comment on lines +115 to +117
if (!response.ok && !result.success) {
throw new Error(result.message || `Failed to prune system (status ${response.status})`);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Simplify the condition check for response success.

The current condition is redundantly complex. If !response.ok is true, we should throw regardless of result.success.

-			if (!response.ok && !result.success) {
+			if (!response.ok) {
 				throw new Error(result.message || `Failed to prune system (status ${response.status})`);
 			}
📝 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
if (!response.ok && !result.success) {
throw new Error(result.message || `Failed to prune system (status ${response.status})`);
}
if (!response.ok) {
throw new Error(result.message || `Failed to prune system (status ${response.status})`);
}
🤖 Prompt for AI Agents (early access)
In src/routes/+page.svelte around lines 115 to 117, simplify the condition by removing the redundant check for result.success. Change the if statement to throw an error whenever !response.ok is true, ignoring result.success, to make the logic clearer and more straightforward.

@kmendell kmendell merged commit f82380b into main Apr 28, 2025
1 check passed
@kmendell kmendell deleted the feat/quick-actions branch April 28, 2025 16:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

⚡️ Feature: Quick Action Buttons Functionality

1 participant