# 40 — Contrats JSON canoniques étendus + types TypeScript > Axe B + livrable §4.3. Tranche la question §3.5 (extensions de `shared/types.ts`). **Tous les ajouts sont rétro-compatibles** : champs optionnels, unions élargies. Un `UpdateSnapshot`/`ExecutionResult` du jalon 1 reste strictement valide. --- ## 1. Principe de rétro-compatibilité État actuel (`shared/types.ts`) : ```ts export type OsFamily = "debian" | "ubuntu" | "unknown"; export type AptProxyMode = "direct" | "runtime"; export type ActionType = "apt_full_upgrade" | "reboot"; // UpdateSnapshot.apt: { enabled, count, rebootRequired, packages: AptPackage[] } // ExecutionResult: { ... action: ActionType, status, rebootRequiredAfterRun, importantLogLines, rawLogRef, reportRef } ``` Règles d'extension : 1. **Élargir les unions** (`OsFamily`, `AptProxyMode`, `ActionType`) — additif, aucun retrait. 2. **Ajouter des blocs optionnels** (`docker?`, `errors?`, `apt` détaillé optionnel, `reboot?`, `postInstall?`) — un payload sans ces blocs reste valide. 3. **Ne jamais retirer ni renommer** un champ existant. Le jalon 1 émet `apt: { enabled, count, rebootRequired, packages }` ; on **ajoute** des champs optionnels à côté. 4. Versionner via `schemaVersion?: number` (aligné `snapshots.schema_version` / `executions.schema_version` de `tache1.9.md`). Absence ⇒ version 1. --- ## 2. Extensions des unions ```ts // Élargissement additif. Le jalon 1 ("debian"|"ubuntu"|"unknown") reste valide. export type OsFamily = "debian" | "ubuntu" | "proxmox" | "raspbian" | "unknown"; export type MachineKind = | "physical" | "vm" | "proxmox_host" | "lxc" | "raspberry_pi" | "workstation" | "unknown"; // "persistent" ajouté (écriture dans /etc/apt/apt.conf.d/). export type AptProxyMode = "direct" | "runtime" | "persistent"; export type ActionType = // jalon 1 (conservés tels quels) | "apt_full_upgrade" | "reboot" // APT | "apt_update_analyze" | "apt_upgrade" | "apt_dist_upgrade" | "apt_autoremove" | "apt_clean" | "reboot_verified" // Docker | "docker_scan" | "docker_inspect_current" | "docker_pull_check" | "docker_compose_apply" | "docker_prune_images" | "docker_compose_down" // probe + custom | "machine_probe" | "post_install"; export type SnapshotStatus = "ok" | "updates_available" | "warning" | "error"; export type ExecutionStatus = "ok" | "warning" | "error"; // inchangé ``` > `reboot` (jalon 1) et `reboot_verified` coexistent : `reboot_verified` ajoute la vérification boot_id ; le code jalon 1 continue d'émettre `reboot`. --- ## 3. Snapshot canonique étendu (`UpdateSnapshot`) ```ts export interface AptPackage { name: string; currentVersion: string | null; targetVersion: string; origin: string | null; // Ajouts optionnels (rétro-compatibles) : arch?: string; operation?: "upgrade" | "install" | "remove" | "hold"; severityHint?: "normal" | "security"; } export interface AptSnapshotDetail { enabled: boolean; count: number; rebootRequired: boolean; packages: AptPackage[]; // Ajouts optionnels : status?: SnapshotStatus; // ok | updates_available | warning | error upgradeCount?: number; // simulation `upgrade` distUpgradeCount?: number; // simulation `dist-upgrade` installed?: AptPackage[]; // nouveaux paquets (dist-upgrade) removed?: AptPackage[]; // suppressions prévues (=> status warning) held?: string[]; // paquets retenus (=> status warning) rebootPkgs?: string[]; // depuis reboot-required.pkgs } export interface DockerSnapshotService { serviceName: string; image: string; // image ref (ex. jellyfin/jellyfin:latest) currentImageId?: string | null; currentDigest?: string | null; candidateImageId?: string | null; // après pull-check candidateDigest?: string | null; currentVersion?: string | null; // label OCI org.opencontainers.image.version candidateVersion?: string | null; sourceUrl?: string | null; // label OCI source status?: "up_to_date" | "updates_available" | "warning" | "error"; } export interface DockerSnapshotStack { name: string; workingDir: string; composeFiles: string[]; projectName?: string | null; status: "candidate" | "enabled" | "ignored" | "error"; detectedBy?: "root_scan" | "label" | "manual"; services: DockerSnapshotService[]; } export interface DockerSnapshot { enabled: boolean; installed: boolean; count: number; // services avec update dispo declaredRoots?: string[]; stacks: DockerSnapshotStack[]; status?: SnapshotStatus; } export interface SnapshotError { source: "apt" | "docker" | "post_install" | "ssh" | "system"; kind: string; // voir 50-erreurs.md (taxonomie) severity: "info" | "warning" | "error"; message: string; // nettoyé, jamais de secret remediation?: string; importantLines?: string[]; } export interface UpdateSnapshot { machineId: string; hostname: string; os: { family: OsFamily; version: string }; checkedAt: string; // ISO 8601 status: MachineStatus; apt: AptSnapshotDetail; // bloc jalon 1 conservé, champs additifs optionnels // Ajouts optionnels (rétro-compatibles) : schemaVersion?: number; kind?: "apt_update_analyze" | "docker_scan" | "reboot_check" | "combined"; machineKind?: MachineKind; docker?: DockerSnapshot; errors?: SnapshotError[]; rawHints?: { logImportantLines: string[] }; } ``` > Le bloc `apt` reste **requis** (présent au jalon 1) ; seuls ses champs *additifs* sont optionnels. `docker`, `errors`, `machineKind`, `kind`, `schemaVersion` sont **optionnels** → un snapshot jalon 1 (sans eux) reste valide. ### Bloc Docker minimal exigé par la validation (couverture §6) Le bloc snapshot Docker contient au minimum : stacks déclarés (`declaredRoots`), stacks candidats (`stacks[].status="candidate"`), services (`stacks[].services[]`), image ref actuelle (`image`), image ID actuelle (`currentImageId`), digest actuel si dispo (`currentDigest`), labels de version si dispo (`currentVersion`), image ID/digest candidat après pull (`candidateImageId`/`candidateDigest`), statut `up_to_date|updates_available|warning|error`. ✔ --- ## 4. Résultat d'exécution étendu (`ExecutionResult`) ```ts export interface AptChange { name: string; arch?: string; fromVersion: string | null; toVersion: string | null; operation: "upgraded" | "installed" | "removed" | "unchanged"; origin?: string | null; } export interface AptExecutionResult { planned: AptPackage[]; // ce qui était prévu (simulation pré-action) applied: AptChange[]; // diff dpkg réel before/after installed: AptChange[]; removed: AptChange[]; held: string[]; errors?: SnapshotError[]; rebootRequiredAfterRun: boolean; } export interface DockerImageChange { stack: string; serviceName?: string; imageRef?: string; fromImageId?: string | null; toImageId?: string | null; fromDigest?: string | null; toDigest?: string | null; operation: "pulled" | "recreated" | "pruned"; } export interface DockerExecutionResult { pull?: { changes: DockerImageChange[]; errors?: SnapshotError[] }; up?: { recreated: string[]; running: string[]; exited: string[]; errors?: SnapshotError[] }; prune?: { imagesDeleted: string[]; bytesReclaimed: number; errors?: SnapshotError[] }; errors?: SnapshotError[]; } export interface RebootResult { beforeBootId: string | null; afterBootId: string | null; requestedAt: string; sshWentDownAt: string | null; sshCameBackAt: string | null; waitedSeconds: number; status: "ok" | "reboot_command_failed" | "ssh_never_went_down" | "machine_did_not_return" | "boot_id_unchanged" | "timeout"; lastRebootDurationSeconds?: number; nextRecommendedWaitSeconds?: number; errors?: SnapshotError[]; } export interface PostInstallResult { profilesRun: string[]; variablesUsed: Record; // NON sensible uniquement filesModified: string[]; packagesInstalled: string[]; servicesEnabled: string[]; rebootsRequested: boolean; networkChange?: { oldEndpoint: string | null; newEndpoint: string | null; reconnectHost: string | null; }; errors?: SnapshotError[]; } export interface ExecutionResult { executionId: string; machineId: string; startedAt: string; finishedAt: string; mode: "manual" | "scheduled" | "hermes_requested"; // élargi (jalon 1 = "manual") action: ActionType; status: ExecutionStatus; rebootRequiredAfterRun: boolean; importantLogLines: string[]; rawLogRef: string; reportRef: string; // Ajouts optionnels (rétro-compatibles) : schemaVersion?: number; apt?: AptExecutionResult; docker?: DockerExecutionResult; reboot?: RebootResult; postInstall?: PostInstallResult; errors?: SnapshotError[]; } ``` > `mode` était `"manual"` (littéral) au jalon 1. L'élargir en union `"manual" | "scheduled" | "hermes_requested"` reste compatible (le jalon 1 émet toujours `"manual"`). Tous les nouveaux blocs (`apt`, `docker`, `reboot`, `postInstall`, `errors`) sont optionnels → une exécution jalon 1 reste valide. --- ## 5. Extension de `TemplateVars` (rendu Mustache) ```ts export interface TemplateVars { aptProxy?: string | null; // existant // Ajouts (tous optionnels) : osProfile?: OsFamily; machineKind?: MachineKind; confValues?: boolean; inactivityTimeout?: number; // Docker : composeRoots?: string; // liste rendue shell-safe par le backend composeScanDepth?: number; stackDir?: string; aggressive?: boolean; // prune agressif // Custom : operatorUser?: string; packages?: string; // liste shell-safe newHostname?: string; interfaceName?: string; staticAddress?: string; reconnectHost?: string; dockerUser?: string; composeRoot?: string; rebootAfterInstall?: boolean; // ... champs de profil custom (typés au cas par cas en tâche 4) } ``` --- ## 6. Déduplication (empreinte fonctionnelle) - **APT** : `dedupKey = os_family + "|" + package + "|" + from + "|" + to + "|" + origin`. Permet à Hermes de mutualiser une même mise à jour vue sur plusieurs machines (une seule recherche web, un seul résumé). Stocké dans `apt_planned_packages.dedup_key` / `apt_applied_packages.dedup_key`. - **Docker** : `dedupKey = image + "|" + fromDigest + "|" + toDigest` ; fallback `image + "|" + fromImageId + "|" + toImageId` quand le digest manque. Le calcul de `dedupKey` se fait **côté backend TS** (déterministe), pas dans le shell. --- ## 7. Réduction déterministe avant Hermes/MCP Le réducteur actuel (`server/templates/aptReduce.ts`) garde les lignes : `Inst `, `Conf `, `Remv `, `Err `, `E:`, `W:`, `dpkg:`, `reboot-required`/`REBOOT_REQUIRED`. **Extension proposée** (renommage suggéré `reduceLines.ts`, additif, sans casser `reduceAptLines`) ajoutant les préfixes Docker : `Pulling`, `Digest`, `Status`, `Downloaded newer image`, `Recreating`, `Started`, `Error`, `deleted`, `Total reclaimed space`. Ce que Hermes reçoit : **JSON canonique réduit** (`important_json`) + lignes importantes (`importantLogLines`). **Jamais** le log brut complet (archivé dans `raw_artifacts`/`rawLogPath`), jamais de secret. --- ## 8. Mapping vers `tache1.9.md` (tables dérivées) | Bloc JSON | Table dérivée | Colonnes clés | |---|---|---| | `apt.packages` / `installed` / `removed` / `held` (simulation) | `apt_planned_packages` | `mode`, `operation`, `current_version`, `target_version`, `origin`, `dedup_key` | | `apt.applied` (diff dpkg) | `apt_applied_packages` | `from_version`, `to_version`, `operation`, `dedup_key` | | `errors[]` source apt | `apt_errors` | `kind`, `severity`, `message`, `important_lines_json`, `remediation` | | `docker.stacks[]` | `docker_compose_stacks` + `docker_stack_services` | `status`, `detected_by`, `current_image_id`, `candidate_digest`, … | | `docker.pull/up/prune changes` | `docker_image_events` | `from_image_id`, `to_image_id`, `operation`, `bytes_reclaimed` | | lignes importantes/notices | `important_messages` | `source`, `category`, `package_name`, `message` | | payload complet snapshot | `snapshots.payload_json` + `important_json` | `kind`, `schema_version`, `status` | | payload complet exécution | `executions.result_json` + `important_json` | `error_kind`, `error_message`, `exit_code` | Le JSON complet reste la vérité canonique (archivé) ; les tables dérivées servent recherche/filtres/dédup/badges (conforme à la règle structurante `tache1.9.md §2`).