Skip to content

feat: auto update containers and stacks#83

Merged
kmendell merged 14 commits intomainfrom
feat/autoupdate
Apr 29, 2025
Merged

feat: auto update containers and stacks#83
kmendell merged 14 commits intomainfrom
feat/autoupdate

Conversation

@kmendell
Copy link
Member

@kmendell kmendell commented Apr 28, 2025

Summary by CodeRabbit

  • New Features
    • Introduced automatic update checks for containers and stacks, with configurable intervals.
    • Added UI switches and options to enable or disable auto-update for containers and stacks.
    • Added a new API endpoint to manually trigger auto-update checks and to view current auto-update status.
  • Improvements
    • Settings now include a configurable auto-update interval (5–1440 minutes).
    • Stack and container creation/editing dialogs now support toggling auto-update.
    • Container details page redesigned with tabbed interface for better navigation and enhanced metrics display.
  • Bug Fixes
    • Improved error handling and feedback for auto-update operations and settings updates.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Apr 28, 2025

Walkthrough

This change introduces an automated update system for Docker containers and stacks, integrating both backend services and frontend controls. New services are added to periodically check and apply updates to containers and stacks labeled or flagged for auto-update, based on configurable global settings. The backend exposes new API endpoints for manual and scheduled update checks, and settings now include an interval for auto-update checks. The frontend is updated to allow users to enable or disable auto-update for containers and stacks and to configure the auto-update interval in the application settings UI.

Changes

File(s) Change Summary
src/hooks.server.ts Modified initialization to concurrently start both compose and auto-update scheduler services with error handling.
src/lib/services/docker/auto-update-service.ts New module implementing logic to check and update containers and stacks flagged for auto-update.
src/lib/services/docker/scheduler-service.ts New scheduler service to periodically run auto-update checks based on settings.
src/lib/services/docker/container-service.ts Extended getContainer to include container labels; added recreateContainer to recreate containers with updated images.
src/lib/services/docker/stack-service.ts Extended stack metadata to include autoUpdate; added listStacks to list managed and optionally external stacks; updated stack update logic to support autoUpdate.
src/lib/services/settings-service.ts, src/lib/types/settings.ts Added autoUpdateInterval property to default settings and settings type definition.
src/lib/types/docker/stack.type.ts Extended StackMeta, Stack, and StackUpdate interfaces to support autoUpdate.
src/routes/api/stacks/[stackId]/+server.ts Refactored PATCH handler to support updating autoUpdate property and simplified error handling.
src/routes/api/system/auto-update/+server.ts New API route for retrieving auto-update status and manually triggering update checks.
src/routes/containers/create-container-dialog.svelte Added UI and logic to enable/disable auto-update label when creating containers.
src/routes/settings/+page.server.ts, src/routes/settings/tabs/app-settings.svelte Added support for configuring and saving auto-update interval in settings UI and backend.
src/routes/stacks/[stackId]/+page.server.ts, src/routes/stacks/[stackId]/+page.svelte Added UI and backend support for toggling autoUpdate on stacks, including state management and form handling.
src/routes/containers/[id]/+page.svelte Refactored container details page to use tabbed interface and updated UI components; no direct auto-update logic but part of UI improvements.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend
    participant API
    participant Scheduler
    participant AutoUpdateService
    participant Docker

    User->>Frontend: Enable auto-update (container/stack or settings)
    Frontend->>API: Save auto-update config (container/stack/settings)
    API->>Scheduler: (On startup/settings change) initAutoUpdateScheduler()
    Scheduler->>AutoUpdateService: Periodic checkAndUpdateContainers()/checkAndUpdateStacks()
    AutoUpdateService->>Docker: List containers/stacks, pull images, restart/redeploy if needed
    AutoUpdateService->>Scheduler: Return update results
    Scheduler->>API: (On manual trigger) Run update checks, return results
    API->>Frontend: Show update status/results
Loading

Possibly related PRs

  • ofkm/arcane#71: Changes the import path of initComposeService() in src/hooks.server.ts, which is related to the initialization logic now updated to include the auto-update scheduler in this PR.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/hooks.server.ts

Oops! Something went wrong! :(

ESLint: 9.25.1

Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@sveltejs/adapter-node' imported from /svelte.config.js
at packageResolve (node:internal/modules/esm/resolve:839:9)
at moduleResolve (node:internal/modules/esm/resolve:908:18)
at defaultResolve (node:internal/modules/esm/resolve:1038:11)
at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:557:12)
at ModuleLoader.resolve (node:internal/modules/esm/loader:525:25)
at ModuleLoader.getModuleJob (node:internal/modules/esm/loader:246:38)
at ModuleJob._link (node:internal/modules/esm/module_job:126:49)
(node:992) ExperimentalWarning: Importing JSON modules is an experimental feature and might change at any time
(Use node --trace-warnings ... to show where the warning was created)

src/lib/services/docker/auto-update-service.ts

Oops! Something went wrong! :(

ESLint: 9.25.1

Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@sveltejs/adapter-node' imported from /svelte.config.js
at packageResolve (node:internal/modules/esm/resolve:839:9)
at moduleResolve (node:internal/modules/esm/resolve:908:18)
at defaultResolve (node:internal/modules/esm/resolve:1038:11)
at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:557:12)
at ModuleLoader.resolve (node:internal/modules/esm/loader:525:25)
at ModuleLoader.getModuleJob (node:internal/modules/esm/loader:246:38)
at ModuleJob._link (node:internal/modules/esm/module_job:126:49)
(node:993) ExperimentalWarning: Importing JSON modules is an experimental feature and might change at any time
(Use node --trace-warnings ... to show where the warning was created)

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

Oops! Something went wrong! :(

ESLint: 9.25.1

Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@sveltejs/adapter-node' imported from /svelte.config.js
at packageResolve (node:internal/modules/esm/resolve:839:9)
at moduleResolve (node:internal/modules/esm/resolve:908:18)
at defaultResolve (node:internal/modules/esm/resolve:1038:11)
at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:557:12)
at ModuleLoader.resolve (node:internal/modules/esm/loader:525:25)
at ModuleLoader.getModuleJob (node:internal/modules/esm/loader:246:38)
at ModuleJob._link (node:internal/modules/esm/module_job:126:49)
(node:994) ExperimentalWarning: Importing JSON modules is an experimental feature and might change at any time
(Use node --trace-warnings ... to show where the warning was created)

  • 13 others
✨ Finishing Touches
  • 📝 Generate Docstrings

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

🔭 Outside diff range comments (1)
src/lib/services/docker/stack-service.ts (1)

486-494: ⚠️ Potential issue

updateStack omits meta in its returned object

Consumers (e.g. the auto-update UI and service layer) rely on stack.meta.
Return the updated metadata to keep the contract consistent with getStack, createStack, and the new listStacks.

 			updatedAt: updatedMeta.updatedAt,
+			meta: updatedMeta
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 9956ffa and 71d20e5.

