feat(tickets): persistance des filtres tickets entre redémarrages (#29)

Introduit le port UiPreferencesGateway et son adapter uiPreferences,
avec le module ticketFilterPersistence qui sauvegarde/restaure les
filtres (recherche, statut, sprint, agents) via useTickets,
useTicketSearch et useProjectAgents. Couvert par tests unitaires et
un test d'intégration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 07:45:50 +02:00
parent 9ee7290cde
commit 79f06c26d9
13 changed files with 793 additions and 20 deletions

View File

@ -971,6 +971,29 @@ export interface WindowGateway {
): Promise<Unsubscribe>;
}
/**
* Thin UI-preferences port (ticket #29). Persists small, per-surface pieces of
* UI state (like the ticket filters) so they survive a restart / reopen. This is
* **frontend-owned** state: the backend stays the source of tickets, never of UI
* preferences. Values are opaque JSON blobs addressed by an explicit string key;
* the adapter serialises/parses them. Reads are best-effort — an absent or
* corrupt entry yields `null` so callers fall back to their default state, and
* writes never throw (a full/blocked store degrades to "not persisted").
*
* Deliberately synchronous: hydration happens during a component's first render,
* so the persisted state is applied without a flash of default filters. The
* localStorage adapter is synchronous; a future async backend (Tauri store)
* would front this with an in-memory cache to keep the contract.
*/
export interface UiPreferencesGateway {
/** Reads and JSON-parses the blob at `key`; `null` when absent or invalid. */
read(key: string): unknown;
/** JSON-serialises and persists `value` at `key`. Best-effort (never throws). */
write(key: string, value: unknown): void;
/** Removes any persisted blob at `key`. Best-effort (never throws). */
remove(key: string): void;
}
/**
* The full set of gateways the app depends on, injected via the DI provider.
* The composition (real vs mock) is chosen in `app/`.
@ -995,4 +1018,5 @@ export interface Gateways {
conversation: ConversationGateway;
ticket: TicketGateway;
window: WindowGateway;
uiPreferences: UiPreferencesGateway;
}