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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user