feat(tasks): completed tasks are deleted after a delay (default 30 days), "Erledigte leeren"
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m34s

thoughts.completed_at is set by triggers on every way a task is completed (migration 14; tasks
completed earlier count from the update). Users choose the delay (7/30/90/365 days or never,
users.done_retention_days); for tasks in a list the owner's delay counts. An hourly job deletes
expired completed tasks (not in the demo) and tells their viewers. POST /tasks/clear-done clears
completed tasks the user may edit. The done tab shows the setting and the button. v0.35.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-11-01 12:18:54 +01:00
1 parent 66b28e1998
commit f91926c208
16 files changed
+433 -6

No files matched your search

+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mindsift-frontend",
"version": "0.34.0",
"version": "0.35.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mindsift-frontend",
"version": "0.34.0",
"version": "0.35.0",
"license": "Unlicense",
"dependencies": {
"@fontsource-variable/inter": "5.3.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "mindsift-frontend",
"private": true,
"version": "0.34.0",
"version": "0.35.0",
"license": "Unlicense",
"type": "module",
"scripts": {
@@ -690,6 +690,31 @@ export const TasksView: React.FC<TasksViewProps> = ({
onRefreshStats();
};
// Completed tasks: cleared by hand or deleted after the owner's delay (done_cleanup.go)
const [doneRetentionDays, setDoneRetentionDays] = useState<number>(currentUser?.doneRetentionDays ?? 30);
useEffect(() => {
if (currentUser?.doneRetentionDays !== undefined) setDoneRetentionDays(currentUser.doneRetentionDays);
}, [currentUser?.doneRetentionDays]);
const handleChangeDoneRetention = async (days: number) => {
const previous = doneRetentionDays;
setDoneRetentionDays(days);
try {
await api.updateSettings({ doneRetentionDays: days });
} catch {
setDoneRetentionDays(previous);
}
};
const handleClearDone = async () => {
const scope = selectedListId === 'all' ? 'aller Listen, die du bearbeiten darfst,' : `aus „${currentSelectedList?.title || 'dieser Liste'}“`;
if (!window.confirm(`Alle erledigten Aufgaben ${scope} endgültig löschen?`)) return;
triggerHaptic(20);
await api.clearDoneTasks(selectedListId === 'all' ? '' : selectedListId);
await loadData();
onRefreshStats();
};
const handlePurgeAllDiscarded = async () => {
if (!window.confirm('Alle verworfenen Gedanken endgültig löschen?')) return;
triggerHaptic(20);
@@ -2586,8 +2611,44 @@ export const TasksView: React.FC<TasksViewProps> = ({
Papierkorb leeren
</button>
)}
{filter === 'done' && doneTasks.length > 0 && !isSelectedListReadOnly && (
<button onClick={handleClearDone} className="btn-action min-h-[38px] sm:min-h-0 ml-auto">
<Trash2 className="w-3.5 h-3.5" />
Erledigte leeren
</button>
)}
</div>
{/* What happens to completed tasks; the delay of the list owner counts */}
{filter === 'done' && (
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-muted px-1">
{currentSelectedList && currentSelectedList.isOwner === false ? (
<span>
Erledigte Aufgaben dieser Liste räumt die Einstellung von {currentSelectedList.ownerName || 'der Eigentümerin bzw. dem Eigentümer'} auf.
</span>
) : (
<>
<span>Erledigte Aufgaben löschen</span>
<select
value={doneRetentionDays}
onChange={(e) => handleChangeDoneRetention(Number(e.target.value))}
className="h-9 sm:h-6 px-1.5 input-field text-xs bg-panel"
aria-label="Erledigte Aufgaben löschen nach"
>
<option value={7}>nach 7 Tagen</option>
<option value={30}>nach 30 Tagen</option>
<option value={90}>nach 90 Tagen</option>
<option value={365}>nach einem Jahr</option>
<option value={0}>nie</option>
</select>
<span className="text-faint">
{doneRetentionDays > 0 ? '(gezählt ab dem Abhaken; gilt für deine eigenen Listen)' : '(bleiben, bis du sie leerst)'}
</span>
</>
)}
</div>
)}
{loading ? (
<div className="p-6 text-center text-faint text-sm">Lade Aufgaben…</div>
) : filter === 'discarded' ? (
+6
View File
@@ -300,6 +300,7 @@ export const api = {
customWebhookUrl?: string;
notifyBeforeMinutes?: number;
notificationSettings?: NotificationSettings;
doneRetentionDays?: number;
}): Promise<User> {
return request<User>('/api/v1/auth/settings', {
method: 'PATCH',
@@ -472,6 +473,11 @@ export const api = {
await request(`/api/v1/thoughts/${thoughtId}`, { method: 'DELETE' });
},
// "Erledigte leeren": completed tasks of one task list ('' = all the user may edit)
async clearDoneTasks(taskListId: string): Promise<{ deleted: number }> {
return request<{ deleted: number }>('/api/v1/tasks/clear-done', { method: 'POST', body: JSON.stringify({ taskListId }) });
},
async purgeDiscarded(): Promise<{ purged: number }> {
return request<{ purged: number }>('/api/v1/thoughts/discarded/purge', { method: 'DELETE' });
},
+2
View File
@@ -111,6 +111,8 @@ export interface User {
customWebhookUrl?: string;
notifyBeforeMinutes?: number;
notificationSettings?: NotificationSettings;
// Completed tasks are deleted this many days after ticking them off (0 = never)
doneRetentionDays?: number;
organizationId?: string;
createdAt: string;
}