feat(p10): archivage automatique des sessions terminées (rétention configurable)

- DB: migration #10 (sessions.archived_at + index), helpers archive/unarchive/archiveExpired (soft-archive, jamais de DELETE)
- core/retention-settings.ts: session_retention_days (défaut 30, 0=jamais, 1-3650) + session_purge_days (off)
- core/session-archive.ts: scheduler start/stop/sweep (calqué DiscoveryService), câblé dans runDaemon
- pty-manager.list({includeArchived}) + SessionSummary.archived + emitHistoricalUpdate
- routes: GET /sessions?includeArchived, POST/DELETE /sessions/:id/archive, POST /sessions/archive-now
- protocole additif: message WS session_archived (relayé par la gateway, abonnés 'sessions')
- settings: retentionDays/purgeDays exposés et validés dans PATCH /settings
- web: store sessions (showArchived + archive/unarchive), SessionsListView (toggle/badge/actions), SettingsView (slider rétention), i18n EN+FR
- tests: retention-settings + session-archive (vitest) + settings-routes étendu ; acceptance-p10.mjs (sweep, event WS, resume d'une session archivée → 201, rétention=0)
This commit is contained in:
2026-06-27 13:31:28 +02:00
parent be43911dc0
commit c8dd539571
22 changed files with 818 additions and 42 deletions

View File

@@ -175,6 +175,26 @@
</div>
</section>
<!-- Sessions (rétention / archivage auto) -->
<section class="card flex flex-col gap-3">
<h2 class="flex items-center gap-2 text-sm font-semibold text-zinc-100">
<Archive :size="16" /> {{ t('settings.sessions') }}
</h2>
<p class="text-xs text-zinc-500">{{ t('settings.sessionsHint') }}</p>
<label class="flex flex-col gap-1">
<span class="text-xs text-zinc-400">{{ t('settings.retentionDays') }}</span>
<input v-model.number="retentionDraft" type="number" min="0" max="3650" class="input w-32" />
<span class="text-xs text-zinc-500">{{ t('settings.retentionDaysHint') }}</span>
</label>
<div>
<BaseButton variant="primary" :loading="settings.saving" :disabled="!retentionDirty" @click="saveRetention">
{{ t('settings.save') }}
</BaseButton>
</div>
</section>
<!-- Sécurité & conformité -->
<section class="card flex flex-col gap-3">
<h2 class="flex items-center gap-2 text-sm font-semibold text-zinc-100">
@@ -240,7 +260,7 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { AlertCircle, Bell, BellOff, Check, CheckCircle2, Coffee, Copy, Download, FolderOpen, KeyRound, Plus, RefreshCw, ScanSearch, Send, Server, Shield, SlidersHorizontal, Terminal, Trash2, X } from '@lucide/vue';
import { AlertCircle, Archive, Bell, BellOff, Check, CheckCircle2, Coffee, Copy, Download, FolderOpen, KeyRound, Plus, RefreshCw, ScanSearch, Send, Server, Shield, SlidersHorizontal, Terminal, Trash2, X } from '@lucide/vue';
import type {
AuditLogEntry,
AuditLogsResponse,
@@ -387,6 +407,22 @@ async function saveClaude(): Promise<void> {
}
}
// ---- Sessions (rétention / archivage auto) ----
const retentionDraft = ref(0);
const retentionDirty = computed(() => retentionDraft.value !== settings.retentionDays);
function syncRetentionDraft(): void {
retentionDraft.value = settings.retentionDays;
}
async function saveRetention(): Promise<void> {
try {
await settings.save({ retentionDays: retentionDraft.value });
syncRetentionDraft();
toasts.success(t('settings.saved'));
} catch (e) {
toasts.error(e);
}
}
// ---- Sécurité & conformité (audit / RGPD) ----
const audit = ref<AuditLogEntry[]>([]);
const auditing = ref(false);
@@ -438,6 +474,7 @@ onMounted(async () => {
await Promise.all([loadTokens(), settings.loaded ? Promise.resolve() : settings.fetch()]);
syncDiscoveryDraft();
syncClaudeDraft();
syncRetentionDraft();
void push.refresh();
});
</script>