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:
@@ -142,6 +142,18 @@ const MIGRATIONS: Array<{ id: number; sql: string }> = [
|
||||
);
|
||||
`,
|
||||
},
|
||||
{
|
||||
// P10 — archivage automatique des sessions terminées (rétention configurable, défaut 30 j).
|
||||
// `archived_at` NULL = active. Soft-archive : JAMAIS de DELETE (resume/fork restent intacts).
|
||||
// Sémantique distincte de hidden_sessions (#9) : `hidden` est manuel et indexé par
|
||||
// claude_session_id ; `archived` est automatique par ancienneté, indexé par `id`, et ne
|
||||
// s'applique qu'aux sessions managées en DB. Les deux filtres sont indépendants et cumulables.
|
||||
id: 10,
|
||||
sql: `
|
||||
ALTER TABLE sessions ADD COLUMN archived_at TEXT;
|
||||
CREATE INDEX idx_sessions_archived_at ON sessions(archived_at);
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
export type Db = DatabaseSync;
|
||||
@@ -218,3 +230,48 @@ export function hideSession(db: Db, claudeSessionId: string): void {
|
||||
export function unhideSession(db: Db, claudeSessionId: string): void {
|
||||
db.prepare('DELETE FROM hidden_sessions WHERE claude_session_id = ?').run(claudeSessionId);
|
||||
}
|
||||
|
||||
// ---- Archivage des sessions terminées (par id, soft-archive — P10) ----
|
||||
|
||||
/** Archive une session managée (idempotent : ne touche pas une session déjà archivée). */
|
||||
export function archiveSession(db: Db, id: string, archivedAt: string): void {
|
||||
db.prepare('UPDATE sessions SET archived_at = ? WHERE id = ? AND archived_at IS NULL').run(archivedAt, id);
|
||||
}
|
||||
|
||||
/** Dés-archive une session (idempotent). La session reste reprenable/forkable (inchangé). */
|
||||
export function unarchiveSession(db: Db, id: string): void {
|
||||
db.prepare('UPDATE sessions SET archived_at = NULL WHERE id = ?').run(id);
|
||||
}
|
||||
|
||||
/** true si une session managée avec cet id existe en DB. */
|
||||
export function sessionExists(db: Db, id: string): boolean {
|
||||
return db.prepare('SELECT 1 FROM sessions WHERE id = ?').get(id) != null;
|
||||
}
|
||||
|
||||
/** true si la session existe en DB et est archivée. */
|
||||
export function isSessionArchived(db: Db, id: string): boolean {
|
||||
const row = db.prepare('SELECT archived_at FROM sessions WHERE id = ?').get(id) as { archived_at: string | null } | undefined;
|
||||
return row != null && row.archived_at != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive en masse les sessions terminées plus anciennes que `cutoffIso` (comparaison lexicographique
|
||||
* sur ended_at, format ISO UTC). Retourne les `id` archivés (pour émettre les events + auditer).
|
||||
* SELECT puis UPDATE dans une transaction → la liste retournée correspond exactement aux lignes mutées.
|
||||
* Les sessions vivantes (ended_at NULL) et déjà archivées sont exclues.
|
||||
*/
|
||||
export function archiveExpiredSessions(db: Db, archivedAt: string, cutoffIso: string): string[] {
|
||||
db.exec('BEGIN');
|
||||
try {
|
||||
const rows = db
|
||||
.prepare('SELECT id FROM sessions WHERE ended_at IS NOT NULL AND ended_at < ? AND archived_at IS NULL')
|
||||
.all(cutoffIso) as Array<{ id: string }>;
|
||||
const stmt = db.prepare('UPDATE sessions SET archived_at = ? WHERE id = ?');
|
||||
for (const r of rows) stmt.run(archivedAt, r.id);
|
||||
db.exec('COMMIT');
|
||||
return rows.map((r) => r.id);
|
||||
} catch (err) {
|
||||
db.exec('ROLLBACK');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user