Skip to content

feat: full control over prune options#2372

Open
kmendell wants to merge 1 commit intomainfrom
feat/prune-control
Open

feat: full control over prune options#2372
kmendell wants to merge 1 commit intomainfrom
feat/prune-control

Conversation

@kmendell
Copy link
Copy Markdown
Member

@kmendell kmendell commented Apr 14, 2026

Checklist

  • This PR is not opened from my fork’s main branch

What This PR Implements

Fixes: #1880

Changes Made

Testing Done

  • Development environment started: ./scripts/development/dev.sh start
  • Frontend verified at http://localhost:3000
  • Backend verified at http://localhost:3552
  • Manual testing completed (describe):
  • No linting errors (e.g., just lint all)
  • Backend tests pass: just test backend

AI Tool Used (if applicable)

AI Tool:
Assistance Level:
What AI helped with:
I reviewed and edited all AI-generated output:
I ran all required tests and manually verified changes:

Additional Context

Disclaimer Greptiles Reviews use AI, make sure to check over its work.

To better help train Greptile on our codebase, if the comment is useful and valid Like the comment, if its not helpful or invalid Dislike

To have Greptile Re-Review the changes, mention greptileai.

Greptile Summary

This PR replaces the legacy boolean prune flags and single dockerPruneMode setting with per-resource mode enums (none | stopped | olderThan for containers, none | dangling | all | olderThan for images, etc.), adds time-filter support via an "olderThan" + until duration, and introduces an in-process settings migration that lifts existing boolean flags into the new granular model. The change is fully backward-compatible: both the JSON API (via a custom UnmarshalJSON) and the scheduled prune job handle the legacy format gracefully.

Confidence Score: 5/5

  • Safe to merge — all findings are P2 style suggestions; the core prune logic, migration path, and backward-compatibility handling are sound.
  • No P0/P1 defects were found. The three comments are code-style violations (two $state-in-$effect patterns and a double function call that should be $derived), none of which affect runtime correctness. The Go side — migration logic, typed options structs, legacy JSON decoding, and scheduler integration — is well-tested and logically correct.
  • frontend/src/lib/components/prune/prune-mode-card.svelte and frontend/src/lib/components/dialogs/prune-confirmation-dialog.svelte for the Svelte reactivity style issues.

Fix All in Codex

Prompt To Fix All With AI
This is a comment left during a code review.
Path: frontend/src/lib/components/prune/prune-mode-card.svelte
Line: 67-84

Comment:
**Bidirectional `$state` updates inside `$effect` blocks**

Two effects mutually update each other's state: Effect 1 (line 67) reads `untilValue` and writes `durationAmountInternal`/`durationUnitInternal`; Effect 2 (line 75) reads those locals and writes `untilValue` back. While the guard conditions (`if (parsed.amount !== durationAmountInternal ...)`) prevent infinite loops in practice, this bidirectional sync violates the project rule of not updating `$state` inside `$effect`. The pattern is fragile — a rounding difference in `serializePruneDurationInternal` / `parsePruneDurationInternal` could create an oscillating loop.

A cleaner approach is to keep `durationAmountInternal` and `durationUnitInternal` as the primary reactive state (bound to the inputs), derive `untilValue` from them, and initialize them once from the prop using a non-reactive assignment or by parsing inside `$derived.by`.

