Hassler Platform

Un runtime personnel durable : un VPS unique, un reverse proxy lisible, des services conteneurisés, PostgreSQL comme cœur, un monolithe modulaire par domaines. La complexité n'entre que lorsqu'elle résout une douleur présente, répétée et observable. Trois mantras gouvernent chaque revue : disposabilité future (tout doit pouvoir être remplacé), composition, simplicité et façon canonique (on n'invente pas, on recherche).

Cette page est le carnet de bord ; la source de vérité est le markdown du repo — docs/blueprint.md (vision), docs/adr/ (décisions), docs/runbook/journal.md (chaque intervention serveur, sans exception), docs/runbook/commandes.md (manuel de survie sans LLM).

AppQuoiÉtat
chatmessagerie familiale temps réel (WS), pièces jointes, comptesen prod
filesexplorateur de fichiers en arborescence, stockage CAS sha256en prod
irishexplorateur de musique irlandaise (23 130 tunes TheSession), partitions, MP3, mode travail DSPen prod
contactsannuaire de la plateforme : personnes et entreprises, téléphones/adresses multiples, autocomplete BAN — le socle des documents Typsten prod
miamdécouverte publique bars/restaurants façon swipe (ex-table) — données OpenStreetMap synchronisées par ville, serving PostGIS local (autour de soi), carte enrichie (horaires, adresse, équipements, mini-carte), fiches sponsorisées labellisées dans le decken prod
adminbackoffice : comptes, santé des services, observabilité stockage (accès URL directe, jamais dans le hub)en prod
docscette pageen prod
louison / hellopages statiques historiquesen prod
hub hassler.frbureau de la plateforme : fenêtres redimensionnables + snap, dock, apps embarquées en iframeen prod

02Topologie actuelle

Trois ports publics (22, 80, 443). Les conteneurs n'écoutent qu'en loopback : seul Caddy est joignable depuis Internet.

flowchart LR
  I((Internet)) --> DNS[DNS OVH
hassler.fr] DNS --> FW[UFW
80 / 443] FW --> C[Caddy
HTTPS auto] C -->|hello.hassler.fr| H["/var/www/hello
statique, sur l'hôte"] C -->|louison.hassler.fr| L[conteneur louison
127.0.0.1:3100] C -->|docs.hassler.fr| D[conteneur docs
127.0.0.1:3101] C -->|hassler.fr + hub.hassler.fr| HB[hub
127.0.0.1:3106] C -->|chat.hassler.fr| CA[chat-app
127.0.0.1:3102] C -->|chat.hassler.fr/ws| CW[chat-ws
127.0.0.1:3103] C -->|files.hassler.fr| FA[files-app
127.0.0.1:3104] C -->|irish.hassler.fr| IR[irish-app
127.0.0.1:3105] C -->|contacts.hassler.fr| CO[contacts-app
127.0.0.1:3107] CA -->|INSERT + NOTIFY| PG[(postgres
127.0.0.1:5432)] CW -->|LISTEN| PG FA --> PG FA --> FB[/opt/hassler/data/files
CAS sha256, bind mount/] IR --> PG CO --> PG IR --> S3[(S3 hassler-media
GRA, MP3)] subgraph VPS["VPS-3 · Ubuntu 26.04 · Gravelines"] FW; C; H subgraph Docker["Docker Compose · /opt/hassler"] L; D; CA; CW; FA; IR; CO; PG end FB end

Côté repo : monorepo Turborepo (bun = gestionnaire de paquets, Node 22 = runtime), apps en Hono + Drizzle côté serveur, React 19 + Vite en Feature-Sliced Design côté web. Les primitives UI vivent dans @hassler/ui (thin wrappers React Aria), les tokens dans @hassler/design-system — règles dans DESIGN.md.

flowchart LR
  subgraph Apps["apps/"]
    HW[hello]; LO[louison]; DO[docs]; CH[chat + chat/web]; FI[files + files/web]; IRA[irish + irish/web]; HUB[hub]
  end
  subgraph Packages["packages/"]
    UI["ui
React Aria thin wrappers"] DS["design-system
tokens.css"] TO["tooling
eslint · tsconfig"] end CH --> UI --> DS CH --> DS FI --> UI FI --> DS IRA --> UI IRA --> DS CTA[contacts + contacts/web] CTA --> UI CTA --> DS HUB --> UI HUB --> DS Apps -.-> TO

03Les apps, en vrai

chat.hassler.fr — la messagerie familiale

Messages persistés en PostgreSQL, temps réel par un processus WebSocket dédié (chat-ws) découplé de l'app HTTP : le serveur insère puis NOTIFY, le WS écoute (LISTEN) et pousse l'id seul — les clients relisent en HTTP. Pièces jointes (images inline, pdf/zip génériques, preview plein écran), présence en ligne, indicateur de saisie, backoffice admin (création de comptes, réinitialisation de mots de passe). Auth Better Auth, sessions cookie, pas d'inscription publique.

files.hassler.fr — l'explorateur de fichiers

Arborescence de dossiers/fichiers en base ; les octets vivent dans un CAS sha256 (content-addressed storage) monté en bind mount sur l'hôte — inspectable sans Docker. Upload jusqu'à 520 Mo (borne Caddy), dédoublonnage par contenu. À venir : sélection multiple, drag & drop, appel depuis le chat (trombone).

irish.hassler.fr — la musique irlandaise

Le projet passion. Données publiques TheSession (licence ODbL, attribution en pied de page ; clause spécifique : le contenu des tunes ne passe jamais par un LLM, y compris en debug) :

DonnéeVolumeUsage
tunes23 130recherche floue pg_trgm (nom + 29 319 alias), filtres combinables type × tonalité, tri popularité (tunebooks)
settings (partitions ABC)54 750gravure abcjs, pager entre variantes, incipit au survol des listes, ABC brut copiable, édition de variantes perso
pistes d'albums138 334 (7 894 albums)discographie par tune ; page album dédiée + drag & drop de MP3 sur les pistes
bookmarksmiroir du compte thesessionpage dédiée, resynchronisée à chaque déploiement
MP3 famillebucket S3 privélecture streaming (URL présignée) + mode travail

Le mode travail est l'outil du musicien : ralentir sans changer la hauteur, transposer sans changer le tempo, boucler une phrase (points A/B) — DSP signalsmith-stretch (WASM/ AudioWorklet) chargé paresseusement, piloté par une machine à états XState v5 explicite (première brique applicative de l'ADR-0004) : le cycle de vie Web Audio vit dans des acteurs, React ne fait que rendre des snapshots. Le mode travail remplace le player natif quand il s'ouvre.

La passe du 2026-07-12 — irish prend la grammaire de miam : navigation basse (Explorer / Signets / Outils, badge du nombre de signets), listes en carte arrondie (bord à bord sur mobile), recherche pleine hauteur, rangées entièrement cliquables. Le mode travail gagne une forme d'onde (crêtes calculées du buffer déjà décodé, dessin canvas aux couleurs du thème) : cliquer = se déplacer, la boucle A→B devient une région ombrée aux poignées draggables sous l'onde, la vitesse passe en select et la hauteur (±12 demi-tons) se réinitialise d'un bouton. Les signets se trient « ajoutés récemment » (le rang du flux thesession est désormais persisté), une variante préférée s'étoile et s'ouvre par défaut, des liens YouTube s'attachent aux morceaux, et les albums affichent enfin leurs pistes absentes de thesession, en retrait. Nouvel onglet Outils : métronome (ordonnanceur « Two Clocks » précis au sample, tap-tempo, accessible aussi depuis un morceau) et accordeur (micro sans filtres voix, détection MPM, jauge en cents, la ajustable) — ensemble, le clic se coupe pendant l'accordage pour ne pas fausser l'aiguille.

Ingestion : script idempotent (upsert par ids thesession), relancé explicitement — comme les migrations, jamais implicite. Prochain chantier : sets « souvent joués ensemble » et recherche mélodique par notes.

contacts.hassler.fr — l'annuaire (2026-07-06)

Le socle du futur courrier Typst (ADR-0011). Un contact est une personne physique OU une entreprise (raison sociale, SIRET, forme, APE, TVA — l'en-tête d'un devis n'est pas celui d'une lettre), et n'est pas forcément un utilisateur — l'annuaire est premier, un compte peut y être lié. Téléphones et adresses multiples, avec un « préféré » par type garanti par la base (index partiels UNIQUE — deux clics concurrents font un 409 propre, jamais deux préférés). Les adresses se saisissent avec l'autocomplete de la BAN (api-adresse.data.gouv.fr — publique, gratuite, sans clé), champs éditables à la main. Annuaire familial partagé : toute la famille lit et écrit ; aucune donnée de contact ne passe dans les logs.

miam.hassler.fr — découvrir où manger

App autonome de découverte restaurants/bars/cafés. OpenStreetMap sert de base d'amorçage : sync régionale vers PostGIS local, recherche floue et fiches partageables. Les données produit enrichies (nom canonique, description, menu, ardoise, claims propriétaires, sponsorisé) vivent dans des tables Miam séparées des colonnes OSM. Le backoffice plateforme peut observer Miam, mais Miam ne dépend pas des users Better Auth de chat ou admin.

Depuis le 2026-07-12, le deck Découvrir sert des fiches sponsorisées (deux campagnes de démo aux assets fictifs) : panneau plein cadre inversé, badge « Sponsorisé » toujours visible, annonceur affiché, CTA vers son site en nouvel onglet. Insertion aléatoire plafonnée par graine stable (max 2 par deck, jamais dans les 3 premières cartes, ≥ 6 cartes organiques entre deux) ; une sponsorisée n'entre jamais dans la sélection (la règle vit dans la factory du deck, pas dans l'UI) et l'undo ne la ramène pas. Depuis le 2026-07-13 (PR9, ADR-0030), les campagnes sont entièrement administrables depuis admin.hassler.fr : création (uuid serveur, cible https obligatoire), édition champ par champ, poids, fenêtre de diffusion, pause — sans suppression (une campagne se désactive, elle garde sa trace). Le statut affiché dit la vérité du serving (en diffusion / programmée / expirée / en pause), pas le seul interrupteur. Les images arriveront avec le stockage média S3.

