feat(projects): add auto-close automation for inactive work items#296
Conversation
The "Auto-close work items" project setting was a non-functional,
local-state-only toggle. It now persists and drives a real automation,
mirroring the existing auto-archive feature.
Backend: Project.CloseIn (months; 0 = off, column already in the schema);
IssueStore.CloseInactiveBefore moves non-archived, non-draft items that are
not already in a completed/cancelled state and have been untouched past the
cutoff into the project's cancelled ("closed") state, skipping projects with
no cancelled state; AutomationService.RunAutoClose sweeps every opted-in
project; the existing 6h in-process ticker runs it alongside auto-archive.
close_in is threaded through project Update.
Frontend: the settings toggle + a months selector read and persist close_in
via projectService.update.
Closes Devlaner#193
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds configurable project auto-close automation across the API, scheduled backend processing, issue storage, tests, and project settings UI. Inactive non-terminal items can be moved to a cancelled state after a configured number of months. ChangesProject Auto-Close
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related issues
Suggested labels: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. 🔧 golangci-lint (2.12.2)level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
apps/api/internal/service/project.go (1)
213-218: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win
close_invalidation doesn't enforce the documented 1/3/6/12-month enum.Only negative values are rejected; any positive integer (e.g.
2,100) is silently accepted, even though the PR objective and UI limit choices to 1, 3, 6, or 12 months. A client bypassing the UI (or a future bug) can persist an unsupported value, and the automation inRunAutoClosewill still act on it without error.♻️ Proposed fix to enforce the allowed set
if closeIn != nil { - if *closeIn < 0 { + if *closeIn != 0 && *closeIn != 1 && *closeIn != 3 && *closeIn != 6 && *closeIn != 12 { return nil, ErrInvalidCloseIn } p.CloseIn = *closeIn }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/internal/service/project.go` around lines 213 - 218, Update the close_in validation in the relevant project update method to accept only the documented values 1, 3, 6, or 12; reject zero, negative values, and any other integers with ErrInvalidCloseIn before assigning p.CloseIn.apps/web/src/pages/SettingsPage.tsx (1)
285-306: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider extracting a shared helper/component for auto-archive and auto-close.
persistAutoClose(Lines 287-305) is structurally identical topersistAutoArchive(Lines 265-283), and the "Auto-close work items" toggle block (Lines 2671-2719) mirrors the "Auto-archive closed work items" block almost line-for-line. With two now-symmetric automations, extracting a genericpersistAutomationField(field: 'archive_in' | 'close_in', enabled, months, ...)helper and a shared<AutomationToggle>subcomponent (icon, label, description, months selector, toggle) would remove the duplication and prevent the two flows from drifting apart as more automations are added.Also applies to: 417-419, 2671-2719
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/pages/SettingsPage.tsx` around lines 285 - 306, The auto-archive and auto-close settings duplicate persistence logic and toggle UI. Extract a shared helper, such as persistAutomationField, parameterized by archive_in or close_in, and a reusable AutomationToggle component covering the icon, text, month selector, and toggle; update persistAutoArchive, persistAutoClose, and both settings blocks to use these shared abstractions while preserving their distinct labels and descriptions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@apps/api/internal/service/project.go`:
- Around line 213-218: Update the close_in validation in the relevant project
update method to accept only the documented values 1, 3, 6, or 12; reject zero,
negative values, and any other integers with ErrInvalidCloseIn before assigning
p.CloseIn.
In `@apps/web/src/pages/SettingsPage.tsx`:
- Around line 285-306: The auto-archive and auto-close settings duplicate
persistence logic and toggle UI. Extract a shared helper, such as
persistAutomationField, parameterized by archive_in or close_in, and a reusable
AutomationToggle component covering the icon, text, month selector, and toggle;
update persistAutoArchive, persistAutoClose, and both settings blocks to use
these shared abstractions while preserving their distinct labels and
descriptions.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 661b362e-b38c-4bde-94c7-6eac474cb9e4
📒 Files selected for processing (11)
apps/api/cmd/api/main.goapps/api/internal/handler/project.goapps/api/internal/model/project.goapps/api/internal/service/automation.goapps/api/internal/service/automation_test.goapps/api/internal/service/project.goapps/api/internal/store/issue.goapps/api/internal/store/project.goapps/web/src/api/types.tsapps/web/src/pages/SettingsPage.tsxapps/web/src/services/projectService.ts
Feature summary
The "Auto-close work items" project setting was a non-functional toggle (local React state only). It now persists and drives a real automation that closes inactive work items, mirroring the existing auto-archive feature.
Linked issues / discussion
Closes #193
User-facing behavior
In Project settings → Automations, the "Auto-close work items" toggle now persists. When enabled, you pick a duration (1/3/6/12 months). Work items that are still active (not completed or cancelled) and have been untouched for that long are automatically moved into the project's cancelled ("closed") state by a background sweep.
What changed
API (
apps/api/)model/project.go:Project.CloseIn(months; 0 = off). Theclose_incolumn already exists in the initial schema, so no migration is needed.store/issue.go:CloseInactiveBeforemoves non-archived, non-draft items that are not already in a completed/cancelled state (including items with no state) and were last touched before the cutoff into the project's cancelled state (lowest-sequence cancelled state). Projects with no cancelled state are skipped. Bumpsupdated_atso a freshly-closed item gets its own clock.store/project.go:ListWithAutoClose(projects withclose_in > 0).service/automation.go:RunAutoClosesweeps every opted-in project.cmd/api/main.go: the existing 6h in-process ticker now runs auto-close alongside auto-archive.service/project.go+handler/project.go:close_inthreaded through project Update, validated (ErrInvalidCloseIn, 400 on negative).UI (
apps/web/)SettingsPage: the toggle + a months selector read and persistclose_inviaprojectService.update(mirrors the auto-archive control).projectService/types.ts:close_infield.Database
close_inalready exists onprojects).Why this design
Auto-close is the direct sibling of the just-merged auto-archive (#194), so it reuses that shape end to end: a per-project months field, a store sweep, an
AutomationServicemethod, and the same 6h ticker. The semantic difference is that auto-close targets active (non-terminal) stale items and moves them into the cancelled group, whereas auto-archive targets settled items and setsarchived_at.Test plan
go vet ./...and fullgo test ./...green (testcontainers).TestAutoClose_ClosesOnlyInactiveStaleIssues,TestAutoClose_DisabledProjectUntouched,TestAutoClose_NoCancelledStateSkips.npm run typecheck,npm run lint,prettiergreen.close_in = 3), the months selector appeared, changed it to 6 (close_in = 6), reloaded — the switch stayed on with "6 months" selected.Out of scope (follow-ups)
Rollout notes
None (no migration; the column already exists).
AI assistance
Claude Code— and AI-assisted commits include aCo-Authored-By:trailerChecklist
--no-verifybypass on the commit (full suite ran on pre-commit)Summary by CodeRabbit
New Features
Bug Fixes