**Rule Used:** What: Avoid updating `$state` inside `$effect` blo... ([source](https://app.greptile.com/review/custom-context?memory=8e0bee41-b073-4a49-a01c-2c2c8782b420))

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: frontend/src/lib/components/dialogs/prune-confirmation-dialog.svelte
Line: 94-107

Comment:
**`$state` variables updated inside `$effect`**

The effect on line 94 updates eight `$state` variables (`containerMode`, `containerUntil`, `imageMode`, etc.) when the dialog opens. Per the project rule, `$state` must not be updated inside `$effect`; computed/derived values should use `$derived` instead.

For a one-shot dialog initialization, a common Svelte 5 alternative is to derive a settings snapshot object and pass it down as a regular argument, or reset the state inside the `onConfirm`/`onCancel` handlers. If reset-on-open is strictly required, this is an accepted escape hatch, but the concern should at minimum be documented with a comment explaining why `$derived` cannot be used here.

**Rule Used:** What: Avoid updating `$state` inside `$effect` blo... ([source](https://app.greptile.com/review/custom-context?memory=8e0bee41-b073-4a49-a01c-2c2c8782b420))

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: frontend/src/lib/components/dialogs/prune-confirmation-dialog.svelte
Line: 186-188

Comment:
**`getSelectedCountInternal()` called twice per render**

`getSelectedCountInternal()` is called on both lines 186 and 188, each of which invokes `buildPruneRequestInternal()` — rebuilding the entire request object and calling `Object.keys` twice on every render cycle. This should be a `$derived` to memoize the count:

```suggestion
	let selectedCountInternal = $derived(Object.keys(buildPruneRequestInternal()).length);
```

Then reference `selectedCountInternal` in both the `disabled` attribute and the `customLabel`.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "feat: full control over prune options" | Re-trigger Greptile

Greptile also left 3 inline comments on this PR.

Copy link
Copy Markdown
Member Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@kmendell kmendell marked this pull request as ready for review April 14, 2026 03:14
@kmendell kmendell requested a review from a team April 14, 2026 03:14
@kmendell
Copy link
Copy Markdown
Member Author

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@getarcaneappbot
Copy link
Copy Markdown
Contributor

Container images for this PR have been built successfully!

  • Manager: ghcr.io/getarcaneapp/arcane:pr-2372
  • Agent: ghcr.io/getarcaneapp/arcane-headless:pr-2372

Built from commit 7f4d8ae

Comment on lines +67 to +84
$effect(() => {
const parsed = parsePruneDurationInternal(untilValue);
if (parsed.amount !== durationAmountInternal || parsed.unit !== durationUnitInternal) {
durationAmountInternal = parsed.amount;
durationUnitInternal = parsed.unit;
}
});

$effect(() => {
if (!hasOlderThanInternal() || value !== olderThanMode) {
return;
}

const normalized = serializePruneDurationInternal(durationAmountInternal, durationUnitInternal);
if (untilValue !== normalized) {
untilValue = normalized;
}
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Bidirectional $state updates inside $effect blocks

Two effects mutually update each other's state: Effect 1 (line 67) reads untilValue and writes durationAmountInternal/durationUnitInternal; Effect 2 (line 75) reads those locals and writes untilValue back. While the guard conditions (if (parsed.amount !== durationAmountInternal ...)) prevent infinite loops in practice, this bidirectional sync violates the project rule of not updating $state inside $effect. The pattern is fragile — a rounding difference in serializePruneDurationInternal / parsePruneDurationInternal could create an oscillating loop.

A cleaner approach is to keep durationAmountInternal and durationUnitInternal as the primary reactive state (bound to the inputs), derive untilValue from them, and initialize them once from the prop using a non-reactive assignment or by parsing inside $derived.by.

Rule Used: What: Avoid updating $state inside $effect blo... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/lib/components/prune/prune-mode-card.svelte
Line: 67-84

Comment:
**Bidirectional `$state` updates inside `$effect` blocks**

Two effects mutually update each other's state: Effect 1 (line 67) reads `untilValue` and writes `durationAmountInternal`/`durationUnitInternal`; Effect 2 (line 75) reads those locals and writes `untilValue` back. While the guard conditions (`if (parsed.amount !== durationAmountInternal ...)`) prevent infinite loops in practice, this bidirectional sync violates the project rule of not updating `$state` inside `$effect`. The pattern is fragile — a rounding difference in `serializePruneDurationInternal` / `parsePruneDurationInternal` could create an oscillating loop.

A cleaner approach is to keep `durationAmountInternal` and `durationUnitInternal` as the primary reactive state (bound to the inputs), derive `untilValue` from them, and initialize them once from the prop using a non-reactive assignment or by parsing inside `$derived.by`.

**Rule Used:** What: Avoid updating `$state` inside `$effect` blo... ([source](https://app.greptile.com/review/custom-context?memory=8e0bee41-b073-4a49-a01c-2c2c8782b420))

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Comment on lines +94 to 107
$effect(() => {
if (open && !wasOpenInternal) {
containerMode = defaults?.pruneContainerMode ?? 'stopped';
containerUntil = defaults?.pruneContainerUntil ?? '';
imageMode = defaults?.pruneImageMode ?? 'dangling';
imageUntil = defaults?.pruneImageUntil ?? '';
networkMode = defaults?.pruneNetworkMode ?? 'unused';
networkUntil = defaults?.pruneNetworkUntil ?? '';
volumeMode = defaults?.pruneVolumeMode ?? 'none';
buildCacheMode = defaults?.pruneBuildCacheMode ?? 'none';
buildCacheUntil = defaults?.pruneBuildCacheUntil ?? '';
}
wasOpenInternal = open;
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 $state variables updated inside $effect

The effect on line 94 updates eight $state variables (containerMode, containerUntil, imageMode, etc.) when the dialog opens. Per the project rule, $state must not be updated inside $effect; computed/derived values should use $derived instead.

For a one-shot dialog initialization, a common Svelte 5 alternative is to derive a settings snapshot object and pass it down as a regular argument, or reset the state inside the onConfirm/onCancel handlers. If reset-on-open is strictly required, this is an accepted escape hatch, but the concern should at minimum be documented with a comment explaining why $derived cannot be used here.

Rule Used: What: Avoid updating $state inside $effect blo... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/lib/components/dialogs/prune-confirmation-dialog.svelte
Line: 94-107

Comment:
**`$state` variables updated inside `$effect`**

The effect on line 94 updates eight `$state` variables (`containerMode`, `containerUntil`, `imageMode`, etc.) when the dialog opens. Per the project rule, `$state` must not be updated inside `$effect`; computed/derived values should use `$derived` instead.

For a one-shot dialog initialization, a common Svelte 5 alternative is to derive a settings snapshot object and pass it down as a regular argument, or reset the state inside the `onConfirm`/`onCancel` handlers. If reset-on-open is strictly required, this is an accepted escape hatch, but the concern should at minimum be documented with a comment explaining why `$derived` cannot be used here.

**Rule Used:** What: Avoid updating `$state` inside `$effect` blo... ([source](https://app.greptile.com/review/custom-context?memory=8e0bee41-b073-4a49-a01c-2c2c8782b420))

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex

Comment on lines +186 to +188
disabled={getSelectedCountInternal() === 0 || isPruning}
loading={isPruning}
customLabel={m.prune_button({ count: getSelectedCountInternal() })}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 getSelectedCountInternal() called twice per render

getSelectedCountInternal() is called on both lines 186 and 188, each of which invokes buildPruneRequestInternal() — rebuilding the entire request object and calling Object.keys twice on every render cycle. This should be a $derived to memoize the count:

Suggested change
disabled={getSelectedCountInternal() === 0 || isPruning}
loading={isPruning}
customLabel={m.prune_button({ count: getSelectedCountInternal() })}
let selectedCountInternal = $derived(Object.keys(buildPruneRequestInternal()).length);

Then reference selectedCountInternal in both the disabled attribute and the customLabel.

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/lib/components/dialogs/prune-confirmation-dialog.svelte
Line: 186-188

Comment:
**`getSelectedCountInternal()` called twice per render**

`getSelectedCountInternal()` is called on both lines 186 and 188, each of which invokes `buildPruneRequestInternal()` — rebuilding the entire request object and calling `Object.keys` twice on every render cycle. This should be a `$derived` to memoize the count:

```suggestion
	let selectedCountInternal = $derived(Object.keys(buildPruneRequestInternal()).length);
```

Then reference `selectedCountInternal` in both the `disabled` attribute and the `customLabel`.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

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: Add full control over docker prune options

2 participants