📒 Files selected for processing (15)
  • src/hooks.server.ts (1 hunks)
  • src/lib/services/docker/auto-update-service.ts (1 hunks)
  • src/lib/services/docker/container-service.ts (1 hunks)
  • src/lib/services/docker/scheduler-service.ts (1 hunks)
  • src/lib/services/docker/stack-service.ts (3 hunks)
  • src/lib/services/settings-service.ts (1 hunks)
  • src/lib/types/docker/stack.type.ts (2 hunks)
  • src/lib/types/settings.ts (1 hunks)
  • src/routes/api/stacks/[stackId]/+server.ts (1 hunks)
  • src/routes/api/system/auto-update/+server.ts (1 hunks)
  • src/routes/containers/create-container-dialog.svelte (4 hunks)
  • src/routes/settings/+page.server.ts (2 hunks)
  • src/routes/settings/tabs/app-settings.svelte (2 hunks)
  • src/routes/stacks/[stackId]/+page.server.ts (3 hunks)
  • src/routes/stacks/[stackId]/+page.svelte (5 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (5)
src/hooks.server.ts (2)
src/lib/services/docker/stack-service.ts (1)
  • initComposeService (22-32)
src/lib/services/docker/scheduler-service.ts (1)
  • initAutoUpdateScheduler (10-35)
src/routes/stacks/[stackId]/+page.server.ts (1)
src/lib/services/docker/stack-service.ts (1)
  • updateStack (449-499)
src/routes/api/system/auto-update/+server.ts (2)
src/lib/services/settings-service.ts (1)
  • getSettings (129-182)
src/lib/services/docker/auto-update-service.ts (2)
  • checkAndUpdateContainers (15-85)
  • checkAndUpdateStacks (90-144)
src/lib/services/docker/stack-service.ts (1)
src/lib/types/docker/stack.type.ts (2)
  • Stack (19-32)
  • StackMeta (1-6)
src/lib/services/docker/scheduler-service.ts (2)
src/lib/services/settings-service.ts (1)
  • getSettings (129-182)
src/lib/services/docker/auto-update-service.ts (2)
  • checkAndUpdateContainers (15-85)
  • checkAndUpdateStacks (90-144)
🪛 Biome (1.9.4)
src/routes/settings/+page.server.ts

[error] 29-29: Use Number.parseInt instead of the equivalent global.

ES2015 moved some globals into the Number namespace for consistency.
Safe fix: Use Number.parseInt instead.

(lint/style/useNumberNamespace)

src/lib/services/docker/auto-update-service.ts

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

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

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

[error] 911-911: This type annotation is trivially inferred from its initialization.

Safe fix: Remove the type annotation.

(lint/style/noInferrableTypes)

🔇 Additional comments (32)
src/lib/types/settings.ts (1)

4-4: LGTM - Clean interface extension

This is a clean addition of the autoUpdateInterval property to the SettingsData interface, using the appropriate numeric type for a time interval value.

src/lib/services/settings-service.ts (1)

19-19: Appropriate default value for auto-update interval

Setting the default auto-update interval to 5 minutes provides a reasonable balance between keeping containers updated and avoiding excessive system load from too-frequent checks.

src/lib/services/docker/container-service.ts (1)

68-69: Good addition of container labels support

Including container labels in the returned object is necessary for the auto-update feature to identify containers marked for automatic updates (via labels like arcane.auto-update=true).

src/routes/containers/create-container-dialog.svelte (4)

16-16: Appropriate import for the auto-update toggle.

The Switch component import is correctly added for the auto-update feature implementation.


90-92: Good addition of auto-update state.

The addition of a reactive state variable for auto-update is clear and follows the established pattern in the component.


225-229: Well-implemented auto-update label logic.

The conditional addition of the arcane.auto-update label is a clean implementation that properly marks containers for auto-update when the feature is enabled by the user.


618-624: UI for auto-update is well integrated.

The auto-update toggle UI is clearly implemented with appropriate labeling and description text. It's properly placed in the Advanced tab section with other configuration options.

src/routes/settings/+page.server.ts (2)

31-33: Good interval validation logic.

The validation logic ensures the auto-update interval stays within reasonable bounds (5 minutes to 24 hours), which is important for preventing system overload from too-frequent checks or excessively delayed updates.


97-97: Properly adding autoUpdateInterval to the settings.

The validated interval value is correctly included in the settings object that will be saved.

src/lib/types/docker/stack.type.ts (3)

5-5: Good addition of autoUpdate flag to StackMeta.

The optional autoUpdate property is appropriately added to the StackMeta interface, enabling tracking of auto-update status for stacks.


31-31: Proper integration of meta information in Stack interface.

Adding the optional meta property to the Stack interface provides a clean way to include metadata like autoUpdate without cluttering the main interface.


37-37: Well-structured StackUpdate interface extension.

The autoUpdate flag is appropriately added to the StackUpdate interface, allowing for updating this setting independently from other stack properties.

src/routes/settings/tabs/app-settings.svelte (2)

20-20: Good derivation of autoUpdateInterval state.

The reactive derived variable properly handles fallbacks from form values to settings to default, maintaining consistency with other settings in the file.


114-120: Well-implemented UI for autoUpdateInterval setting.

The UI for configuring the auto-update interval is:

  1. Properly conditional on autoUpdate being enabled
  2. Includes appropriate min/max constraints (5-1440 minutes)
  3. Has clear labeling and helpful description text
  4. Follows the established UI pattern of other similar settings
src/routes/stacks/[stackId]/+page.svelte (6)

18-18: LGTM: Import Switch component for auto-update feature

This import adds the required UI component for toggling the auto-update feature.


33-33: LGTM: Add tracking for original autoUpdate value

This properly adds tracking for the original autoUpdate value to detect changes.


35-35: LGTM: Update change detection logic to include autoUpdate

Change detection now properly tracks modifications to the autoUpdate setting.


57-61: LGTM: Include autoUpdate in the stack update payload

The autoUpdate property is now properly included in the JSON payload sent to the API.


75-75: LGTM: Reset original autoUpdate value after save

This correctly updates the reference value after a successful save to reset the change detection.


216-222: LGTM: Add UI controls for auto-update feature

Good implementation of the auto-update toggle with descriptive text explaining the feature to users.

src/routes/stacks/[stackId]/+page.server.ts (4)

21-21: LGTM: Add autoUpdate to editorState

This correctly retrieves the autoUpdate value from stack metadata with a sensible default.


35-35: LGTM: Add default autoUpdate value in error case

Providing a default value for autoUpdate in the error case ensures consistent behavior.


49-49: LGTM: Extract autoUpdate from form data

Good handling of form data to extract the autoUpdate boolean value.


52-52: LGTM: Include autoUpdate in updateStack call

The autoUpdate parameter is correctly passed to the service layer.

src/routes/api/stacks/[stackId]/+server.ts (3)

6-7: LGTM: Simplified parameter extraction

Clean refactoring to directly extract the needed parameters from request and params.


10-11: LGTM: Update API call and response

Properly includes autoUpdate in the service call and simplifies the response.


13-14: LGTM: Consolidated error handling

Error handling is now more concise while still providing useful information.

src/lib/services/docker/scheduler-service.ts (4)

1-6: LGTM: Set up imports and timer tracking

Good approach to track the timer for proper cleanup and reuse of existing services.


10-35: LGTM: Robust scheduler initialization

The scheduler initialization handles settings correctly, clears existing timers properly, and uses appropriate defaults.

The initial run followed by scheduled runs ensures updates are checked immediately after initialization.


40-54: LGTM: Comprehensive update check implementation

Good error handling and detailed logging of results for both container and stack updates.


59-65: LGTM: Clean scheduler shutdown

Properly clears the interval and resets the timer reference to prevent memory leaks.

src/lib/services/docker/stack-service.ts (1)

457-463: ⚠️ Potential issue

Preserve existing autoUpdate flag when no new value is supplied

Spreading meta first and then conditionally overriding with updates.autoUpdate works, but the current logic unintentionally removes an existing flag when the caller omits the property (i.e. supplies undefined).
A safer pattern keeps the original value unless the caller explicitly sets it:

-const updatedMeta: StackMeta = {
-	...meta,
-	name: updates.name || meta.name,
-	updatedAt: new Date().toISOString(),
-	// Add auto-update flag if provided
-	...(updates.autoUpdate !== undefined ? { autoUpdate: updates.autoUpdate } : {})
-};
+const updatedMeta: StackMeta = {
+	...meta,
+	name: updates.name ?? meta.name,
+	updatedAt: new Date().toISOString(),
+	// Only override when caller explicitly supplies a boolean
+	autoUpdate: updates.autoUpdate !== undefined ? updates.autoUpdate : meta.autoUpdate
+};

Likely an incorrect or invalid review comment.

Comment on lines +28 to +36
try {
const [containerResults, stackResults] = await Promise.all([checkAndUpdateContainers(), checkAndUpdateStacks()]);

return json({
success: true,
containers: containerResults,
stacks: stackResults
});
} catch (error: any) {
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Manual trigger is skipped when global auto-update is disabled

checkAndUpdateContainers/Stacks return early when settings.autoUpdate === false, so a POST request does nothing unless the global switch is on.
That defeats the purpose of a manual “Check now” button.

Consider adding a force flag to the service functions or calling lower-level helpers directly to ensure the POST route always performs a check irrespective of global settings.

🤖 Prompt for AI Agents (early access)
In src/routes/api/system/auto-update/+server.ts around lines 28 to 36, the manual trigger POST request does nothing if the global auto-update setting is disabled because checkAndUpdateContainers and checkAndUpdateStacks return early. To fix this, modify these functions to accept a force flag that bypasses the global setting check, or call lower-level update helpers directly in the POST handler to ensure the manual trigger always performs the update check regardless of the global auto-update setting.

@kmendell
Copy link
Member Author

@codetabbitai review

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

🔭 Outside diff range comments (2)
src/routes/settings/+page.server.ts (1)

61-63: 🧹 Nitpick (assertive)

Use Number.parseInt for consistency

parseInt is still used here, so the Biome warning will persist.

-const parsedInterval = parseInt(pollingIntervalStr, 10);
+const parsedInterval = Number.parseInt(pollingIntervalStr, 10);
🧰 Tools
🪛 Biome (1.9.4)

[error] 61-61: Use Number.parseInt instead of the equivalent global.

ES2015 moved some globals into the Number namespace for consistency.
Safe fix: Use Number.parseInt instead.

(lint/style/useNumberNamespace)

src/lib/services/docker/stack-service.ts (1)

352-363: 🧹 Nitpick (assertive)

Be aware of larger payloads when returning composeContent

getStack now returns the full compose file string. For large stacks this could be hundreds of KB and will travel over every API call that relies on getStack. Make sure only endpoints/clients that truly need the compose are calling this, or consider an optional includeCompose flag to avoid unnecessary transfer.

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

70-73: Second pullImage call is still redundant

checkContainerImageUpdate already pulled the image to detect updates; pulling again here doubles network I/O and slows the loop.

Refer to the earlier review on the same point – the recommendation still applies.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 71d20e5 and 6d3e88f.

📒 Files selected for processing (5)
  • src/hooks.server.ts (1 hunks)
  • src/lib/services/docker/auto-update-service.ts (1 hunks)
  • src/lib/services/docker/container-service.ts (2 hunks)
  • src/lib/services/docker/stack-service.ts (3 hunks)
  • src/routes/settings/+page.server.ts (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/hooks.server.ts (2)
src/lib/services/docker/stack-service.ts (1)
  • initComposeService (22-32)
src/lib/services/docker/scheduler-service.ts (1)
  • initAutoUpdateScheduler (10-35)
src/lib/services/docker/container-service.ts (3)
src/lib/types/docker/container.type.ts (1)
  • ServiceContainer (4-15)
src/lib/services/docker/core.ts (1)
  • getDockerClient (24-32)
src/lib/types/errors.ts (1)
  • DockerApiError (55-65)
src/lib/services/docker/stack-service.ts (1)
src/lib/types/docker/stack.type.ts (2)
  • Stack (19-32)
  • StackMeta (1-6)
🪛 Biome (1.9.4)
src/lib/services/docker/container-service.ts

[error] 449-449: Forbidden non-null assertion.

(lint/style/noNonNullAssertion)

🔇 Additional comments (3)
src/hooks.server.ts (1)

5-10: Fail-fast initialization looks good – double-check top-level await portability

Great job wrapping both service initializers in a single try/await Promise.all(...) and exiting on failure – this satisfies the “don’t run in a degraded state” concern from the previous review.

One small thing to verify: top-level await requires the code to be emitted as an ES module. SvelteKit’s default Vite setup handles this, but if the project ever targets an older Node runtime (< v14.8) or is compiled to CommonJS, the build will fail. Nothing to change in code right now—just confirm your tooling and deployment environment support TLA.

src/routes/settings/+page.server.ts (1)

29-33: 👍 Switched to Number.parseInt and added clamping

The new parsing plus min/max clamp (5 – 1440) removes the linter warning and guards against out-of-range input. Looks solid.

src/lib/services/docker/stack-service.ts (1)

460-463: Avoid silently omitting autoUpdate when false

The spread (...(updates.autoUpdate !== undefined ? …)) means sending { autoUpdate: false } works, but omitting the key keeps the previous value. That’s correct. Just ensure callers always send false instead of undefined when they intend to disable auto-update to prevent accidental persistence of an old true value.

Comment on lines +911 to +944
export async function listStacks(includeExternal = false): Promise<Stack[]> {
// Get managed stacks
const managedStacks = await loadComposeStacks();

// Add meta information to managed stacks
const enrichedManagedStacks = await Promise.all(
managedStacks.map(async (stack) => {
try {
// Read the meta file to get autoUpdate property
const metaPath = await getStackMetaPath(stack.id);
const metaContent = await fs.readFile(metaPath, 'utf8');
const meta = JSON.parse(metaContent) as StackMeta;

// Return stack with meta included
return {
...stack,
meta
};
} catch (err) {
console.warn(`Failed to read meta for stack ${stack.id}:`, err);
return stack; // Return stack without meta if there was an error
}
})
);

// Get external stacks if requested
let externalStacks: Stack[] = [];
if (includeExternal) {
externalStacks = await discoverExternalStacks();
}

// Combine managed and external stacks
return [...enrichedManagedStacks, ...externalStacks];
}
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)

listStacks duplicates meta-file I/O – consider re-using work done in loadComposeStacks

loadComposeStacks() already reads each meta.json; listStacks reads it again for every stack, doubling disk I/O. An easy win:

- const managedStacks = await loadComposeStacks();
- const enrichedManagedStacks = await Promise.all(
-   managedStacks.map(async (stack) => {
-     ...
-   })
- );
+ const enrichedManagedStacks = await loadComposeStacks(); // meta already present if we enrich there once

or enrich inside loadComposeStacks and return it in one pass.

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

🤖 Prompt for AI Agents (early access)
In src/lib/services/docker/stack-service.ts around lines 911 to 944, the listStacks function redundantly reads the meta.json file for each stack after loadComposeStacks has already done so, causing duplicated disk I/O. To fix this, modify loadComposeStacks to include the meta information when it loads each stack so that listStacks can directly use the enriched stacks without re-reading meta files. This consolidation will eliminate the extra file reads and improve performance.

Comment on lines +492 to +499
const containers = await listContainers(true); // Get all containers including the new one
const newServiceContainer = containers.find((c) => c.id === newContainer.id);

if (!newServiceContainer) {
throw new Error(`Container ${newContainer.id} was created but not found in container list`);
}

return newServiceContainer;
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)

Race: new container may not appear in listContainers immediately

Right after startContainer, Docker sometimes needs a few milliseconds before listContainers shows the new container. Instead of a second round-trip you can get the ID directly:

return {
  id: newContainer.id,
  names: [originalContainer.name],
  name: originalContainer.name,
  image: createOptions.Image,
  imageId: '',      // populate via newContainer.inspect() if needed
  command: createOptions.Cmd?.join(' ') ?? '',
  created: Date.now(),
  state: 'running',
  status: 'running',
  ports: []
};

Or poll inspect() on newContainer instead of calling listContainers.

🤖 Prompt for AI Agents (early access)
In src/lib/services/docker/container-service.ts around lines 492 to 499, the current code calls listContainers immediately after starting a new container, which may not list the container due to a race condition. To fix this, avoid relying on listContainers and instead construct the container object directly using newContainer.id and the original container's properties, or implement a polling mechanism using newContainer.inspect() until the container is available. This ensures you return the container info reliably without waiting for listContainers to update.

Comment on lines +428 to +450
HostConfig: {
PortBindings: originalContainer.networkSettings?.Ports || {},
NetworkMode: originalContainer.networkSettings?.Networks?.bridge ? 'bridge' : Object.keys(originalContainer.networkSettings?.Networks || {})[0] || undefined,
Binds: originalContainer.mounts?.filter((mount) => mount.Type === 'bind' || mount.Type === 'volume').map((mount) => `${mount.Source}:${mount.Destination}${mount.RW ? '' : ':ro'}`)
},
Cmd: originalContainer.config.Cmd,
Entrypoint: originalContainer.config.Entrypoint,
WorkingDir: originalContainer.config.WorkingDir,
User: originalContainer.config.User,
Volumes: originalContainer.config.Volumes,
Tty: originalContainer.config.Tty,
OpenStdin: originalContainer.config.OpenStdin,
StdinOnce: originalContainer.config.StdinOnce
};