Une revue adversariale se vérifie contre les données réelles. Un correctif « sécurisant » a validé l'id du toggle en uuid — or la colonne est du texte et les campagnes seedées portent des slugs : tout id réel devenait un 400, le toggle était mort. Reproduit au curl, corrigé en borne de forme (longueur max), re-testé slug/inconnu/hors-borne.

hassler.fr — le hub, un bureau dans le navigateur

L'apex sert un bureau : chaque app s'ouvre dans une fenêtre (iframe) depuis un dock. Les fenêtres sont déplaçables et redimensionnables (8 poignées, chacune ancrée sur le côté opposé), se snappent aux quatre côtés et aux quatre coins (demis et quarts) ou en plein écran, et se minimisent vers le dock. La fenêtre active passe au premier plan au clic. Deux responsabilités séparées, selon l'ADR-0004 : l'état persistant (collection de fenêtres, géométrie, empilement) vit dans une factory jotai — les fenêtres sont des atoms-in-atom (splitAtom), chaque fenêtre ne relit que le sien ; le geste transitoire (drag, resize) est une machine à états XState v5 qui ne fait que committer le rect dans les atoms.

Le z-order ne réordonne jamais le tableau. Empiler en déplaçant la fenêtre dans le tableau (donc dans le DOM) fait recharger son iframe — l'app repartait de zéro à chaque clic. L'ordre du tableau est stable ; l'empilement est un champ z par fenêtre, et minimiser masque en display:none sans démonter l'iframe. De même, le snap se calcule sur les bords de la fenêtre face au viewport, pas sur le curseur (ce que font les gestionnaires de fenêtres) — sinon un curseur qui frôle un bord snappe par accident.

Le hub est aussi la vitrine du design system : le sélecteur de thème du dock propose cinq skins (système, clair, sombre, un thème pqp navy+orange et un thème aurora teal, avec glow) qui ne font que repeindre les tokens @hassler/design-system. Le même sous-menu Thème se retrouve dans un menu kebab présent dans chaque app (chat, irish, files) — construit sur le composant Menu + SubmenuTrigger de @hassler/ui (React Aria), aux côtés des nouvelles briques partagées (CopyButton, Link/Router).

04Déploiement & CI

Le repo est la source de vérité. Une commande : ./infra/scripts/deploy.sh. Jamais d'édition à la main sur le serveur. Chaque push passe le quality gate GitHub Actions (lint, types, build, steiger) ; chaque déploiement réussi est enregistré comme Deployment GitHub (onglet Environments : quoi, quand). Les migrations SQL sont un pas explicite du script, toujours relues avant.

sequenceDiagram
  participant M as Mac (repo git)
  participant V as VPS /opt/hassler
  participant D as Docker
  participant C as Caddy
  M->>V: rsync compose.yaml + apps/ + Caddyfile
  V->>D: docker compose build + migrations explicites
  D-->>V: images immuables, conteneurs up
  V->>C: caddy validate + reload
  C-->>M: HTTPS 200 sur chaque sous-domaine
  M->>M: gh api : Deployment enregistré

Cache HTTP (depuis le 2026-07-05) : index.html servi en no-cache (revalidation systématique), assets hashés Vite en immutable un an — plus jamais de « vieux bundle » après un déploiement.

05Réseau, DNS & TLS

Une seule machine exposée, un seul point d'entrée chiffré, et un domaine dont on ne touche jamais la partie courrier.

Le chemin d'une requête

Le navigateur résout irish.hassler.fr via le DNS d'OVH, obtient l'IP du VPS, ouvre une connexion TLS sur le port 443. Caddy termine le HTTPS (certificats Let's Encrypt renouvelés seuls) et, selon le nom annoncé dans la poignée de main TLS (le SNI), relaie vers le bon conteneur en loopback. Aucun conteneur n'est joignable directement : seuls 22, 80 et 443 sont ouverts.

La carte DNS (zone hassler.fr)

NomTypeCibleRôle
hassler.fr (apex)A / AAAAVPS (51.38.177.41 · IPv6)le hub — le bureau de la plateforme
wwwCNAME→ apexredirigé en 301 vers l'apex par Caddy
chat / files / irish / docs…AVPSune app par sous-domaine
@MXmx1/2/3.mail.ovh.netcourrier OVH — jamais touché
autoconfig / autodiscoverCNAMEmailconfig.ovh.netréglages mail auto — intacts

Bascule de l'apex vers le hub (2026-07-05)

Le domaine racine servait un ancien site (source sur GitHub, rien à perdre) ; il pointe désormais sur le hub. Règle d'or d'une bascule sans coupure : le vhost avant le DNS. On a donc (1) ajouté le bloc Caddy hassler.fr et déployé, (2) repointé les enregistrements A/AAAA de l'apex vers le VPS, (3) rechargé Caddy pour émettre le certificat une fois le DNS en place. Les enregistrements MX et mail n'ont pas bougé — la messagerie continue sans interruption.

Piège vérifié : un dig hassler.fr peut renvoyer longtemps l'ANCIENNE IP à cause du cache du résolveur local. Pour trancher, on interroge un serveur faisant autorité (dig +short NS hassler.fr puis dig hassler.fr @<ns>) ou un résolveur public (@8.8.8.8, @1.1.1.1) — eux voient la vérité de la zone, pas le cache de la machine.

06Stockage & sauvegardes

Trois lieux de vie pour les octets, deux buckets S3, un principe : la sauvegarde ne dort jamais au même endroit que la donnée.

DonnéeOù elle vitComment elle survit
Messages, comptes, arbres files, tunes PostgreSQL 18 (volume pgdata) pg_dumpall nightly → restic
Photos du chat volume Docker chatfiles restic nightly
Fichiers du domaine files CAS sha256, bind mount /opt/hassler/data/files restic nightly
MP3 d'irish bucket S3 hassler-media (GRA, privé, versionné) versioning du bucket — réplication GRA→RBX à instruire
Secrets hors-git (.env, credentials S3) fichiers 600 sur le VPS restic nightly + gestionnaire de mots de passe

restic (ADR-0008) : dépôt chiffré de bout en bout sur le bucket hassler-backups à Roubaix — un autre site que le VPS (Gravelines). Timers systemd : sauvegarde à 03:30, vérification d'intégrité le dimanche (+ relecture de 5 % des octets la 1re semaine du mois). Rétention 7 quotidiennes / 4 hebdo / 12 mensuelles. Test de restauration réussi le 2026-07-04 (sha256 identiques, base rechargée dans un PostgreSQL jetable). Kit de survie : le repo git, les accès OVH, le mot de passe restic du gestionnaire — rien d'autre. Toutes les commandes : docs/runbook/commandes.md.

Les tunes d'irish sont des données publiques re-générables : l'ingestion des dumps TheSession est idempotente, la synchro des bookmarks tourne à chaque déploiement.

07Sécurité

08Incidents & leçons

Le journal complet vit dans docs/runbook/journal.md. Les leçons qui ont changé les règles :

Les albums fantômes (2026-07-05) — les albums d'un tune étaient en base, dans l'API, dans le bundle… mais invisibles. Double cause : (1) index.html servi sans Cache-Control → les navigateurs gardaient un vieil index en cache heuristique, donc de vieux bundles ; (2) dans une liste flex scrollable, des rangées portant overflow: hidden voient leur min-height tomber à 0 (spec flexbox) — les longues listes étaient écrasées à 0px. Règles : en-têtes de cache explicites partout ; shrink-0 sur les items de liste flex ; et reproduire dans un vrai navigateur avec les vraies données avant de conclure (diagnostic final fait en pilotant Chrome par MCP : mesures de rects, pas de spéculation).
L'id du dump recordings est un id d'album — première ingestion : 138 334 pistes écrasées en 7 754 lignes (l'id se répète par piste). Clé technique + recording_id, migration relue à la main. Règle : toujours vérifier la cardinalité réelle d'un dump avant de choisir la clé primaire.
nuqs est shallow par défaut — des filtres écrits dans l'URL par nuqs ne réveillaient pas le reader react-router : liste jamais réactive. Règle : une seule source (useQueryStates), les queries react-query clées dessus.
Un mot de passe affiché est un mot de passe grillé — un debug a affiché un credential OpenStack en clair : suppression + recréation immédiates. Règle : filtrer les retours d'API de credentials par noms de champs, jamais de dump brut.
La revue 48h (2026-07-06) — trois audits parallèles sur tout le monorepo (backend adversarial, boundaries/ graphe, code mort) : zéro finding critique, graphe 100 % acyclique, mais 6 majeurs corrigés (ownership d'un DELETE, crash du process ws si PostgreSQL tombe pendant un handshake, index de jointure manquant sur 138 k lignes, secrets à fallback silencieux, deploy otage d'un site tiers, uploads bufferisés en RAM). Première MR relue de bout en bout — le rapport vit dans docs/review/. Règle confirmée : ce qui n'est pas revu adversarialement n'est pas fini.
Docker + workspaces bun — le stage deps de CHAQUE Dockerfile doit copier TOUS les manifests du monorepo, sinon --frozen-lockfile casse à l'apparition d'un nouveau workspace. Vérifié deux fois à nos dépens.
Better Auth partagé ≠ cookie partagé — les apps historiques partagent secret + table session, mais chaque sous-domaine garde son cookie __Host-<app>. Le proxy admin peut relayer une valeur de session vers chat/files/irish sous le nom attendu par l'app cible. Miam est l'exception : admin vérifie l'humain, puis appelle Miam via un secret serveur-à-serveur pour préserver l'autonomie future du domaine.

09Roadmap

Livré récemment : le hub hassler.fr (fenêtres, snap, dock, cinq thèmes), irish v2 (incipit au survol, albums + drag & drop de MP3, ABC copiable, édition de variantes, et depuis le 2026-07-07 une popularité en étoiles honnête — classification head/tail breaks, les quintiles s'écrasaient sur la loi de puissance), l'annuaire de contacts (personnes, entreprises ET administrations — CAF/CPAM/impôts seedés depuis l'annuaire officiel — le socle des courriers Typst), et une grammaire de listing unifiée dans toutes les apps : recherche fine avec loupe-spinner, raccourcis de filtre, compteur. Le backoffice centralisé a son premier étage : la CLI hassler (login, stats fichiers par uploadeur, liste des comptes) qui valide le contrat /admin avant une app d'administration dédiée (accès par URL directe, jamais dans le hub) — laquelle montrera aussi la santé de chaque service (dots colorés). Décidé et acté (ADR-0013) : les facettes publiques — des dossiers public/Desktop1…N de files projetés sur les plans du hub, chaque facette étant un explorateur en grille façon Finder (vignettes, navigation, fil d’ariane), avant l’animation cube. Les facettes sont en ligne : hassler.fr ouvre sur un bureau public plein écran (dock persistant, chevrons, pastilles de plan), et les médias — images, vidéos, sons — s’ouvrent dans un visualiseur commun partagé par le chat, le hub et files. À venir : l’app d’administration (gestion des comptes + observabilité des uploads/médias/fichiers en tables ET graphes, stockage par emplacement, purge des orphelins, pastilles de santé — ADR-0012), chat v2 (salles multiples + messages directs — ADR-0014), la suite miam propriétaires (revue des claims, espace manager, édition de fiche : description, horaires, galerie, menu, ardoise, événements — le formulaire public de revendication enregistre déjà, personne ne le relit encore ; les fiches sponsorisées, elles, sont en prod depuis le 2026-07-12 — ADR-0028/0029), les documents Typst (courriers depuis l’annuaire), le launchpad du dock, les sets irish et la recherche mélodique.

flowchart LR
  A[1-5
VPS · SSH
firewall]:::done --> B[6
Caddy
HTTPS]:::done B --> C[7-8
Docker
1re app]:::done C --> D[9
PostgreSQL]:::done D --> G[16-18
chat
realtime]:::done G --> F[12
files]:::done F --> E[10-11
irish]:::done E --> ALB[albums · DnD
édition ABC]:::done ALB --> HUB[hub hassler.fr
fenêtres · snap · thèmes]:::done HUB --> ADM[backoffice
CLI hassler]:::done ADM --> V2[à venir
app admin · facettes publiques
chat v2 · restos PostGIS] F --> H[19-23
observabilité] H --> I[24-30
IA · agents
documents Typst] classDef done fill:#3f8e5c,stroke:#2e6b45,color:#fff

10Décisions d'architecture

ADR-0001 · OVH VPS-3accepté

6 vCores, 12 Go, 100 Go NVMe à Gravelines. Le stockage est une ressource fondatrice (domaine files) et l'upgrade OVH est un aller simple : autant couvrir toute la roadmap d'emblée pour ~3 €/mois de plus que le VPS-2.

ADR-0002 · Ubuntu Server 26.04 LTSaccepté

Meilleure documentation serveur + guides OVH + support jusqu'en 2031. Debian 13 écartée de peu ; la fraîcheur de la 26.04 est un risque faible pour un socle headless minimal.

ADR-0003 · Monorepo privé GitHubaccepté

Un seul repo pour apps, capacités, infra et docs : le pendant naturel du monolithe modulaire, et le meilleur contexte pour les agents de code. Extraction possible plus tard via git filter-repo.

ADR-0004 · Orchestration agents + XStateproposé

Fable orchestrateur, Opus codeur, modèles routés par complexité. Côté applicatif : XState v5 comme moteur de workflows — la machine à états route et valide, le LLM comprend, jamais l'inverse. Première brique applicative livrée le 2026-07-05 : la machine du lecteur de travail d'irish.

ADR-0005 · Stack TS : Hono · Drizzle · WS découplé · React/FSDaccepté

Bun (install) + Node (runtime), Turborepo. Serveur WebSocket séparé de l'app, bus PostgreSQL LISTEN/NOTIFY (l'événement porte l'id, les clients relisent en HTTP). Front React/Vite en Feature-Sliced Design. Depuis 2026-07 : primitives UI en package (@hassler/ui, React Aria) + tokens (@hassler/design-system) — quarantaine Base UI levée.

ADR-0006 · Auth : Better Auth, sessions cookieaccepté

Le standard TS 2026 (l'équipe Auth.js le recommande). Sessions serveur en cookies durcis (position OWASP), pas d'inscription publique — comptes créés par l'admin. SSO multi-apps différé.

ADR-0008 · Sauvegardes : restic → OVH Object Storage RBXaccepté

Timer systemd sur le VPS (pas de serveur dédié) : pg_dumpall + volumes + secrets hors-git, chiffrés par restic vers le bucket hassler-backups à Roubaix — un autre site que le VPS (Gravelines), versioning S3 activé contre un VPS compromis. Mot de passe restic copié hors-VPS obligatoirement. Test de restauration réel réussi le 2026-07-04.

ADR-0009 · irish : app dédiée, données TheSessionaccepté

App à part entière (pas un module du chat) : Hono + Drizzle + journal de migrations séparé (__drizzle_migrations_irish — jamais de journal partagé entre services d'une même base). Données publiques TheSession sous ODbL : attribution affichée, contenu jamais traité par un LLM (clause explicite du dump).

ADR-0010 · Ingestion idempotente des dumpsaccepté

Les ids thesession sont les clés primaires : la ré-ingestion est un upsert pur, les liens profonds restent stables. Remplacement en bloc pour les tables satellites (alias, recordings, bookmarks). Lancement explicite, comme les migrations. Le script ne logge que des comptes, jamais des contenus.

ADR-0011 · Domaine contacts — annuaire de la plateformeaccepté

Un domaine dédié (app Hono + web FSD, port 3107, migrations propres) plutôt qu'une annexe du backoffice chat : user ≠ contact, personne OU entreprise (bloc juridique pour les devis), canaux et adresses 1-N avec « préféré » garanti par index partiels UNIQUE, autocomplete BAN. Écartés : package partagé, Redis/Mongo du rapport, adresse JSONB. Accepté et livré le 2026-07-06.

ADR-0012 · Backoffice centralisé — /admin par domaineaccepté

Chaque domaine expose SES endpoints /admin derrière un middleware requireAdmin vérifié serveur (pattern Django « admin déclaré par module, surface unique »). Trois consommateurs successifs : CLI hassler (livrée — login par session, l’API-key attendra better-auth 1.7), app admin fait-main (Recharts pour les stats, accès par URL directe uniquement, jamais dans le hub, URL non publiée), serveur MCP en dernier. Médias agrégés en SQL, jamais de scan du storage.

ADR-0013 · Dossiers publics → facettes du hubaccepté

Table de projection public_desktop (une ligne = un dossier publié, jetable par DROP) + seed Desktop1..3 ; publier = déplacer un fichier dans public/DesktopN. Première surface API anonyme de la plateforme : GET-only, anti-traversal ltree, allowlist de colonnes, CORS restreint à hassler.fr, noindex. Sur le hub : un explorateur lecture seule en grille façon Finder (vignettes, nom dessous, fil d’ariane en haut), plans coulissants d’abord, cube ensuite.

ADR-0014 · Chat v2 — salles + messages directsaccepté

Les DM sont des salles ordinaires à 2 membres (unicité de la paire EN BASE — leçon Matrix), le salon actuel devient la « salle famille » automatique et non-quittable. Non-lus par curseur last_read_at, un seul canal NOTIFY avec le roomId dans le payload, liste Discussions en une requête LATERAL. Gouvernance famille : chacun crée des salles et ouvre des DM, pas de rôles par salle. Correctif intégré : les pièces jointes se resserrent par appartenance à la salle.

ADR-0015 · Miam, ex-table — bars/restaurantsaccepté

Découverte publique façon swipe, données OpenStreetMap jamais appelées à la requête : synchronisation par ville (cron + admin), serving 100 % PostGIS local (geography + index GIST + ST_DWithin). Ville à deux modes d’extraction (Narbonne-Plage n’a pas de polygone administratif). Cinq amenities (restaurant, fast-food, café, bar, pub), favoris en localStorage, filtres « ouvert maintenant » + cuisine. ODbL : une colonne = un seul monde, jamais de donnée maison dans une colonne OSM.

ADR-0029 · Miam admin autonome, enrichi et sponsoriséaccepté

Miam reste extractible : tables métier, owners/managers et future auth appartiennent au domaine Miam. admin.hassler.fr est un backoffice plateforme consommateur HTTP : il vérifie l'admin connecté, puis appelle les endpoints Miam avec un secret interne MIAM_ADMIN_INTERNAL_TOKEN. Aucun cookie Better Auth admin n'est relayé vers Miam. Le futur admin Miam couvrira carte régionale, sync, fiches enrichies, claims propriétaires, analytics agrégés et cartes sponsorisées clairement labellisées.

ADR-0030 · Miam : le backoffice écrit via le pontaccepté

Le pont admin→miam passe de la lecture à l'écriture de contenu (campagnes, puis assertions établissements, médias, couverture) sans bouger la frontière : chaque route relayée est énumérée une par une (la liste des relais EST la liste des pouvoirs du backoffice), la validation appartient au domaine (zod strict, verdicts 400/404 remontés tels quels), les ids créés par l'admin sont des uuid serveur, pas de DELETE en v1. Les médias iront sur un bucket S3 dédié en upload direct navigateur ; la couverture s'étendra par département avec dry-run obligatoire.

ADR-0016 · Service outils statelessaccepté

Un seul service pour les transformations sans état : Typst → PDF (courriers/devis/factures/CV depuis l’annuaire) et optimisation d’images (AVIF primaire + WebP fallback via sharp — recherche 2026 : AVIF ~20 % plus léger que WebP, 93-95 % de support). Tous deux CPU-bound et purs (entrée → artefact, aucun état). Service interne (loopback, pas de sous-domaine public), appelé serveur-à-serveur (files à l’upload, documents, CLI, MCP). Bornes : taille max, timeout, concurrence.

11Jargon

Le vocabulaire de la plateforme, en clair — pour relire ce carnet sans rien tenir pour acquis.

TermeCe que c'est
Apex (racine)Le domaine nu hassler.fr, sans sous-domaine devant.
Sous-domaineUn préfixe : chat.hassler.fr, irish.hassler.fr. Une app chacun.
A / AAAAFait pointer un nom vers une IP — A en IPv4, AAAA en IPv6.
CNAMEUn alias : « ce nom est le même qu'un autre ». Interdit de le mélanger avec un A/AAAA sur le même nom.
MXMail eXchange : quels serveurs reçoivent le courrier du domaine. On n'y touche jamais.
TTLTime To Live : combien de temps un résolveur garde une réponse DNS en cache.
Résolveur / autoritéLe résolveur (FAI, 8.8.8.8…) répond depuis son cache ; les serveurs faisant autorité (OVH ici) détiennent la vérité de la zone.
Reverse proxyUn portier (Caddy) qui reçoit toutes les requêtes et les distribue aux bons services derrière.
TLS / HTTPS / ACMETLS chiffre la connexion (le cadenas). ACME est le protocole par lequel Caddy obtient et renouvelle seul les certificats Let's Encrypt.
SNIServer Name Indication : le nom de domaine annoncé au début de la poignée de main TLS, qui permet à Caddy de choisir le bon certificat.
LoopbackL'adresse 127.0.0.1, joignable seulement depuis la machine elle-même — nos conteneurs n'écoutent que là.
CASContent-Addressed Storage : un fichier rangé sous l'empreinte (sha256) de son contenu — même contenu = même emplacement, dédoublonnage gratuit.
MigrationUn script versionné qui fait évoluer le schéma de la base ; joué explicitement au déploiement, jamais en douce.
IdempotentRejouable sans dégât : relancer l'ingestion des tunes deux fois donne le même résultat qu'une fois.
ODbLLa licence des données TheSession : attribution obligatoire, et le contenu des morceaux ne passe jamais par un LLM.
FSDFeature-Sliced Design : l'organisation du front en couches (app → pages → widgets → features → entities → shared).
Machine à états (XState)Un modèle explicite « états + transitions » ; le cœur du lecteur de travail d'irish. La machine décide, le reste obéit.
Service-to-serviceUn appel serveur interne entre deux apps. Pour Miam admin : admin-app vérifie l'admin, puis appelle miam-app avec un secret technique.