feat: task trash, triage trash and retention settings for every area; admin can forbid keeping things for good
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m39s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-11-01 15:44:34 +01:00
1 parent a927a94a95
commit 5a9c5bac37
36 files changed
+1356 -175

No files matched your search

@@ -19,8 +19,9 @@ import {
Copy,
Archive,
Mic,
Clock,
} from 'lucide-react';
import { AuthStatus, NotificationEvents, NotificationSettings, PushDevice, User, UserTheme } from '../../lib/types';
import { AuthStatus, NotificationEvents, NotificationSettings, PushDevice, Retention, User, UserTheme } from '../../lib/types';
import { ApiKeysPanel } from './ApiKeysPanel';
import { ServerBackupsCard } from './ServerBackupsCard';
import { saveBlob } from '../../lib/download';
@@ -68,7 +69,25 @@ interface UserSettingsViewProps {
onBack: () => void;
}
type UserSettingsSection = 'profile' | 'appearance' | 'alarms' | 'voice' | 'webhooks' | 'data';
// Kontoeinstellungen → Aufbewahrung: the areas and the delays offered (storage.RetentionChoices)
const RETENTION_AREAS: { key: keyof Retention; title: string; text: string; verb: string }[] = [
{ key: 'doneTasks', title: 'Erledigte Aufgaben', text: 'Wandern danach in den Papierkorb der Aufgaben.', verb: 'in den Papierkorb nach' },
{ key: 'taskTrash', title: 'Papierkorb der Aufgaben', text: 'Gelöschte und geleerte Aufgaben.', verb: 'endgültig löschen nach' },
{ key: 'triageTrash', title: 'Papierkorb der Triage', text: 'Beim Sortieren verworfene Gedanken.', verb: 'endgültig löschen nach' },
{ key: 'noteTrash', title: 'Papierkorb der Notizen', text: 'Gelöschte Notizen im Workspace.', verb: 'endgültig löschen nach' },
{ key: 'listDone', title: 'Abgehakte Listeneinträge', text: 'In Einkaufs- und Checklisten.', verb: 'löschen nach' },
];
const RETENTION_OPTIONS = [
{ days: 7, label: '7 Tagen' },
{ days: 14, label: '14 Tagen' },
{ days: 30, label: '30 Tagen' },
{ days: 90, label: '90 Tagen' },
{ days: 365, label: 'einem Jahr' },
{ days: 730, label: 'zwei Jahren' },
{ days: 0, label: 'nie' },
];
type UserSettingsSection = 'profile' | 'appearance' | 'alarms' | 'voice' | 'retention' | 'webhooks' | 'data';
export function buildNotificationSettings(user: User): NotificationSettings {
const hasLegacy = Array.isArray(user.alarmChannels);
@@ -140,6 +159,22 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
useEffect(() => {
void loadVoiceConfig().then(() => setVoiceChoice(voiceChoiceAvailable()));
}, []);
// Retention: one delay per area (storage/retention.go); "nie" only when the server allows it
const [retentionSaving, setRetentionSaving] = useState(false);
const [retentionError, setRetentionError] = useState<string | null>(null);
const handleChangeRetention = async (key: keyof Retention, days: number) => {
if (!currentUser.retention) return;
setRetentionSaving(true);
setRetentionError(null);
try {
onUserUpdated(await api.updateSettings({ retention: { ...currentUser.retention, [key]: days } }));
} catch (err: any) {
setRetentionError(err?.message || 'Speichern fehlgeschlagen');
} finally {
setRetentionSaving(false);
}
};
const handleChangeVoiceInput = async (next: '' | 'browser') => {
setVoiceSaving(true);
try {
@@ -675,6 +710,21 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
</button>
)}
<button
onClick={() => setSection('retention')}
className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${
section === 'retention'
? 'bg-panel-elevated text-main border-l-2 border-l-accent'
: 'text-muted hover:text-main hover:bg-panel-hover'
}`}
>
<Clock className="w-4 h-4 text-accent flex-shrink-0" />
<div>
<p className="font-semibold">Aufbewahrung</p>
<p className="text-[10px] text-faint">Erledigtes & Papierkörbe aufräumen</p>
</div>
</button>
<button
onClick={() => setSection('webhooks')}
className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${
@@ -1964,6 +2014,55 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
</div>
)}
{/* ================================================================= */}
{/* SECTION: AUFBEWAHRUNG */}
{/* ================================================================= */}
{section === 'retention' && (
<div className="space-y-5">
<div className="border-b border-border pb-3">
<h2 className="text-sm font-bold text-main">Aufbewahrung</h2>
<p className="text-muted mt-0.5">
Wie lange Erledigtes und Gelöschtes bleibt, gezählt ab dem Abhaken bzw. Löschen. In geteilten Aufgabenlisten,
Listen und Notizen gilt die Einstellung der Person, der sie gehören.
</p>
</div>
{retentionError && (
<div className="alert-err flex items-center gap-2">
<AlertCircle className="w-4 h-4 text-status-err flex-shrink-0" />
<span>{retentionError}</span>
</div>
)}
{currentUser.retention && (
<div className="divide-y divide-border">
{RETENTION_AREAS.map((area) => (
<div key={area.key} className="py-3 flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<div className="min-w-0">
<p className="font-semibold text-main">{area.title}</p>
<p className="text-muted">{area.text}</p>
</div>
<label className="flex items-center gap-2 flex-shrink-0">
<span className="text-muted">{area.verb}</span>
<select
value={currentUser.retention![area.key]}
disabled={retentionSaving}
onChange={(e) => handleChangeRetention(area.key, Number(e.target.value))}
className="h-11 sm:h-8 px-2 input-field text-base sm:text-xs bg-panel"
aria-label={`${area.title}: ${area.verb}`}
>
{RETENTION_OPTIONS.filter((o) => o.days > 0 || currentUser.retentionNeverAllowed !== false).map((o) => (
<option key={o.days} value={o.days}>
{o.label}
</option>
))}
</select>
</label>
</div>
))}
</div>
)}
</div>
)}
{/* ================================================================= */}
{/* SECTION 5: EXPORT & IMPORT */}
{/* ================================================================= */}