// If we need to add custom network configuration for non-default networks
if (originalContainer.networkSettings?.Networks) {
const networks = Object.entries(originalContainer.networkSettings.Networks);
// If container uses a non-default network, set it up
if (networks.length > 0 && networks[0][0] !== 'bridge') {
const [networkName, networkConfig] = networks[0];
createOptions.HostConfig!.NetworkMode = networkName;

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Remove the non-null assertion on HostConfig

Static analysis flags HostConfig! as forbidden. We can avoid it by initialising the object earlier:

- HostConfig: {
-   PortBindings: originalContainer.networkSettings?.Ports || {},
-   NetworkMode: originalContainer.networkSettings?.Networks?.bridge
-     ? 'bridge'
-     : Object.keys(originalContainer.networkSettings?.Networks || {})[0] || undefined,
-   Binds: originalContainer.mounts?.filter((m) => m.Type === 'bind' || m.Type === 'volume')
-     .map((m) => `${m.Source}:${m.Destination}${m.RW ? '' : ':ro'}`)
- },
+ HostConfig: (() => {
+   const hc: Docker.ContainerCreateOptions['HostConfig'] = {
+     PortBindings: originalContainer.networkSettings?.Ports || {},
+     Binds: originalContainer.mounts
+       ?.filter((m) => m.Type === 'bind' || m.Type === 'volume')
+       .map((m) => `${m.Source}:${m.Destination}${m.RW ? '' : ':ro'}`)
+   };
+   hc.NetworkMode =
+     originalContainer.networkSettings?.Networks?.bridge
+       ? 'bridge'
+       : Object.keys(originalContainer.networkSettings?.Networks || {})[0] || undefined;
+   return hc;
+ })(),

This removes the ! and keeps the intent clear.

📝 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
HostConfig: {
PortBindings: originalContainer.networkSettings?.Ports || {},
NetworkMode: originalContainer.networkSettings?.Networks?.bridge ? 'bridge' : Object.keys(originalContainer.networkSettings?.Networks || {})[0] || undefined,
Binds: originalContainer.mounts?.filter((mount) => mount.Type === 'bind' || mount.Type === 'volume').map((mount) => `${mount.Source}:${mount.Destination}${mount.RW ? '' : ':ro'}`)
},
Cmd: originalContainer.config.Cmd,
Entrypoint: originalContainer.config.Entrypoint,
WorkingDir: originalContainer.config.WorkingDir,
User: originalContainer.config.User,
Volumes: originalContainer.config.Volumes,
Tty: originalContainer.config.Tty,
OpenStdin: originalContainer.config.OpenStdin,
StdinOnce: originalContainer.config.StdinOnce
};
// If we need to add custom network configuration for non-default networks
if (originalContainer.networkSettings?.Networks) {
const networks = Object.entries(originalContainer.networkSettings.Networks);
// If container uses a non-default network, set it up
if (networks.length > 0 && networks[0][0] !== 'bridge') {
const [networkName, networkConfig] = networks[0];
createOptions.HostConfig!.NetworkMode = networkName;
HostConfig: (() => {
const hc: Docker.ContainerCreateOptions['HostConfig'] = {
PortBindings: originalContainer.networkSettings?.Ports || {},
Binds: originalContainer.mounts
?.filter((m) => m.Type === 'bind' || m.Type === 'volume')
.map((m) => `${m.Source}:${m.Destination}${m.RW ? '' : ':ro'}`)
};
hc.NetworkMode =
originalContainer.networkSettings?.Networks?.bridge
? 'bridge'
: Object.keys(originalContainer.networkSettings?.Networks || {})[0] || undefined;
return hc;
})(),
Cmd: originalContainer.config.Cmd,
Entrypoint: originalContainer.config.Entrypoint,
WorkingDir: originalContainer.config.WorkingDir,
User: originalContainer.config.User,
Volumes: originalContainer.config.Volumes,
Tty: originalContainer.config.Tty,
OpenStdin: originalContainer.config.OpenStdin,
StdinOnce: originalContainer.config.StdinOnce
};
// If we need to add custom network configuration for non-default networks
if (originalContainer.networkSettings?.Networks) {
const networks = Object.entries(originalContainer.networkSettings.Networks);
// If container uses a non-default network, set it up
if (networks.length > 0 && networks[0][0] !== 'bridge') {
const [networkName, networkConfig] = networks[0];
createOptions.HostConfig!.NetworkMode = networkName;
🧰 Tools
🪛 Biome (1.9.4)

[error] 449-449: Forbidden non-null assertion.

(lint/style/noNonNullAssertion)

🤖 Prompt for AI Agents (early access)
In src/lib/services/docker/container-service.ts around lines 428 to 450, remove the non-null assertion operator (!) on HostConfig by initializing the HostConfig object explicitly before it is used. This avoids the forbidden non-null assertion and keeps the intent clear by ensuring HostConfig is always defined when accessed later in the code.

Comment on lines +68 to 70
mounts: inspectData.Mounts,
labels: inspectData.Config.Labels
};
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)

Return type out of sync

getContainer now returns labels (and mounts) but the return type is still implicit any. If callers start using these new props, explicit typing helps:

-export async function getContainer(containerId: string) {
+export async function getContainer(containerId: string): Promise<{
+  id: string;
+  name: string;
+  created: string;
+  path: string;
+  args: string[];
+  state: Docker.ContainerInspectInfo['State'];
+  image: string;
+  config: Docker.ContainerInspectInfo['Config'];
+  networkSettings: Docker.ContainerInspectInfo['NetworkSettings'];
+  mounts: Docker.ContainerInspectInfo['Mounts'];
+  labels: Record<string, 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
mounts: inspectData.Mounts,
labels: inspectData.Config.Labels
};
export async function getContainer(containerId: string): Promise<{
id: string;
name: string;
created: string;
path: string;
args: string[];
state: Docker.ContainerInspectInfo['State'];
image: string;
config: Docker.ContainerInspectInfo['Config'];
networkSettings: Docker.ContainerInspectInfo['NetworkSettings'];
mounts: Docker.ContainerInspectInfo['Mounts'];
labels: Record<string, string>;
}> {
const inspectData = await docker.getContainer(containerId).inspect();
return {
id: inspectData.Id,
name: inspectData.Name,
created: inspectData.Created,
path: inspectData.Path,
args: inspectData.Args,
state: inspectData.State,
image: inspectData.Image,
config: inspectData.Config,
networkSettings: inspectData.NetworkSettings,
mounts: inspectData.Mounts,
labels: inspectData.Config.Labels
};
}
🤖 Prompt for AI Agents (early access)
In src/lib/services/docker/container-service.ts around lines 68 to 70, the getContainer function now returns mounts and labels properties, but the return type is implicit any. To fix this, explicitly define the return type of getContainer to include mounts and labels with their correct types, ensuring callers have proper type information for these new properties.

Comment on lines +205 to +221
// Extract image references from compose file (simplified parsing)
const composeLines = fullStack.composeContent.split('\n');
const imageLines = composeLines.filter((line) => line.trim().startsWith('image:') || line.includes(' image:'));

if (imageLines.length === 0) {
console.log(`No image references found in stack ${stack.name}.`);
return false;
}

const imageRefs = imageLines
.map((line) => {
const imagePart = line.split('image:')[1].trim();
// Handle potential comments or extra content on the line
return imagePart.replace(/['"]/g, '').split(/[\s#]/)[0];
})
.filter((ref) => ref && (ref.includes(':') || ref.includes('/'))); // Filter out invalid/local refs

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)

Compose parsing is brittle – consider YAML parsing instead of line heuristics

Scanning lines for image: misses:

• multi-line mappings
• services defined with anchors/aliases
• images wrapped in quotes with leading spaces/tabs

Parsing the compose content once with a YAML parser (yaml, js-yaml) avoids these edge cases and yields a structured object whose services[svc].image fields can be traversed reliably.

🤖 Prompt for AI Agents (early access)
In src/lib/services/docker/auto-update-service.ts around lines 205 to 221, the current approach of extracting image references by scanning lines for 'image:' is brittle and misses complex YAML structures like multi-line mappings, anchors, and quoted images with spaces. Replace the line-based heuristic with a proper YAML parser such as 'yaml' or 'js-yaml' to parse the entire compose content into a structured object, then reliably traverse the services to extract the image fields from each service definition.

Comment on lines +55 to +64
// Process eligible containers
for (const container of eligibleContainers) {
const containerId = container.id; // Use consistent ID variable
try {
// Skip if already being updated
if (updatingContainers.has(containerId)) {
console.log(`Auto-update: Skipping ${container.name} (${containerId}), already in progress.`);
continue;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Potential race: add to updatingContainers before performing the expensive checks

A concurrent invocation of checkAndUpdateContainers can evaluate updateAvailable as true for the same container before the current call adds it to the updatingContainers set, leading to duplicate work.

Add the ID to the set immediately after passing the “already updating” guard:

-            const updateAvailable = await checkContainerImageUpdate(container);
-            if (updateAvailable) {
-                updatingContainers.add(containerId);
+            updatingContainers.add(containerId);
+            const updateAvailable = await checkContainerImageUpdate(container);
+            if (updateAvailable) {

and ensure you delete it in the finally block to maintain symmetry.


I’ll inspect the surrounding logic to confirm where updatingContainers is mutated and whether there’s a finally cleanup:


🏁 Script executed:

#!/bin/bash
set -e

# Show the block where containers are processed
rg -n "Process eligible containers" -A20 src/lib/services/docker/auto-update-service.ts

# Locate checkContainerImageUpdate calls
rg -n "checkContainerImageUpdate" -C5 src/lib/services/docker/auto-update-service.ts

# Check how and where we remove IDs from updatingContainers
rg -n "updatingContainers.delete" -C5 src/lib/services/docker/auto-update-service.ts

Length of output: 2766


Prevent concurrent checks by pre-locking the container ID

To avoid two parallel invocations both calling the expensive checkContainerImageUpdate, acquire the lock immediately after the “already updating” guard and use a try…finally to always release it:

src/lib/services/docker/auto-update-service.ts

@@ 58,14 ↓
     // Skip if already being updated
     if (updatingContainers.has(containerId)) {
         console.log(`Auto-update: Skipping ${container.name} (${containerId}), already in progress.`);
         continue;
     }

-    const updateAvailable = await checkContainerImageUpdate(container);
-    if (updateAvailable) {
-        updatingContainers.add(containerId);
+    // Pre-lock to prevent duplicate checks
+    updatingContainers.add(containerId);
+    try {
+        const updateAvailable = await checkContainerImageUpdate(container);
+        if (!updateAvailable) {
+            console.log(`Auto-update: Container ${container.name} (${containerId}) is up-to-date.`);
+            return;
+        }

         console.log(`Auto-update: Update found for container ${container.name} (${containerId}). Recreating...`);
         await pullImage(container.image);
         await recreateContainer(containerId);
-        console.log(`Auto-update: Container ${container.name} recreated successfully`);
-        updatingContainers.delete(containerId);
+        console.log(`Auto-update: Container ${container.name} recreated successfully`);
+    } finally {
+        updatingContainers.delete(containerId);
     }

This guarantees that once a container enters the update flow, no other invocation can re-check it, and the lock is always cleaned up—even on errors.

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

🤖 Prompt for AI Agents (early access)
In src/lib/services/docker/auto-update-service.ts around lines 55 to 64, to prevent concurrent invocations from processing the same container simultaneously, add the container ID to the updatingContainers set immediately after the check that skips already updating containers. Then wrap the update logic in a try...finally block and ensure the container ID is removed from updatingContainers in the finally block to guarantee cleanup even if errors occur. This change prevents duplicate work by locking the container early in the update flow.

@kmendell
Copy link
Member Author

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Apr 29, 2025

✅ Actions performed

Full review triggered.

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

♻️ Duplicate comments (9)
src/hooks.server.ts (1)

5-10: Great implementation of the error handling pattern.

This is an excellent improvement over the previous implementation. The code now properly:

  1. Awaits concurrent initialization of both services
  2. Exits the application if either critical service fails to initialize
  3. Provides clear error logging

This matches the suggestion from the previous review and ensures the application doesn't run in a degraded state.

src/routes/api/system/auto-update/+server.ts (1)

27-46: ⚠️ Potential issue

Manual update checks don't work when auto-update is globally disabled.

The current implementation skips manual checks if auto-update is globally disabled, defeating the purpose of a manual check.

Modify the implementation to allow manual checks regardless of the global setting:

export const POST: RequestHandler = async () => {
	try {
-		const [containerResults, stackResults] = await Promise.all([checkAndUpdateContainers(), checkAndUpdateStacks()]);
+		// Pass force=true to run updates even if global setting is disabled
+		const [containerResults, stackResults] = await Promise.all([
+			checkAndUpdateContainers({ force: true }),
+			checkAndUpdateStacks({ force: true })
+		]);

		return json({
			success: true,
			containers: containerResults,
			stacks: stackResults
		});

You'll also need to modify the service functions to accept and use this parameter:

// In auto-update-service.ts:
- export async function checkAndUpdateContainers(): Promise<{
+ export async function checkAndUpdateContainers(options?: { force?: boolean }): Promise<{
	checked: number;
	updated: number;
	errors: Array<{ id: string; error: string }>;
}> {
	const settings = await getSettings();

	// Skip if global auto-update is disabled
-	if (!settings.autoUpdate) {
+	if (!settings.autoUpdate && !options?.force) {
		return { checked: 0, updated: 0, errors: [] };
	}

And similarly for checkAndUpdateStacks.

src/lib/services/docker/stack-service.ts (1)

912-934: Meta file re-read duplicates work done by loadComposeStacks
This exact I/O duplication was already flagged in a previous review.

Rather than re-reading every meta.json, enhance loadComposeStacks() to include the meta object (it already reads the file) and directly return those enriched stacks here.

Ref-example:

- const managedStacks = await loadComposeStacks();
- const enrichedManagedStacks = await Promise.all(
-   managedStacks.map(async (stack) => { ...read meta again... })
- );
+ const enrichedManagedStacks = await loadComposeStacks(); // meta already present

This removes two file-system calls per stack and simplifies the function.

src/lib/services/docker/container-service.ts (3)

68-71: Public return type now diverges – add an explicit interface

getContainer() gained mounts & labels but still returns any.
An explicit signature prevents silent breakage when callers access these new props.
(The same concern was raised earlier for this function.)


448-451: Avoid the non-null assertion – initialise HostConfig first

createOptions.HostConfig!.NetworkMode = … triggers Biome’s noNonNullAssertion rule and can crash if HostConfig is later unset.

- createOptions.HostConfig!.NetworkMode = networkName;
+createOptions.HostConfig = { ...(createOptions.HostConfig ?? {}), NetworkMode: networkName };
🧰 Tools
🪛 Biome (1.9.4)

[error] 449-449: Forbidden non-null assertion.

(lint/style/noNonNullAssertion)


492-499: 🛠️ Refactor suggestion

Race: new container may not appear in listContainers() immediately

Immediately after startContainer, Docker may still be populating its container list.
Instead of a fresh listContainers call, inspect the newly created container directly:

- const containers = await listContainers(true);
- const newServiceContainer = containers.find((c) => c.id === newContainer.id);
+const inspected = await newContainer.inspect();
+const newServiceContainer: ServiceContainer = {
+  id: inspected.Id,
+  names: [inspected.Name.substring(1)],
+  name: inspected.Name.substring(1),
+  image: inspected.Config.Image,
+  imageId: inspected.Image,
+  command: inspected.Path + ' ' + inspected.Args.join(' '),
+  created: inspected.Created,
+  state: inspected.State.Status,
+  status: inspected.State.Running ? 'running' : 'stopped',
+  ports: inspected.NetworkSettings.Ports
+};

This removes the timing window and an extra Docker API round-trip.

src/lib/services/docker/auto-update-service.ts (3)

56-68: 🛠️ Refactor suggestion

Lock container earlier to avoid race conditions (re-iterating previous feedback)

updatingContainers.add(containerId) is executed after the expensive checkContainerImageUpdate call, leaving a window where two scheduler ticks can run the same network-heavy check and even both proceed to recreating the container.
Add the ID to the set immediately after passing the “already updating” guard, and release it in a finally block to guarantee cleanup:

-			const updateAvailable = await checkContainerImageUpdate(container);
-			if (updateAvailable) {
-				updatingContainers.add(containerId);
+			updatingContainers.add(containerId);           // pre-lock
+			try {
+				const updateAvailable = await checkContainerImageUpdate(container);
+				if (!updateAvailable) {
+					console.log(`Auto-update: Container ${container.name} (${containerId}) is up-to-date.`);
+					continue;
+				}

…and finally { updatingContainers.delete(containerId); }.


70-74: 🧹 Nitpick (assertive)

Redundant second pullImage (previously pointed out)

checkContainerImageUpdate already executed pullImage(imageRef).
Pulling again doubles registry traffic and slows the update loop.

-				console.log(`Auto-update: Pulling latest image ${container.image} for ${container.name}...`);
-				await pullImage(container.image);
+				// Latest image is already pulled inside checkContainerImageUpdate; skip duplicate pull.

206-223: 🛠️ Refactor suggestion

Line-oriented compose parsing remains brittle (see prior review)

Scanning for 'image:' strings still misses multi-line mappings, YAML anchors, quoted values, etc.
Parsing once with a YAML library (yaml, js-yaml) gives you a structured object (services[svc].image) and eliminates these edge cases.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 6e5ad85 and 97d04a2.

📒 Files selected for processing (16)
  • src/hooks.server.ts (1 hunks)
  • src/lib/services/docker/auto-update-service.ts (1 hunks)
  • src/lib/services/docker/container-service.ts (2 hunks)
  • src/lib/services/docker/scheduler-service.ts (1 hunks)
  • src/lib/services/docker/stack-service.ts (3 hunks)
  • src/lib/services/settings-service.ts (1 hunks)
  • src/lib/types/docker/stack.type.ts (2 hunks)
  • src/lib/types/settings.ts (1 hunks)
  • src/routes/api/stacks/[stackId]/+server.ts (1 hunks)
  • src/routes/api/system/auto-update/+server.ts (1 hunks)
  • src/routes/containers/[id]/+page.svelte (5 hunks)
  • src/routes/containers/create-container-dialog.svelte (4 hunks)
  • src/routes/settings/+page.server.ts (2 hunks)
  • src/routes/settings/tabs/app-settings.svelte (2 hunks)
  • src/routes/stacks/[stackId]/+page.server.ts (3 hunks)
  • src/routes/stacks/[stackId]/+page.svelte (5 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
src/lib/services/docker/scheduler-service.ts (2)
src/lib/services/settings-service.ts (1)
  • getSettings (129-182)
src/lib/services/docker/auto-update-service.ts (2)
  • checkAndUpdateContainers (15-96)
  • checkAndUpdateStacks (101-155)
src/lib/services/docker/stack-service.ts (1)
src/lib/types/docker/stack.type.ts (2)
  • Stack (19-32)
  • StackMeta (1-6)
src/routes/api/system/auto-update/+server.ts (2)
src/lib/services/settings-service.ts (1)
  • getSettings (129-182)
src/lib/services/docker/auto-update-service.ts (2)
  • checkAndUpdateContainers (15-96)
  • checkAndUpdateStacks (101-155)
src/routes/stacks/[stackId]/+page.server.ts (1)
src/lib/services/docker/stack-service.ts (1)
  • updateStack (449-499)
🪛 Biome (1.9.4)
src/lib/services/docker/container-service.ts

[error] 449-449: Forbidden non-null assertion.

(lint/style/noNonNullAssertion)

🔇 Additional comments (29)
src/lib/types/settings.ts (1)

4-4: LGTM! The type definition is properly extended.

The addition of autoUpdateInterval as a numeric property is clean and aligned with the auto-update feature being implemented.

src/hooks.server.ts (1)

2-2: LGTM! Good import for the new service.

The import of the auto-update scheduler is properly added.

src/routes/containers/create-container-dialog.svelte (3)

16-16: LGTM! Proper import for the UI component.

The import of the Switch component is correctly added.


90-92: LGTM! Good state initialization.

The auto-update boolean state is properly initialized with a default value of false.


225-229: LGTM! Well-implemented label integration.

The code correctly adds the 'arcane.auto-update' label to the container configuration when the auto-update feature is enabled.

src/routes/settings/+page.server.ts (2)

29-32: Well implemented validation for autoUpdateInterval.

The code correctly parses the autoUpdateInterval from form data, applies proper validation by clamping it between 5 and 1440 minutes. This is consistent with the frontend constraints and provides a good user experience by enforcing reasonable limits.


97-97: LGTM! Proper integration of the new setting.

The validated autoUpdateInterval is correctly included in the updated settings object, making it available for the scheduler service to use.

src/lib/types/docker/stack.type.ts (3)

5-5: Good addition of the autoUpdate flag.

The optional autoUpdate boolean property in the StackMeta interface is appropriate for indicating whether a stack should be automatically updated.


31-31: LGTM! Well-integrated metadata field.

Making the meta field optional ensures backward compatibility with existing stacks that might not have metadata.


37-37: Proper extension of the StackUpdate interface.

Adding the optional autoUpdate field to StackUpdate enables the API to update this setting while maintaining backward compatibility.

src/routes/settings/tabs/app-settings.svelte (2)

20-20: LGTM! Consistent reactive state management.

The derived variable for autoUpdateInterval follows the same pattern as other settings and correctly handles form values and fallbacks.


114-120: Well-designed UI for auto-update interval.

The auto-update interval UI is appropriately shown only when auto-update is enabled. The input field has proper constraints matching the server-side validation (5-1440 minutes) and includes helpful explanatory text.

src/routes/stacks/[stackId]/+page.svelte (6)

18-18: LGTM! Required import for the new UI component.

The Switch component import is needed for the auto-update toggle UI.


33-33: LGTM! Proper state tracking for original value.

Adding the derived variable for originalAutoUpdate follows the existing pattern and enables change detection.


35-35: LGTM! Comprehensive change detection.

The hasChanges derived state now correctly includes checking for changes to the autoUpdate field, which ensures the save button is enabled appropriately.


57-61: LGTM! Complete API payload.

The PATCH request body now properly includes the autoUpdate flag along with other stack properties.


75-75: LGTM! Proper state reset after save.

After a successful save, the originalAutoUpdate value is properly updated to reset the change detection.


216-222: Well-designed UI for stack auto-update toggle.

The auto-update toggle UI is cleanly integrated with the existing form, with a clear label and helpful explanatory text that explains the feature to users. The binding to editorState.autoUpdate ensures the value is properly tracked.

src/routes/api/stacks/[stackId]/+server.ts (2)

10-11: LGTM! Properly handles the new autoUpdate flag.

The update function now correctly includes the autoUpdate parameter, and the response is appropriately simplified.


13-14: Error handling has been simplified and improved.

The consolidated error handling is cleaner than handling specific error types separately, and it properly logs the error while returning a user-friendly message.

src/routes/stacks/[stackId]/+page.server.ts (4)

20-21: LGTM! Good addition of autoUpdate to editor state.

The editor state now properly includes the autoUpdate flag from stack metadata with a sensible false default.


34-35: LGTM! Consistent application of autoUpdate in error state.

The error fallback state correctly includes the autoUpdate property with the same false default.


49-49: LGTM! Proper extraction of autoUpdate from form data.

The code correctly extracts and converts the autoUpdate checkbox value to a boolean.


52-52: LGTM! autoUpdate is now passed to updateStack.

The autoUpdate flag is properly included in the stack update parameters, which aligns with the changes in the stack service.

src/lib/services/docker/scheduler-service.ts (4)

1-6: LGTM! Proper imports and state tracking.

The imports correctly reference the necessary services, and the timer is properly tracked with a module-level variable.


10-35: LGTM! Well-structured scheduler initialization.

The scheduler initialization is well-structured, with proper cleanup of existing timers, settings retrieval, and interval calculation. The code handles the disabled state correctly and logs informative messages.


40-54: LGTM! Comprehensive update check implementation.

The update check function properly calls both container and stack update functions, logs detailed results, and handles errors gracefully.


59-65: LGTM! Clean scheduler shutdown.

The function properly cleans up by clearing the interval and resetting the timer reference.

src/routes/api/system/auto-update/+server.ts (1)

6-25: LGTM! Clean GET endpoint with meaningful responses.

The GET handler properly retrieves settings and returns the auto-update status with clear, user-friendly messages. Error handling is comprehensive.

export const DEFAULT_SETTINGS: SettingsData = {
dockerHost: isDev ? (process.platform === 'win32' ? 'npipe:////./pipe/docker_engine' : 'unix:///var/run/docker.sock') : 'unix:///var/run/docker.sock',
autoUpdate: false,
autoUpdateInterval: 5,
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 adding a comment to clarify the time unit.

The default value of 5 for autoUpdateInterval is reasonable, but it would be helpful to add a comment explicitly mentioning that this is in minutes, especially for future developers.

-	autoUpdateInterval: 5,
+	autoUpdateInterval: 5, // in minutes - determines how often container/stack updates are checked
📝 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
autoUpdateInterval: 5,
autoUpdateInterval: 5, // in minutes - determines how often container/stack updates are checked
🤖 Prompt for AI Agents (early access)
In src/lib/services/settings-service.ts at line 19, add a comment next to the autoUpdateInterval property to clarify that the value 5 represents minutes. This will help future developers understand the time unit without ambiguity. Keep the comment concise and directly related to the time unit.

Comment on lines +618 to 624
<!-- Auto-update -->
<div class="flex items-center space-x-2 py-4 border-t">
<Switch id="auto-update" name="autoUpdate" bind:checked={autoUpdate} />
<Label for="auto-update" class="font-medium">Enable auto-update</Label>
<p class="text-xs text-muted-foreground">When enabled, Arcane will periodically check for newer versions of this container's image and automatically update it.</p>
</div>
</div>
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 adding a warning for 'latest' tag images.

The auto-update UI is well implemented, but it might be helpful to warn users or provide guidance when they're enabling auto-update for containers with the 'latest' tag since these may not behave as expected with auto-updates.

<div class="flex items-center space-x-2 py-4 border-t">
    <Switch id="auto-update" name="autoUpdate" bind:checked={autoUpdate} />
    <Label for="auto-update" class="font-medium">Enable auto-update</Label>
-    <p class="text-xs text-muted-foreground">When enabled, Arcane will periodically check for newer versions of this container's image and automatically update it.</p>
+    <p class="text-xs text-muted-foreground">When enabled, Arcane will periodically check for newer versions of this container's image and automatically update it. 
+    {#if selectedImage.endsWith(':latest')}
+        <span class="text-warning-500">Note: This image uses the 'latest' tag, which may not trigger updates as expected.</span>
+    {/if}
+    </p>
</div>
📝 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
<!-- Auto-update -->
<div class="flex items-center space-x-2 py-4 border-t">
<Switch id="auto-update" name="autoUpdate" bind:checked={autoUpdate} />
<Label for="auto-update" class="font-medium">Enable auto-update</Label>
<p class="text-xs text-muted-foreground">When enabled, Arcane will periodically check for newer versions of this container's image and automatically update it.</p>
</div>
</div>
<!-- Auto-update -->
<div class="flex items-center space-x-2 py-4 border-t">
<Switch id="auto-update" name="autoUpdate" bind:checked={autoUpdate} />
<Label for="auto-update" class="font-medium">Enable auto-update</Label>
<p class="text-xs text-muted-foreground">
When enabled, Arcane will periodically check for newer versions of this container's image and automatically update it.
{#if selectedImage.endsWith(':latest')}
<span class="text-warning-500">
Note: This image uses the 'latest' tag, which may not trigger updates as expected.
</span>
{/if}
</p>
</div>
🤖 Prompt for AI Agents (early access)
In src/routes/containers/create-container-dialog.svelte around lines 618 to 624, add a warning message or guidance below the auto-update switch that appears only when the container image tag is 'latest'. This warning should inform users that enabling auto-update on 'latest' tag images may lead to unexpected behavior. Implement this by conditionally rendering a small, styled text element that clearly communicates this caution to the user.

Comment on lines +6 to +7
const { stackId } = params;
const { name, composeContent, autoUpdate } = await request.json();
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

The destructured request properties lack validation.

The code extracts name, composeContent, and autoUpdate from the request JSON, but there's no validation for these values. The autoUpdate property should specifically be validated as a boolean.

- const { name, composeContent, autoUpdate } = await request.json();
+ const data = await request.json();
+ const name = data.name;
+ const composeContent = data.composeContent;
+ const autoUpdate = typeof data.autoUpdate === 'boolean' ? data.autoUpdate : false;
📝 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 { stackId } = params;
const { name, composeContent, autoUpdate } = await request.json();
const { stackId } = params;
const data = await request.json();
const name = data.name;
const composeContent = data.composeContent;
const autoUpdate = typeof data.autoUpdate === 'boolean' ? data.autoUpdate : false;
🤖 Prompt for AI Agents (early access)
In src/routes/api/stacks/[stackId]/+server.ts around lines 6 to 7, the destructured properties from the request JSON lack validation, especially the autoUpdate property which should be a boolean. Add validation logic after extracting name, composeContent, and autoUpdate to check that autoUpdate is indeed a boolean and that name and composeContent meet expected criteria (e.g., non-empty strings). If validation fails, return an appropriate error response to prevent processing invalid input.

Comment on lines +25 to +35
const intervalMinutes = settings.autoUpdateInterval || 60; // Default to 60 minutes
const intervalMs = intervalMinutes * 60 * 1000;

console.log(`Starting auto-update scheduler with interval of ${intervalMinutes} minutes`);

// Initial run
await runAutoUpdateChecks();

// Schedule regular checks
autoUpdateTimer = setInterval(runAutoUpdateChecks, intervalMs);
}
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 handling very short intervals and concurrent runs.

The scheduler doesn't account for very short intervals or long-running update checks that might overlap. Consider adding minimum interval validation and a mechanism to prevent concurrent runs.

const intervalMinutes = settings.autoUpdateInterval || 60; // Default to 60 minutes
+ // Ensure minimum interval is at least 5 minutes to prevent excessive updates
+ const safeIntervalMinutes = Math.max(5, intervalMinutes);
- const intervalMs = intervalMinutes * 60 * 1000;
+ const intervalMs = safeIntervalMinutes * 60 * 1000;

- console.log(`Starting auto-update scheduler with interval of ${intervalMinutes} minutes`);
+ console.log(`Starting auto-update scheduler with interval of ${safeIntervalMinutes} minutes`);

// Initial run
- await runAutoUpdateChecks();
+ // Use setTimeout for initial run to avoid blocking initialization
+ setTimeout(runAutoUpdateChecks, 1000);

And in the runAutoUpdateChecks function:

+ // Flag to prevent concurrent runs
+ let isRunning = false;
+
async function runAutoUpdateChecks(): Promise<void> {
+  // Skip if already running
+  if (isRunning) {
+    console.log('Auto-update already in progress, skipping this run');
+    return;
+  }
+  
+  isRunning = true;
  console.log('Running scheduled auto-update checks...');

  try {
    // Check containers first
    const containerResults = await checkAndUpdateContainers();
    console.log(`Auto-update check completed for containers: Checked ${containerResults.checked}, Updated ${containerResults.updated}, Errors ${containerResults.errors.length}`);

    // Then check stacks
    const stackResults = await checkAndUpdateStacks();
    console.log(`Auto-update check completed for stacks: Checked ${stackResults.checked}, Updated ${stackResults.updated}, Errors ${stackResults.errors.length}`);
  } catch (error) {
    console.error('Error during auto-update check:', error);
+  } finally {
+    isRunning = false;
  }
}
🤖 Prompt for AI Agents (early access)
In src/lib/services/docker/scheduler-service.ts around lines 25 to 35, the scheduler lacks validation for very short intervals and does not prevent overlapping runs of runAutoUpdateChecks. Add a minimum interval check to ensure intervalMinutes is not below a safe threshold (e.g., 1 minute). Implement a concurrency guard, such as a boolean flag, to skip starting a new runAutoUpdateChecks execution if the previous one is still running, preventing concurrent executions.

Comment on lines +1 to +5
import type { RequestHandler } from './$types';
import { json } from '@sveltejs/kit';
import { getSettings } from '$lib/services/settings-service';
import { checkAndUpdateContainers, checkAndUpdateStacks } from '$lib/services/docker/auto-update-service';

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding API authentication/authorization.

These endpoints can trigger potentially impactful operations across your Docker infrastructure. Consider adding authentication or authorization checks.

For example, you could add a middleware or check session tokens:

+ import { validate_auth } from '$lib/server/auth';

export const GET: RequestHandler = async ({ request }) => {
+	// Validate user is authenticated and authorized
+	const auth = await validate_auth(request);
+	if (!auth.valid) {
+		return json({ error: 'Unauthorized' }, { status: 401 });
+	}

	try {
		const settings = await getSettings();
📝 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
import type { RequestHandler } from './$types';
import { json } from '@sveltejs/kit';
import { getSettings } from '$lib/services/settings-service';
import { checkAndUpdateContainers, checkAndUpdateStacks } from '$lib/services/docker/auto-update-service';
import type { RequestHandler } from './$types';
import { json } from '@sveltejs/kit';
import { getSettings } from '$lib/services/settings-service';
import { checkAndUpdateContainers, checkAndUpdateStacks } from '$lib/services/docker/auto-update-service';
+ import { validate_auth } from '$lib/server/auth';
export const GET: RequestHandler = async ({ request }) => {
+ // Validate user is authenticated and authorized
+ const auth = await validate_auth(request);
+ if (!auth.valid) {
+ return json({ error: 'Unauthorized' }, { status: 401 });
+ }
try {
const settings = await getSettings();
const containersResult = await checkAndUpdateContainers(settings);
const stacksResult = await checkAndUpdateStacks(settings);
return json({
containers: containersResult,
stacks: stacksResult
});
} catch (error) {
return json({ error: String(error) }, { status: 500 });
}
};
🤖 Prompt for AI Agents (early access)
In src/routes/api/system/auto-update/+server.ts around lines 1 to 5, the API endpoints lack authentication or authorization, which is critical since they perform impactful Docker operations. Implement an authentication mechanism by adding middleware or session token checks at the start of the request handler to verify the user's identity and permissions before proceeding with any updates. Ensure unauthorized requests are rejected with appropriate HTTP status codes.

Comment on lines +683 to +695
<h4 class="text-sm font-medium mb-2 flex items-center gap-2">
<Network class="h-4 w-4 text-muted-foreground" /> Network I/O
</h4>
<div class="grid grid-cols-2 gap-4 bg-muted/30 p-3 rounded-md">
<div>
<div class="text-xs text-muted-foreground">Received</div>
<div class="text-sm font-medium mt-1">{formatBytes(stats.networks?.eth0?.rx_bytes || 0)}</div>
</div>
<div>
<div class="text-xs text-muted-foreground">Transmitted</div>
<div class="text-sm font-medium mt-1">{formatBytes(stats.networks?.eth0?.tx_bytes || 0)}</div>
</div>
</div>
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)

Interface name hard-coded to eth0 – aggregate all networks

On Mac / Windows or custom bridges the primary interface might be en0, br0, etc. Summing all interfaces avoids empty metrics:

{:else}
<div class="grid grid-cols-2 gap-4 bg-muted/30 p-3 rounded-md">
  <div>
    <div class="text-xs text-muted-foreground">Received</div>
    <div class="text-sm font-medium mt-1">
      {formatBytes(Object.values(stats.networks ?? {}).reduce((a, n) => a + (n.rx_bytes || 0), 0))}
    </div>
  </div>
  <div>
    <div class="text-xs text-muted-foreground">Transmitted</div>
    <div class="text-sm font-medium mt-1">
      {formatBytes(Object.values(stats.networks ?? {}).reduce((a, n) => a + (n.tx_bytes || 0), 0))}
    </div>
  </div>
</div>
🤖 Prompt for AI Agents (early access)
In src/routes/containers/[id]/+page.svelte around lines 683 to 695, the network interface name is hard-coded to 'eth0', which may cause missing data on systems using different interface names. Replace the direct access to stats.networks.eth0.rx_bytes and tx_bytes with a sum over all network interfaces by using Object.values(stats.networks ?? {}) and reducing to accumulate rx_bytes and tx_bytes respectively. This will aggregate the metrics across all interfaces and avoid empty or incorrect values.

Comment on lines +670 to +678
<!-- Memory Usage -->
<div>
<div class="flex justify-between items-center mb-2">
<span class="text-sm font-medium flex items-center gap-2"><MemoryStick class="h-4 w-4 text-muted-foreground" /> Memory Usage</span>
<span class="text-sm font-semibold">{memoryUsageFormatted} / {memoryLimitFormatted}</span>
</div>
<div class="w-full bg-secondary rounded-full h-3 overflow-hidden">
<div class="bg-primary h-3 rounded-full transition-all duration-300" style="width: {memoryUsagePercent.toFixed(2)}%"></div>
</div>
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)

Clamp displayed memory-usage percentage

memoryUsagePercent can exceed 100 % on kernel-overcommit or cgroup quirks, yet the text is shown un-clamped (only the bar is). Presenting >100 % is confusing.

- <span class="text-sm font-semibold">{memoryUsageFormatted} / {memoryLimitFormatted}</span>
+<span class="text-sm font-semibold">
+  {memoryUsageFormatted} / {memoryLimitFormatted}
+  ({Math.min(memoryUsagePercent, 100).toFixed(2)}%)
+</span>
📝 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
<!-- Memory Usage -->
<div>
<div class="flex justify-between items-center mb-2">
<span class="text-sm font-medium flex items-center gap-2"><MemoryStick class="h-4 w-4 text-muted-foreground" /> Memory Usage</span>
<span class="text-sm font-semibold">{memoryUsageFormatted} / {memoryLimitFormatted}</span>
</div>
<div class="w-full bg-secondary rounded-full h-3 overflow-hidden">
<div class="bg-primary h-3 rounded-full transition-all duration-300" style="width: {memoryUsagePercent.toFixed(2)}%"></div>
</div>
<!-- Memory Usage -->
<div>
<div class="flex justify-between items-center mb-2">
<span class="text-sm font-medium flex items-center gap-2">
<MemoryStick class="h-4 w-4 text-muted-foreground" /> Memory Usage
</span>
<span class="text-sm font-semibold">
{memoryUsageFormatted} / {memoryLimitFormatted}
({Math.min(memoryUsagePercent, 100).toFixed(2)}%)
</span>
</div>
<div class="w-full bg-secondary rounded-full h-3 overflow-hidden">
<div
class="bg-primary h-3 rounded-full transition-all duration-300"
style="width: {memoryUsagePercent.toFixed(2)}%"
></div>
</div>
</div>
🤖 Prompt for AI Agents (early access)
In src/routes/containers/[id]/+page.svelte around lines 670 to 678, the displayed memory usage percentage text shows values above 100%, which can confuse users due to kernel overcommit or cgroup quirks. To fix this, clamp the displayed memoryUsagePercent value to a maximum of 100 before rendering it in the text span, while keeping the bar width styling unchanged.

Comment on lines +126 to +143
// Process eligible stacks
for (const stack of eligibleStacks) {
try {
// Skip if already being updated
if (updatingStacks.has(stack.id)) continue;

const updateAvailable = await checkStackImagesUpdate(stack);
if (updateAvailable) {
updatingStacks.add(stack.id);

console.log(`Auto-update: Redeploying stack ${stack.name} (${stack.id})`);
await fullyRedeployStack(stack.id);

console.log(`Auto-update: Stack ${stack.name} redeployed successfully`);
results.updated++;

updatingStacks.delete(stack.id);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Pre-lock stacks to prevent duplicate redeploys

Similar to containers, updatingStacks.add(stack.id) is executed only after the costly checkStackImagesUpdate.
Move the lock acquisition just after the “already updating” guard and wrap the block in try…finally so concurrent scheduler ticks cannot redeploy the same stack twice.

🤖 Prompt for AI Agents (early access)
In src/lib/services/docker/auto-update-service.ts around lines 126 to 143, the updatingStacks lock is added only after the costly checkStackImagesUpdate call, which can cause duplicate redeploys in concurrent scheduler ticks. Move updatingStacks.add(stack.id) immediately after the if (updatingStacks.has(stack.id)) continue guard, then wrap the redeploy logic in a try...finally block to ensure updatingStacks.delete(stack.id) is always called, preventing concurrent redeploys of the same stack.

Comment on lines +215 to +222
const imageRefs = imageLines
.map((line) => {
const imagePart = line.split('image:')[1].trim();
// Handle potential comments or extra content on the line
return imagePart.replace(/['"]/g, '').split(/[\s#]/)[0];
})
.filter((ref) => ref && (ref.includes(':') || ref.includes('/'))); // Filter out invalid/local refs

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)

Skip digest-only image references

Digest-only refs (sha256:<hash>) cannot be updated by tag; pulling them always fails.
Reuse the regex you added for containers to skip such lines early:

if (/^sha256:[A-Fa-f0-9]{64}$/.test(imageRef)) {
  continue; // digest-only, nothing to update
}

This avoids noisy errors and wasted pulls.

🤖 Prompt for AI Agents (early access)
In src/lib/services/docker/auto-update-service.ts around lines 215 to 222, the code extracts image references but does not skip digest-only references like "sha256:<hash>", which cause pull failures. Add a check using the regex /^sha256:[A-Fa-f0-9]{64}$/ to detect digest-only refs and skip processing them early in the map or filter step to avoid attempting to update these immutable images.

Comment on lines +168 to +185
// Current image digest
const currentImage = await getImage(container.imageId);
if (!currentImage) return false;

// Pull the image to check for updates (but don't use it yet)
await pullImage(imageRef);

// Get the fresh image info
const lastColon = imageRef.lastIndexOf(':');
const imageName = lastColon === -1 ? imageRef : imageRef.slice(0, lastColon);
const tag = lastColon === -1 ? 'latest' : imageRef.slice(lastColon + 1);
const freshImages = await listImages();
const freshImage = freshImages.find((img) => (img.repo === imageName || img.repo.endsWith(`/${imageName}`)) && img.tag === tag);

if (!freshImage) return false;

// Compare image IDs - if different, update is available
return freshImage.id !== container.imageId;
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

Incorrect property casing causes perpetual “update found”

Dockerode’s image objects expose the identifier as Id (capital “I”), but the code compares freshImage.id (lower-case).
Because freshImage.id is undefined, the expression undefined !== container.imageId is always true, so every container is considered outdated in every run.

-		return freshImage.id !== container.imageId;
+		return freshImage.Id !== container.imageId;

Please audit all image-ID accesses (id, Id, imageId) for consistent casing.

📝 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
// Current image digest
const currentImage = await getImage(container.imageId);
if (!currentImage) return false;
// Pull the image to check for updates (but don't use it yet)
await pullImage(imageRef);
// Get the fresh image info
const lastColon = imageRef.lastIndexOf(':');
const imageName = lastColon === -1 ? imageRef : imageRef.slice(0, lastColon);
const tag = lastColon === -1 ? 'latest' : imageRef.slice(lastColon + 1);
const freshImages = await listImages();
const freshImage = freshImages.find((img) => (img.repo === imageName || img.repo.endsWith(`/${imageName}`)) && img.tag === tag);
if (!freshImage) return false;
// Compare image IDs - if different, update is available
return freshImage.id !== container.imageId;
// Current image digest
const currentImage = await getImage(container.imageId);
if (!currentImage) return false;
// Pull the image to check for updates (but don't use it yet)
await pullImage(imageRef);
// Get the fresh image info
const lastColon = imageRef.lastIndexOf(':');
const imageName = lastColon === -1 ? imageRef : imageRef.slice(0, lastColon);
const tag = lastColon === -1 ? 'latest' : imageRef.slice(lastColon + 1);
const freshImages = await listImages();
const freshImage = freshImages.find((img) => (img.repo === imageName || img.repo.endsWith(`/${imageName}`)) && img.tag === tag);
if (!freshImage) return false;
// Compare image IDs - if different, update is available
return freshImage.Id !== container.imageId;
🤖 Prompt for AI Agents (early access)
In src/lib/services/docker/auto-update-service.ts between lines 168 and 185, the code incorrectly accesses the Docker image ID property as freshImage.id instead of freshImage.Id, causing the update check to always return true. To fix this, replace all instances of freshImage.id with freshImage.Id to match Dockerode's property casing. Also, review all image ID references in this block to ensure consistent use of Id with a capital "I" to prevent similar issues.

@kmendell kmendell merged commit 0a5132f into main Apr 29, 2025
1 check passed
@kmendell kmendell deleted the feat/autoupdate branch April 29, 2025 02:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant