Diffusion interne · SW-ARCH-001 · Révision B

STREET WIZARD

Dossier d'architecture, de design, de sécurité et d'audit — tous sous-systèmes

DocumentSW-ARCH-001
RévisionB — remplace la révision A du 2026-08-21
Établi le2026-09-15
Dépôtstreetwizardapp, commit f7cbf82
BasePostgreSQL 17.6.1 · schéma wizardops · ca-central-1
ArchitecteSébastien Germain — Montréal
DestinatairesInterne · partenaires · acquéreurs sous NDA
PérimètreApplication, base, design, sécurité, conformité
Avertissement de méthode — à lire avant le reste

Chaque chiffre de ce dossier a été mesuré sur le dépôt et sur la base de production au commit indiqué. Aucun n'est estimé, arrondi de mémoire, ni repris d'un rapport antérieur — y compris les chiffres de la révision A, qui ont tous été re-mesurés et dont plusieurs ont changé. La méthode est donnée à la section 2 et elle est reproductible.

Ce qui n'a pas pu être mesuré est listé en clair à la section 15, avec la raison. Un dossier qui prétend tout couvrir cache toujours quelque chose ; celui-ci nomme ses trous.

Les sections 9 (design), 10 (sécurité), 11 (audit) et 17 (dossier acquéreur) sont nouvelles dans cette révision. Elles existent parce qu'un dossier purement technique ne répond pas aux questions qu'un partenaire ou un acheteur pose en premier.

01 Sommaire de commandement

Street Wizard est une application de navigation GPS 3D temps réel doublée d'un réseau social de scène automobile : carte de conduite au ras du sol, flotte visible en direct, garage progressif, radio et musique communautaires, petites annonces, crews et murs publics. Elle est livrée sur le web (PWA installable) et empaquetée pour les boutiques natives via Capacitor.

Structure — chiffres mesurés au commit f7cbf82

49 538lignes de code+76 % / rév. A
152fichiers ts/tsx+16
66modules moteurs+10
93tables+5
203fonctions serveur+43
188security definer92,6 %
112rpc appelées+33
21tables scellées+6
100 %tables sous rls93 / 93
71migrations+50
7routes+4
15tables en accès direct−1
Répartition du codeLignesUnitésRév. A
Moteurs — src/lib13 75766 modules8 709 / 56
Composants — src/components30 74975 composants17 147 / 77
Routes — src/routes4 9377 routes2 234 / 3
Total applicatif49 538152 fichiers28 090 / 136
Migrations SQL versionnées—71 fichiers21
Médias livrés — public/34 Mo202 images · 5 boucles audionon mesuré

Le décompte exclut routeTree.gen.ts, fichier généré par le routeur. Les lignes sont physiques, commentaires inclus — voir M1, section 2.

Ce qui s'est amélioré depuis la révision A

Le partage de schéma s'est vidé. Le constat majeur F-01 de la révision A — 54 tables d'un autre produit dans le même schéma, derrière la même clé anonyme — n'a plus la même portée. L'autre produit vit désormais dans ses propres schémas (chantier, 41 tables ; public, 42 tables). Les 50 tables étrangères encore présentes dans wizardops totalisent 116 lignes, dont 19 tables entièrement vides, et 41 d'entre elles existent à l'identique dans chantier. Ce sont des résidus, pas un produit vivant. Voir F-01 révisé, section 13.

La couverture RLS est complète. 93 tables sur 93 portent RLS actif. Le motif « table scellée » est passé de 15 à 21 tables, et la surface d'accès direct depuis le client est descendue de 16 à 15 tables — fleet_queue a été scellée par la migration 0064.

La conformité boutique existe. Suppression de compte dans l'app, blocage, signalement de contenu, pages de confidentialité et de conditions : rien de tout cela n'existait à la révision A. Voir section 11.

Posture générale — la faiblesse structurelle a changé de place

Le système est fonctionnel et livré. Sa faiblesse structurelle n'est plus le partage de schéma : c'est le modèle d'identité. Une passphrase est réduite côté client à un SHA-256 sans sel et sans fonction de dérivation, puis comparée telle quelle en base. Le condensat est donc un porteur : qui l'obtient s'authentifie. Aucune limitation de débit ne protège l'ouverture de session. Voir F-02, section 13, et la section 10 en entier.

C'est le seul constat de ce dossier classé majeure qui n'a pas d'atténuation structurelle en place, et c'est la première chose à corriger.

Point fort structurel — inchangé, et étendu

Le motif « table scellée + RPC » : 21 tables portent RLS actif avec zéro politique, ce qui les rend illisibles et inécrivables depuis le client, quelle que soit la clé. Tout accès passe par une fonction SECURITY DEFINER qui revérifie ses propres règles. Ces 21 tables sont exactement celles qui portent un secret, une identité, un contenu privé ou un compteur à valeur — la correspondance n'est pas fortuite. Voir section 7.

02 Méthode de mesure

Sept familles de mesure. Chacune est reproductible par un tiers avec l'accès au dépôt et à la base. Une mesure dont la méthode n'est pas donnée n'apparaît pas dans ce dossier.

RéfFamilleSource de la mesure
M1Volume de codefind src -name '*.ts*' | xargs wc -l, en excluant routeTree.gen.ts. Lignes physiques, commentaires inclus — ce dépôt commente le pourquoi des invariants, souvent sur vingt lignes ; un décompte « lignes utiles » sous-estimerait le volume à maintenir et effacerait la documentation qui a le plus de valeur au transfert.
M2Surface d'appel clientExtraction des littéraux passés à supabase.rpc() et .from(), puis seconde passe sur les cinq helpers locaux qui masquent le nom (reports.ts, wall.ts, orgs.ts, user-places.ts, trip-history.ts). La première passe seule rend 85 RPC ; le total réel est 112. Angle mort classique de sous-comptage — il a été refait cette fois.
M3Surface serveurRequêtes sur pg_class, pg_proc, pg_policies, pg_indexes, pg_trigger, pg_namespace, sur la base de production.
M4ConstantesLecture directe des export const dans le fichier nommé. Aucune valeur citée de mémoire ni reprise de la révision A — COCKPIT_PITCH valait 66, il vaut 70.
M5ComportementSQL contre lignes jetables en production, supprimées après mesure. Aucun exercice navigateur n'a été relancé pour cette révision : les verdicts de certification de la révision A sont reportés tels quels et datés, jamais promus.
M6Avis automatisésLinter de sécurité Supabase (get_advisors, type security), exécuté le 2026-09-15 à 01:55 UTC sur la base de production. Les six familles remontées sont reproduites intégralement, y compris celles qui sont attendues. Voir section 11.
M7Poids des médiasdu -sh public et décompte de fichiers. Poids sur disque, non compressé : ce n'est pas le poids transféré, qui dépend du cache et du service worker et n'a pas été mesuré.
Règle de prudence — une absence n'est pas une preuve

Une assertion qui vérifie une absence (« l'écran n'affiche pas X ») réussit aussi quand l'écran n'a rien affiché du tout. Tous les exercices navigateur de la révision A portaient donc un garde-fou qui échoue bruyamment si la vue n'a pas rendu — il avait démasqué quatre faux succès sur la console fondateur. Cette révision n'ajoute aucun exercice navigateur ; elle ne peut donc ni confirmer ni infirmer ces verdicts, et elle le dit plutôt que de les recopier comme s'ils avaient été revérifiés.

Ce que « mesuré » veut dire, et ce que ça ne veut pas dire

Un chiffre mesuré décrit l'état du système : combien de tables, quelle constante, quelle politique. Il ne décrit pas sa performance. Ce dossier ne contient aucune mesure de latence, d'images par seconde, d'octets par rouleur ni de coût serveur, parce qu'aucune instrumentation ne les a produites. Voir N-6, section 15. Une affirmation de performance sans instrumentation est une opinion, et la première chose qu'un ingénieur en vérification diligente vérifie.

03 Delta depuis la révision A

Cette section existe parce qu'un dossier révisé qui ne dit pas ce qui a bougé oblige le lecteur à comparer deux documents ligne à ligne. Trois tableaux : ce qui est arrivé, ce qui est parti, ce qui a changé de valeur.

3.1 · Ce qui est arrivé — sous-systèmes et capacités

CapacitéPiècesCe que ça ouvre
Nouveau Conteneur natifCapacitor 8.5.1 · capacitor.config.ts · dossier android/ · identifiant net.streetwizard.appApp Store et Google Play. La coquille est pré-rendue (SW_TARGET=app → dist/client), pas une webview pointée sur le site : démarrage hors réseau, et la règle 4.2 d'Apple n'est pas dans la ligne de mire.
Nouveau Convoisconvoy.ts 362 · ConvoySheet.tsx 647 · canal wo-convoy-<id> · 11 RPCRouler en groupe vers une destination partagée. Le groupe vit en base (survit aux reconnexions), les positions restent sur le canal flotte global filtré par appartenance.
Nouveau Modérationmoderation.ts 165 · fleet_blocks · fleet_abuse_reports · migration 0067Bloquer, signaler, joindre l'éditeur. Exigence App Store 1.2 pour toute app à contenu généré. Le blocage est privé : personne n'est prévenu d'avoir été bloqué.
Nouveau Suppression de compteaccount.ts 83 · fleet_account_delete · migration 0066 · route /data-deletionExigence App Store 5.1.1(v) et Google Play. Suppression dans l'app, pas par courriel. Efface les lignes serveur puis l'état local.
Nouveau Back office6 panneaux : rides 638 · promo 468 · radio 429 · grants 414 · serveur 360 · orgs 333 · ride-admin, radio-admin, orgs-admin, ride-grantsDistribution de véhicules, codes promo, stations de radio, crews, télémétrie serveur — sans passer par SQL. Toutes les RPC résolvent le rang depuis le jeton.
Nouveau Codes promopromo.ts 190 · PromoSheet.tsx 238 · 3 tables · migration 0025Deux temps séparés à dessein : regarder puis échanger. Un choix irréversible ne se fait pas les yeux fermés.
Nouveau XP sociale et VIPxp.ts 52 · rider_xp, xp_is_vip, xp_post_rules · migration 0033Deuxième piste de progression, délibérément séparée du niveau : le niveau se dérive des kilomètres, l'XP de la vie sociale. Les fondre casserait la garantie « niveau 37 = 700 km roulés ».
Nouveau Guidage vocalvoice.ts 149Remis en service avec des règles entre la voix et l'ambiance : elle baisse le reste, ne s'empile pas, a une priorité (manœuvre > alerte), et parle la langue de l'app.
Nouveau Lieux, trajets, Quick Traveluser-places.ts 115 · trip-history.ts 97 · home.ts 113 · QuickTravel.tsx 300 · migrations 0039, 0041Favoris et travail en base ; historique écrit à l'arrivée, jamais au départ. La maison ne quitte jamais l'appareil — c'est elle qui pilote le mode fantôme.
Nouveau Stationnementparking.ts 139 · table fleet_parkedLe char reste visible à l'arrêt sans exposer la maison : sous PARK_SAFE_M, la position publiée est décalée sur l'église la plus proche, à défaut arrondie sur une grille de ~500 m.
Nouveau Vitrine de crew publiqueRoute /org/$slug 1 546 · presence.ts 140 · directory.ts 143Page web publique par crew, lisible sans app. Elle lit la présence sans jamais s'y déclarer — sinon un lecteur de page web apparaîtrait comme un véhicule immobile sur la carte.
Nouveau Médias — ID3, pochettes, fiche chansonid3.ts 387 · AlbumDetailModal.tsx 367 · song_detail · migrations 0069, 0071Le fichier est la source de vérité : titre, artiste et pochette sont lus dans les tags, jamais cherchés chez un service tiers — une pochette trouvée par titre serait celle d'une autre chanson affichée sous le nom d'un rouleur.
Nouveau Cartes partageablesshare-card.ts 198 · meet-card.ts 216Rendu <canvas> 1200×630 côté client, export JPEG, partage natif. Zéro rendu serveur, zéro dépendance.
Nouveau Ambiance météoweather-sfx.ts 84 · sfx.ts 283 · Open-Meteo (sans clé)La boucle d'ambiance suit la météo réelle du point où l'on roule.
Nouveau Pages légales/privacy 134 · /terms 114 · /data-deletion 171Prérequis de soumission des deux boutiques et base de toute discussion de conformité. Voir section 11.
Nouveau Premier lancementGlobeIntro.tsx 652 · SignupGate.tsx 452 · FirstRunTips.tsx 308 · demo-tour.ts 56 · onboarding.ts 72Séquence d'entrée cinématique, porte d'inscription, conseils au premier lancement, tour de démonstration.
Nouveau Éditeur richeTipTap 3.27 · RichEditor.tsx 192 · richtext.ts 71 · migration 0051Posts de mur formatés, fiches techniques de véhicule.

3.2 · Ce qui est parti — retiré du produit

Un dossier qui n'énumère que les ajouts laisse croire que le produit n'a fait que grossir. Quatre sous-systèmes de la révision A n'existent plus. Ils sont listés ici avec la migration qui les a fermés, pour qu'un lecteur de la révision A sache quoi rayer.

RetiréÉtaitFermé parCe qui reste
S-12 Chantier / benneBenne, voyages, équipes, géorepérage, 9 RPC, 3 tables scellées, canal chantier:<org>, 1 238 lignes0048_retire_dump_rust, puis 0057_solo_haul_runsRien dans Street Wizard. Les tables fleet_dump_* sont absentes de la base. Le produit chantier vit dans le schéma chantier, séparé.
S-07 Spots de la routeSpots communautaires, vote de proximité 200 m, statut à 2 h, enterrement à −3, 660 lignesTables road_spots, road_spot_votes, road_spot_status absentes de la baseQuatre fonctions orphelines (road_status_ttl, road_vote_radius_m, _road_dist_m, _road_named) — voir D-3. Le nom « spots » a été réattribué : spots.ts désigne aujourd'hui les décors de showroom débloqués au niveau, un sujet sans rapport.
File musicale du meetFile collaborative avec vote d'ordre, uploadTrack, fetchQueue, voteTrack, table fleet_queue en accès direct0064_close_fleet_queueLe lecteur d'ID3 a survécu à sa feature : le fichier s'appelait queue.ts, il s'appelle id3.ts. La table est scellée. La file live de playlist.ts l'a remplacée.
Notes d'essuie-glaceMot laissé sur un pare-brise — note_leave, notes_inbox, notes_mark_read0065_close_wiper_notesTable fleet_notes et trois fonctions conservées mais plus appelées par aucun client. Voir D-3.

3.3 · Ce qui a changé de valeur

ÉlémentRév. ARév. BLecture
Lignes applicatives28 09049 538+76 % en 25 jours de calendrier
Tables wizardops8893+5 net, après retrait de 6 tables (dump, spots) et ajout de 11
Fonctions serveur160203+43
Part SECURITY DEFINER91,9 %92,6 %188 / 203
Tables scellées1521+6 — le motif s'étend
Politiques RLS112109−3, cohérent avec 6 tables passées en scellé (zéro politique)
Index161180+19, dont le lot 0072_indexes_for_the_hot_polls
Tables étrangères au schéma5450Et surtout : 116 lignes au total, 19 tables vides, 41 dupliquées ailleurs
Accès direct en table1615fleet_queue scellée ; réduction de surface
COCKPIT_PITCH6670Plus d'horizon en conduite ; encadré par MIN 45 / MAX 80
Poids base (schéma SW)33 Mo (tout schéma)3,8 Mo (43 tables SW)Mesures non comparables — la révision A pesait le schéma entier, résidus compris

04 Architecture générale — les quatre couches

COUCHE 1Persistance et autorité
  • PostgreSQL 17.6.1 / Supabase · projet cprop · région ca-central-1 · schéma wizardops
  • 93 tables · 180 index · 23 déclencheurs · 0 vue · 3,8 Mo pour les 43 tables Street Wizard
  • 203 fonctions, dont 188 SECURITY DEFINER (92,6 %)
  • 93 tables sur 93 sous RLS ; 21 scellées : RLS actif, zéro politique → RPC obligatoire
  • L'autorité vit ici. Le client ne fait jamais foi.
▼
COUCHE 2Transport
  • supabase-js 2.108 · PostgREST + Realtime (WebSocket, Phoenix v2)
  • 112 RPC distinctes · 15 tables en accès direct · 1 seau de stockage (fleet-tracks)
  • 6 familles de canaux : wo-fleet-live (présence), wo-reports, wo-wall-<seq>, wo-convoy-<id>, wo-radio-<prov>, dm:<guest>
  • Un sujet = un canal. Le canal de présence porte déjà le multijoueur et a cassé une fois pour avoir servi à deux choses.
  • Mapbox Search Box et Directions · Open-Meteo (météo, sans clé) · Resend (courriel de réinitialisation, en fonction serveur)
▼
COUCHE 3Moteurs — 13 757 lignes, 66 modules
  • Un module = un domaine. Aucun module ne rend de JSX.
  • Un hook d'abonnement rend une fonction d'arrêt, jamais un booléen : un abonnement qu'on ne peut pas couper est une fuite.
  • On ne relit pas à chaque fix GPS — l'ancre se déplace après distance réelle parcourue.
  • Échec réseau = on garde l'état précédent, jamais un vide. Effacer les épingles au premier trou de réseau ferait clignoter la carte dans les rangs, là où elles servent le plus.
  • Un refus est rendu, jamais avalé : un favori qui disparaît en silence, c'est quelqu'un qui arrête d'en poser.
▼
COUCHE 4Rendu et emballage
  • React 19.2 · TanStack Router 1.170 / Start 1.168 · Vite · Tailwind 4.2 · TipTap 3.27 · Lenis · Sonner
  • Mapbox GL JS v3.9.0 chargé à l'exécution depuis le CDN — pas une dépendance npm. Voir F-04.
  • Marqueurs = éléments HTML au-dessus du canvas, jamais des couches WebGL
  • Contrat d'empilement : les onglets plein écran sont en z-90 ; tout ce que rend le moteur de carte reste sous 90. Les bandes de zone sûre sont en z-210.
  • Deux cibles de construction : Nitro (web, SSR) et SW_TARGET=app (coquille pré-rendue pour Capacitor)
Décision d'architecture — pas de fonction edge, pas de serveur d'application

Le projet ne déploie aucune fonction edge Supabase. La logique serveur est entièrement en PL/pgSQL, dans la même transaction que la donnée qu'elle protège. Conséquence favorable : un plafond anti-triche, un gain d'XP et l'écriture qui le déclenche ne peuvent pas diverger, parce qu'il n'y a pas deux endroits où ils vivent. Conséquence défavorable : tout ce qui demande à sortir du réseau (courriel, appel d'API tierce) doit passer par une fonction serveur TanStack — c'est le cas de l'envoi de réinitialisation via Resend, et c'est le seul endroit du système où une clé secrète existe.

05 Cartographie des sous-systèmes

Dix-huit sous-systèmes. Les totaux agrègent les modules nommés ; ils ne couvrent pas les fichiers partagés (i18n, storage, supabase, juice, units, utils), comptés une fois au volume global.

IDSous-systèmeModules principauxLignesÉtat
S-01Navigation & carte 3Dmapbox, NavExperience, camera, route-track, nav-view, cluster6 663OK*
S-02Flotte temps réel & présencefleet, chat, presence, parking1 180Non mesuré
S-03Identité, session, rangssession, profile, account, fleet-reset858Constat F-02
S-04Odomètre & anti-trichecommunity, profile, update_rider_km~290OK
S-05Progression, garage, véhiculesgarage, GarageView, ride-catalog, ride-grants, spots2 616OK
S-06Signalements routiersreports, ReportGlyph, ReportDial844OK
S-07Convoisconvoy, ConvoySheet1 009Non mesuré
S-08Commerces (POI / Places)places, PoiGlyph, PlaceSheet811Partiel
S-09Petites annoncesmarket, MarketSheet, MarketComposer, MarketCard, MarketGlyph2 111OK
S-10Social — crews, murs, privé, réputationorgs, wall, dm, social, notifs, street-cred, xp, riders, leaderboard2 617OK
S-11Média — radio, playlist, ID3radio, playlist, playlist-player, id3, provinceRadio, MediaCenter4 390Non mesuré
S-12Modération & sécurité communautairemoderation165Livré
S-13Console fondateur & back officeadmin-stats, ride-admin, radio-admin, orgs-admin, 6 panneaux3 480OK
S-14Plateforme, PWA, conteneur natifstreetwizard-pwa, wake-lock, graphics, nav-layers, Capacitor415Livré
S-15Lieux, trajets, Quick Traveluser-places, trip-history, home, QuickTravel625Livré
S-16Son, voix, ambiancesfx, voice, weather-sfx, media-fx, juice708Non mesuré
S-17Monétisation — promo, grants, VIPpromo, ride-grants, PromoSheet, AdminPromoPanel, AdminGrantsPanel1 505Livré
S-18Vitrine publique & partageRoute /org/$slug, directory, share-card, meet-card, pages légales2 522Livré

Les verdicts « OK » de S-04, S-05, S-06, S-09, S-10 et S-13 sont reportés de la révision A et datent du 2026-08-21. Aucun exercice n'a été relancé pour cette révision — voir M5 et la section 14.

06 Dossiers de sous-système

S-01Navigation et carte 3DCritique
Rôle
Rendre la carte, tenir la caméra sur le véhicule, calculer l'itinéraire, détecter la sortie de route, porter tous les marqueurs.
Pièces
mapbox.ts 756 · NavExperience.tsx 4 648 · camera.ts 251 · route-track.ts 171 · nav-view.ts 68 · cluster.ts 69 · Speedometer.tsx 551 · HighwaySignBoard.tsx 160

Décision — Mapbox n'est pas une dépendance npm

La bibliothèque est chargée à l'exécution, version épinglée v3.9.0, par injection d'un <script> pointant le CDN Mapbox (GL_VERSION, mapbox.ts:71). Favorable : zéro poids Mapbox dans le bundle initial. Défavorable : carte indisponible si le CDN ne répond pas, version verrouillée par une constante du code et non par le lockfile, et aucun contrôle d'intégrité de sous-ressource. Voir F-04.

Règles d'or — non négociables

  • Les marqueurs sont du HTML. Véhicules, pairs, signalements, commerces et annonces sont des éléments DOM au-dessus du canvas. Ni couches symboles, ni WebGL. Toute modification qui suppose des objets GL est fausse pour ce dépôt.
  • Bloquer la pression, pas le clic. Mapbox lie le déplacement sur mousedown/touchstart. Au moment où click se déclenche, la carte a déjà bougé. Tout contrôle dans un marqueur intercepte pointerdown + mousedown + touchstart (non passif) et click.
  • L'ancre du véhicule est padding.top. Mapbox centre sur (haut + hauteur − bas)/2 : un grand « bas » remonte la voiture. Inversion déjà commise une fois, documentée en garde dans camera.ts. Ne pas ré-inverser.
  • Certaines options n'existent qu'à la construction (anticrénelage, plafond de densité de pixels). Les changer impose de remonter la carte via sa clé React — déjà câblé.

Sortie de route — pourquoi l'écart latéral ne suffit pas

Une première version déclenchait le recalcul sur l'écart latéral. Ce critère reste à ~0 m pendant un demi-tour complet : on roule sur la ligne, à l'envers. Le module projette donc la position en curviligne et surveille la progression — WRONGWAY_M = 120 m de recul cumulé, plancher 15 km/h.

La pose de caméra — trois régimes, et un seuil de bascule

Nouveau depuis la révision A : la caméra n'a plus une pose mais trois, et le régime se choisit au zoom. BROWSE_ZOOM_HI = 16,5 : au-dessus, vue de conduite, angle du conducteur. BROWSE_ZOOM_LO = 12 : en dessous, carte à plat, on lit la ville. Entre les deux, interpolation. COCKPIT_PITCH est passé de 66 à 70, encadré par MIN 45 / MAX 80 pour que le doigt ne puisse pas mettre la caméra sous le sol ni la coucher à l'horizontale.

État : opérationnel. Deux corrections de terrain de la révision A attendent toujours une confirmation au volant (voir N-5).

S-02Flotte temps réel, présence et stationnementCritique
Canal
wo-fleet-live, mode présence, clé = guest_id de l'appareil
Pièces
fleet.ts 754 · chat.ts 147 · presence.ts 140 · parking.ts 139
Propriété de confidentialité — la position ne s'écrit pas

La position circule en présence — mémoire du serveur temps réel — et n'est jamais écrite en table. Elle disparaît à la déconnexion, y compris quand l'onglet meurt sans prévenir : Supabase purge la clé tout seul, ce qu'un battement de cœur en base ne sait pas faire proprement. Le mode fantôme et le fantôme automatique près du domicile coupent la diffusion en amont de la publication, pas à l'affichage.

Le stationnement — le seul endroit où une position s'écrit, et elle est décalée

fleet_parked, une ligne par rouleur, purgée dès qu'il repart en live. Règle : si le point de stationnement est à moins de PARK_SAFE_M de la maison, la position publiée est décalée sur l'église la plus proche (catégorie Search Box place_of_worship — le lieu de rassemblement par défaut de la scène) ; à défaut, sur une caserne ; à défaut, arrondie sur une grille d'environ 500 m. Le flou est honnête et déclaré : l'écran dit « secteur », pas une fausse adresse précise.

Présence en lecture seule — et pourquoi la vitrine web ne se déclare pas

presence.ts s'abonne et lit, rien d'autre. Se déclarer présent depuis la page web publique d'un crew ferait apparaître sur la carte, en conduite, quelqu'un qui lit une page web — un pin qui ne roule pas et ne bougera jamais. La déclaration reste l'affaire de useFleetChannel, sur la carte, là où il y a vraiment un véhicule.

S-03Identité, session, rangs et suppression de compteCritique
Pièces
session.ts 253 · profile.ts 456 · account.ts 83 · fleet-reset.ts 66
Tables
fleet_accounts, fleet_sessions, fleet_roles, fleet_admins — toutes scellées
Décision de sécurité — on n'accepte plus une identité en argument

La console fondateur prenait autrefois une adresse courriel et la comparait à celle du fondateur. Or cette adresse est publique : elle est dans le dépôt. Appelée en anonyme avec cette chaîne, la fonction rendait tout le registre des rouleurs.

Elle prend désormais un jeton de session et résout le rang côté serveur. Le progrès n'est pas la longueur du secret : c'est d'avoir cessé d'accepter une identité affirmée par l'appelant. Le motif a été généralisé à tout le back office ajouté depuis (ride_grant, promo_code_save, radio_station_save, orgs_admin…).

Le jeton — ce qu'il est, précisément

256 bits tirés par le serveur (extensions.gen_random_bytes(32), migration 0017), rendus en hexadécimal, insérés dans fleet_sessions avec une expiration à 90 jours. Chaque ouverture purge au passage les sessions expirées. Le client ne garde que ce jeton opaque ; le rang n'est jamais stocké localement comme une vérité, il est demandé au serveur (fleet_whoami).

Limites assumées — et elles sont importantes

1. Le guest_id n'est pas une preuve. C'est une chaîne locale, et elle sort du roster d'un crew, de la liste de ceux qui ont réagi à un post, des membres d'un convoi. Les plafonds bornent ce qu'un appelant peut faire par unité de temps ; ils ne prouvent pas qu'il est propriétaire de la ligne. Voir F-03.

2. La passphrase est réduite à un SHA-256 non salé, côté client. hashPass() (profile.ts:236) fait un crypto.subtle.digest et envoie le résultat ; le serveur compare pass_hash = p_hash. Il n'y a ni sel, ni coût, ni fonction de dérivation. Le condensat est le mot de passe. Voir F-02.

3. Le jeton vit dans localStorage, donc il est lisible par du code exécuté sur la page. C'est le compromis de toute app web sans cookie httpOnly. Le durcissement suivant est un cookie posé côté serveur ; il demande une route SSR et il n'est pas fait.

Suppression de compte — ce qui prouve que c'est bien toi

Un compte réclamé exige la passphrase : c'est la seule preuve d'identité que ce modèle possède. Un rouleur qui n'a jamais réclamé de compte n'a que son guest_id, et c'est déjà ce avec quoi toute l'app écrit en son nom ; le code ne fait pas semblant que c'en est plus. Le serveur efface ses lignes, puis l'état local est effacé — s'arrêter au serveur laisserait l'app ouverte sur un compte qui n'existe plus.

S-04Odomètre communautaire et anti-tricheCritique

Règle de plafonnement

On n'accepte que ce que 300 km/h sur le temps réellement écoulé autorise ; le surplus est jeté. Fenêtre plafonnée à 24 h, soit 7 200 km par rattrapage — jamais atteignable par un vrai rouleur. Le refus reste muet : une erreur explicite dirait à un script sous quel seuil se placer.

Panne de production documentée — à retenir

Le 6 août, une ligne est passée en gel permanent et s'est figée à 314,14 km. L'appareil, lui, comptait juste (697,7 km ; le niveau 37 affiché concorde avec la formule). Treize jours plus tard, l'odomètre mondial affichait 359,30 km : une seule ligne retenait 87 % du total.

Trois défauts distincts, tous corrigés : (a) km_synced_at était remis à jour par des appels qui n'acceptaient rien — un ping sans distance consommait la fenêtre de mesure, si bien que le delta honnête suivant se retrouvait divisé par des secondes au lieu des heures réellement roulées ; (b) le gel était définitif et sans appel ; (c) un rattrapage honnête après un trajet hors réseau — le cas normal dans un rang — était indistinct d'une triche.

Vérification en production, sur ligne jetable supprimée après : arrivée prise telle quelle · ping à delta nul laissant la fenêtre à 20,0 h · rattrapage de +380 km accepté · 99 999 km en une minute plafonné à +5,00 km sans drapeau · ping suivant toujours accepté · recul ignoré.

S-05Progression, garage et distribution de véhicules

Formule de niveau : niveau = ⌊ (km / 3)1/1,5 ⌋, bornée au maximum. Repères : niveau 10 → 95 km · niveau 22 → 305 km · niveau 37 → 676 km. Le niveau n'est jamais stocké comme vérité : il est recalculé. Côté serveur il n'est jamais repris — greatest(niveau, calculé) — pour qu'une retouche de formule ne déclasse personne.

Le garage est le plus gros module moteur du dépôt (garage.ts, 1 225 lignes) : catalogue de véhicules, sprites 3/4 pour la fiche et vue arrière détourée pour le pin de carte, déblocages par niveau, décors de showroom (spots.ts) débloqués eux aussi au niveau, de Montréal (gratuit) au niveau maximum.

Distribution — une RPC qui existait mais n'avait aucun bouton

Les RPC ride_grant, ride_grant_revoke et ride_grants_admin existaient depuis la migration 0024 et fonctionnaient. Ce qui manquait était l'écran : aucun appel dans toute l'app. Mesure prise avant d'écrire le module — ride_grants : 0 ligne. Les véhicules promis au premier crew hébergé n'étaient pas bloqués par une règle, ils n'avaient simplement aucun bouton. C'est un cas d'école de ce qu'une revue de code seule ne voit pas et qu'une mesure de la base montre.

Deux pistes de progression, volontairement séparées : le niveau se dérive des kilomètres — on ne peut pas se le fabriquer sans rouler — et l'XP mesure la vie sociale. Verser de l'XP de publication dans le niveau casserait la garantie : quelqu'un monterait au clavier et « niveau 37 » cesserait de vouloir dire « j'ai roulé 700 km ». Le gain d'XP est décidé par le serveur, dans la même transaction que le post : délai entre deux posts payants, plafond quotidien, bonus VIP.

S-06Signalements routiers

Le côté de la route n'est jamais demandé. Le cap GPS du signaleur part avec l'alerte ; le filtrage « devant moi, dans mon sens » se fait à la lecture. La concurrence pose la question ; ici on la déduit, ce qui retire un geste en conduisant.

Appui long = police, ~450 ms, sans rien ouvrir. C'est ~80 % des signalements : lui faire traverser un menu est le vrai défaut de la concurrence.

Diffusion sur un canal dédié wo-reports — ajouté depuis la révision A, où la relecture était périodique. Un signalement apparaît désormais chez les autres sans attendre la prochaine lecture. Durée de vie serveur par report_ttl, fusion des doublons, confirmation et infirmation par vote.

S-07ConvoisNouveau
Canal
wo-convoy-<id> — présence des membres + diffusion de la destination
Serveur
11 RPC : convoy_create, convoy_get, convoy_join, convoy_join_public, convoy_leave, convoy_mine, convoy_public_list, convoy_set_dest, convoy_set_visibility, convoy_halt
Scellées
fleet_convoys, fleet_convoy_members

Séparation nette entre ce qui doit survivre et ce qui doit disparaître : le groupe vit en base (il survit aux reconnexions, à la fermeture de l'app, au tunnel), les positions restent en présence sur le canal flotte global, filtrées par appartenance. Aucune position de convoi n'est écrite. Le leader partage une destination ; elle est diffusée sur le canal et route chez tout le monde.

S-08Commerces — POI et fiches PlacesPartiel

« Devant », pas « autour ». Le contraire de la concurrence, qui renvoie volontiers dix kilomètres en arrière. On ne garde que ce qui est devant : le long du trajet si l'on navigue, sinon dans un cône vers le cap.

État inchangé depuis la révision A : la fiche riche (photos, horaires, achalandage, Building ID) est architecturée en deux étapes — Search Box API pour le mapbox_id, puis l'endpoint Details — avec un substitut propre en attendant. Deux points restent à confirmer avec un vrai jeton : l'URL de l'endpoint et la forme du parseur. Ils sont isolés à deux endroits, marqués comme tels. Voir R-7.

S-09Petites annonces
La position est floutée en base, pas à l'affichage

Le brouillage (400 m à 1,2 km) est appliqué au stockage. Il n'existe donc aucun chemin par lequel l'adresse exacte d'un vendeur sortirait du serveur — ni par une API, ni par une erreur d'écran, ni par un client modifié.

Forme : le pin d'annonce est une pastille de prix, pas l'hexagone maison. Sur la même carte, une auto à vendre et un radar ne doivent pas avoir la même silhouette : l'une invite, l'autre avertit.

S-10Social — crews, meets, murs, privé, réputation
Règle de probité sur la réputation

Une valeur inconnue reçoit un état inconnu explicite (« NEW », tiret), jamais un nombre généré. Un score plausible est indistinguable d'une mesure pour la personne qui le lit — et il est affiché sous le nom d'une personne réelle. Depuis la migration 0046, la colonne nickname n'a plus de valeur par défaut : un nom inconnu arrive vide plutôt que faux, et l'écran doit rendre l'état inconnu.

Le rang vient du serveur (fleet_whoami), jamais de l'appareil. Il n'ouvre qu'un bouton de plus ; la RPC revérifie le galon de son côté. Un client modifié ne gagne rien à mentir.

Depuis la révision A : réactions typées sur le mur (migrations 0049 → 0063, dont un déploiement en trois temps — accepter les deux formes pendant la bascule, puis restreindre), épinglage de commentaire, liste des réacteurs, recherche de rouleurs, classement, fiches techniques de véhicule en texte riche.

S-11Média — radio, playlist, ID3Non mesuré

Chaque rouleur porte un CD ; les autres le consultent depuis sa fiche, mettent un « j'aime » ou un favori qui l'ajoute à leur roadtrip. Une chanson peut passer en direct sur la radio de province (canal wo-radio-<prov>, rotation serveur par radio_rotate).

Le fichier est la source de vérité, pas une API

Le titre, l'artiste et la pochette sont lus dans les tags ID3 du fichier déposé (id3.ts, 387 lignes). On ne va pas les chercher chez Spotify, Genius ou MusicBrainz — et ce n'est pas une question de budget. Le catalogue de Street Wizard, ce sont des pistes que les rouleurs ont faites eux-mêmes : elles n'existent dans aucun de ces catalogues. Une recherche par titre n'y renverrait pas « rien », elle renverrait la pochette d'une autre chanson qui porte le même nom, et on l'afficherait sous le nom d'un rouleur comme si c'était la sienne. C'est exactement la fabrication de données que le projet s'interdit.

4 390 lignes, aucune mesure automatisée dans ce dossier. C'est le plus grand angle mort recensé, et il a triplé de taille depuis la révision A — voir N-3.

Migration 0070, likes_were_failing_silently : les « j'aime » échouaient sans rien dire. Le nom du fichier est le constat. C'est le type de défaut que ce sous-système produira encore tant qu'il n'est pas exercé.

S-12Modération et sécurité communautaireNouveau

Mur public, commentaires, messages privés et annonces sont du contenu généré par les utilisateurs. La règle 1.2 de l'App Store exige de pouvoir signaler un contenu, bloquer une personne et joindre l'éditeur. Rien de tout cela n'existait.

Le blocage est privé

Personne n'est prévenu d'avoir été bloqué. Un blocage annoncé se retourne contre la personne qui bloque — c'est la première chose qu'on apprend en modérant quoi que ce soit.

Limite déclarée dans le code lui-même

blockedIds() rend les identifiants bloqués et les écrans filtrent à la lecture. Les lignes arrivent donc encore au téléphone avant d'être écartées. Le filtrage côté serveur, dans les RPC de flux, est le durcissement suivant — il n'est pas fait, et le module le dit au lieu de le laisser croire. Voir D-1.

S-13Console fondateur et back office

Quatre états distincts sont rendus — pas de session / session sans rang / hors ligne / d'accord — parce que la version précédente rendait null dans les quatre cas et affichait la même phrase grise : impossible de savoir laquelle, donc impossible de réparer.

Fenêtre sur les gels : le gel est muet pour le rouleur, par conception. Le prix de ce silence est qu'un faux positif ne se voyait nulle part. La console affiche donc en permanence le compte de gels — un zéro y est un zéro mesuré — et, dès qu'il dépasse zéro, une bande dit combien et depuis quand, le rouleur concerné remonté en tête de liste.

Étendu depuis la révision A en six panneaux : véhicules (catalogue et variantes), codes promo, dons de véhicules, stations de radio, crews, télémétrie serveur. 3 480 lignes. Toutes les RPC de ces panneaux prennent un jeton et résolvent le rang côté serveur : aucune identité n'est acceptée en argument.

S-14Plateforme, PWA et conteneur natif

Veille écran : GPS = écran toujours allumé sur la carte, verrou repris automatiquement au retour d'arrière-plan. La veille tue le GPS ; c'est un verrou fonctionnel, pas un confort.

Couches de navigation : un registre central fait que le geste retour Android/PWA referme la surface ouverte au lieu de quitter la carte. Vingt-deux surfaces y sont inscrites.

Les bandes de zone sûre — ce qui les peint réellement

En PWA installée sur iPhone avec apple-mobile-web-app-status-bar-style: black-translucent et viewport-fit=cover, iOS rend la barre d'état transparente et c'est la page qui peint les zones sûres. Ni theme-color, ni theme_color du manifeste ne les peignent — le manifeste est en plus mis en cache par iOS et n'est relu qu'à la réinstallation. Ce qui les peint : deux div fixes en z-210, hauteur env(safe-area-inset-top) et env(safe-area-inset-bottom), fond var(--cyan).

Condition nécessaire, découverte à la troisième tentative : viewport-fit=cover doit être déclaré à la racine, pas seulement sur la route carte. L'app est rendue à la racine par index.tsx (l'URL reste propre, sans /carte), et cette route ne déclarait aucun viewport — on retombait sur celui par défaut, sans cover, donc env(safe-area-inset-top) valait 0 et la bande avait une hauteur nulle. Elle n'a jamais pu s'afficher, ni en navigateur, ni en PWA.

background_color reste le gris de l'app (#14171d) : il peint l'écran de lancement, pas les bandes, et un éclair cyan à l'ouverture n'est pas voulu.

Le conteneur natif — ce qui est fait et ce qui ne l'est pas

Capacitor 8.5.1, identifiant net.streetwizard.app, dossier android/ présent. La cible build:app coupe Nitro et demande à TanStack Start de pré-rendre une coquille — vérifié : dist/client/index.html, 5 Ko, avec la feuille de style et l'amorce du client. C'est ce dossier qu'on embarque.

On n'utilise pas server.url. Pointer la webview sur https://streetwizard.net serait cinq minutes de travail, et ce serait littéralement le site dans une fenêtre : le cas que la règle 4.2 d'Apple vise en premier, sans démarrage hors-ligne ni écran de chargement propre. La coquille embarquée démarre sans réseau ; les données arrivent ensuite, comme dans le navigateur.

Ce qui reste à faire, déclaré : le dossier ios/ (exige macOS et CocoaPods — un projet natif déposé sans qu'aucun Xcode ne l'ait ouvert serait invérifiable) ; l'origine publique à donner à la fonction serveur de réinitialisation, qui n'existe pas dans la coquille ; les permissions natives (position en arrière-plan, audio en veille) et leurs textes de justification iOS — ce sont eux qu'Apple lit.

S-15Lieux, trajets et Quick TravelNouveau

Trois sources volontairement distinctes, affichées séparément : les favoris et le travail vivent en base (user_places, scellée, plafonnée par user_places_cap) ; l'historique vit en base aussi (trip_history, scellée) ; la maison ne quitte jamais l'appareil.

Les fondre en une seule liste ferait disparaître une différence qui compte : un favori est une intention, une entrée d'historique est une trace. L'historique est écrit à l'arrivée, jamais au départ — une destination abandonnée en chemin n'est pas un trajet, et la reproposer serait proposer de refaire une route qu'on a précisément décidé de ne pas faire.

La maison reste en localStorage parce que c'est elle qui pilote le mode fantôme : la donnée la plus sensible du produit est aussi la seule qui n'a jamais de raison de partir sur le réseau.

S-16Son, voix et ambianceNon mesuré

Le guidage vocal avait été coupé (speak() réduit à un no-op) parce qu'il entrait en conflit avec le design sonore : la voix parlait par-dessus l'ambiance et le démarrage moteur, et deux sons qui se battent valent moins que le silence. Le remettre en enlevant le return aurait reproduit le problème à l'identique — le défaut n'était pas la voix, c'était l'absence de règles entre elle et le reste.

Les quatre règles, dans voice.ts :

  1. Elle baisse le reste. Toute annonce coupe l'ambiance le temps de parler et la rend ensuite. La voix ne se superpose plus, elle prend la parole.
  2. Elle ne s'empile pas. Une file d'attente est pire que rien en voiture : une consigne qui arrive trois secondes après le virage fait tourner au mauvais endroit. On annule ce qui est en cours et on dit la chose neuve.
  3. Elle a une priorité. Un virage passe devant une alerte routière ; une alerte ne coupe jamais une consigne de manœuvre.
  4. Elle parle la langue de l'app (fr/en), pas un fr-CA codé en dur, et choisit une vraie voix de cette langue si le système en a une.

L'ambiance suit la météo réelle du point où l'on roule (Open-Meteo, sans clé, cache par arrondi de coordonnées) : beau temps et pluie ne sonnent pas pareil.

S-17Monétisation — codes promo, dons, VIPNouveau

Aucun paiement n'est encaissé dans l'application à ce jour : il n'y a ni Stripe, ni achat intégré, ni abonnement. Ce sous-système est l'outillage de la monétisation — ce qui permet de distribuer, de récompenser et de mesurer — pas la caisse.

Codes promo : deux temps séparés à dessein — regarder (promo_peek) puis échanger (promo_redeem). Le rouleur voit ce que le code lui ouvre et choisit sa caisse, au lieu d'échanger à l'aveugle et de découvrir après coup ce qu'il vient de prendre. Un choix irréversible ne se fait pas les yeux fermés. Trois tables, toutes scellées.

Dons de véhicules : quatre natures — boss, crew_perk, gift, staff — attribuées depuis le back office, jeton obligatoire, révocables.

VIP : xp_is_vip côté serveur ; le statut modifie les règles de gain d'XP, pas le niveau.

S-18Vitrine publique et partageNouveau

Page de crew publique (/org/$slug, 1 546 lignes) : lisible sans app, sans compte, indexable. Roster, présence en direct (en lecture seule — voir S-02), véhicules du crew, mur. C'est la surface d'acquisition du produit : un crew partage son lien, et ce lien montre quelque chose de vivant plutôt qu'une page d'atterrissage.

Cartes partageables : assemblage sur un <canvas> 1200×630 des mêmes calques que le garage (décor + véhicule + fiche technique), export JPEG, partage natif sur mobile et téléchargement ailleurs. 100 % client, aucun rendu serveur : léger, instantané, zéro dépendance.

Pages légales : /privacy, /terms, /data-deletion. Prérequis de soumission des deux boutiques — voir section 11.

07 Surface de base de données

93tables
203fonctions
92,6 %security definer
180index
109politiques rls
21tables scellées
23déclencheurs
0vue
3,8 Mo43 tables street wizard
71migrations
100 %couverture rls
0fonction edge

Le motif « table scellée »

Vingt-et-une tables portent RLS actif et zéro politique. Ce n'est pas un oubli : c'est le motif. Avec RLS actif et aucune politique, PostgreSQL refuse tout au client anonyme — lecture comme écriture, quelle que soit la clé qu'il présente. Le seul chemin restant est une fonction SECURITY DEFINER qui revérifie ses propres règles.

Table scelléeCe qu'elle protègeNature
app_secretsSecrets applicatifsSecret
fleet_sessionsJetons de session (256 bits, 90 j)Identité
fleet_accountsComptes réclamés, condensats de passphraseIdentité
fleet_roles · fleet_adminsRangs et administrationIdentité
fleet_dm · fleet_notifsContenu privéVie privée
fleet_blocks · fleet_abuse_reportsBlocages et signalements NouveauVie privée
fleet_convoys · fleet_convoy_membersConvois NouveauVie privée
trip_history · user_placesTrajets et lieux d'une personne NouveauVie privée
promo_codes · promo_code_rides · promo_redemptionsCodes et échanges NouveauValeur
ride_catalog · ride_grants · org_ridesCatalogue et attributions NouveauValeur
radio_stationsGrille de diffusion NouveauValeur
fleet_queueAncienne file du meet, fermée par 0064Vestige scellé

Ces vingt-et-une tables sont exactement celles qui portent soit un secret, soit une identité, soit un contenu privé, soit une valeur distribuable. La correspondance n'est pas fortuite, et elle a tenu à l'ajout de six tables depuis la révision A — c'est le meilleur signe que le motif est compris et pas seulement appliqué.

Partage de schéma — le constat de la révision A, re-mesuré

La portée a diminué, et on peut le chiffrer

La révision A relevait 54 tables d'un autre produit (bons de travail, factures, clients, devis, employés, paie horaire) dans le schéma wizardops, derrière la même clé anonyme, et classait le rayon d'explosion comme couvrant les deux produits.

Mesures au 2026-09-15 : il reste 50 tables étrangères dans wizardops. Elles totalisent 116 lignes. 19 sont entièrement vides. 41 existent à l'identique dans le schéma chantier (41 tables, 36 fonctions) et 40 dans public (42 tables, 39 fonctions), où le produit vit désormais. Ce sont des résidus de migration, pas un produit en exploitation.

Le risque n'est pas nul pour autant : une politique RLS trop permissive posée sur l'une de ces 50 tables reste exposée à la clé publique de Street Wizard, et un développeur qui écrit dans la mauvaise copie perd sa donnée en silence. Mais le constat passe de majeure à mineure, et le remède change de nature : ce n'est plus « séparer deux produits », c'est supprimer 50 tables vides. Voir F-01 et R-3.

Numérotation des migrations

71 fichiers, de 0001_market_get à 0072_indexes_for_the_hot_polls. Deux numéros n'existent pas : 0020 et 0058. Les collisions relevées à la révision A (deux fichiers 0016, deux 0017) subsistent telles quelles — renommer un fichier déjà appliqué provoquerait un rejeu chez quiconque suit le registre des migrations. La décision reste au propriétaire. Voir F-05.

Les noms de migration portent le constat plutôt que le changement, et c'est une pratique du dépôt qui a de la valeur en vérification diligente : 0040_unpoison_rider_names, 0046_no_name_is_not_a_name, 0062_civic_ef_is_not_a_starter, 0070_likes_were_failing_silently, 0072_indexes_for_the_hot_polls. Un lecteur apprend l'histoire du produit en lisant la liste des fichiers.

08 Registre des invariants

Ces valeurs ne sont pas décoratives : chacune répare un comportement observé sur la route. Toutes lues dans le code au commit en tête de dossier — aucune reprise de la révision A.

Caméra et pose — camera.ts

ConstanteValeurRaison d'être
COCKPIT_PITCH70Inclinaison en cockpit — 66 à la rév. A, relevée pour rendre l'horizon
COCKPIT_PITCH_MIN / _MAX45 / 80Le doigt ne peut mettre la caméra ni sous le sol ni à l'horizontale Nouveau
WIDE_PITCH / WIDE_ZOOM62 / 16,2Vue large non-cockpit (démo, présentation) Nouveau
COCKPIT_ZOOM_STILL / _FAST18,2 / 16,8Le zoom recule à ≥ 90 km/h
BROWSE_ZOOM_HI / _LO16,5 / 12Seuils de bascule entre vue de conduite et carte à plat Nouveau
VEHICLE_ANCHOR_TOP0,34Ancre haute — jamais « bas ». Inversion déjà commise une fois
MOVE_ROT_KMH3Sous cette vitesse, pas de rotation
STOP_KMH2,5Seuil « arrêté » Nouveau
POS_RATE / POS_RATE_IDLE3,5 / 0,6Gain doux à la sortie du bruit GPS (ease-in, anti-à-coup)
PEER_POS_RATE6Les pairs se rattrapent plus vite que soi Nouveau
BEARING_RATE / ZOOM_RATE3,0 / 2,0Cap, par l'arc le plus court ; zoom
SNAP_METERS250Au-delà : saut, pas de glissement (1er fix, téléportation GPS)
GPS_NOISE_MIN_M / MAX_M4 / 30On tient au moins dans 4 m ; on ne gèle jamais au-delà de 30 m
DR_MAX_S / DR_MIN_KMH / DR_ARM_S / DR_BLEND1,4 / 4 / 1,2 / 0,45Dead-reckoning : horizon de prédiction, plancher, armement, lissage EMA
POSE_DEADBAND_M5Seuil de déplacement réel
SPRITE_BOX_PX / PEER_BOX_PX118 / 96Boîte du sprite véhicule et du pin de pair Nouveau

Trajet, odomètre, signalements, vie privée

ConstanteValeurRaison d'être
WRONGWAY_M120 mRecul cumulé avant recalcul — l'écart latéral reste à 0 pendant un demi-tour
WRONGWAY_MIN_KMH15 km/hPlancher de vitesse
HEADING_MIN_KMH / _M8 / 8Anti-girouette à l'arrêt
Plafond odomètre300 km/hPlafond physique, pas légal
Fenêtre max24 h7 200 km par rattrapage — jamais atteignable
Formule de niveau⌊(km/3)1/1,5⌋Recalculée, jamais stockée ; greatest() côté serveur
Appui long « police »~450 ms~80 % des signalements, sans ouvrir de menu
Brouillage annonce400 m – 1,2 kmAppliqué au stockage, pas à l'affichage
PARK_SAFE_Mseuil « près de la maison »En dessous : décalage sur église / caserne / grille ~500 m
Expiration de session90 joursPurge des expirées à chaque ouverture
Jeton de session256 bitsgen_random_bytes(32), hex

09 Design — système visuel, sonore et d'interaction

Cette section est nouvelle. Le design de Street Wizard n'est pas une couche de peinture posée sur l'ingénierie : la moitié des décisions listées ici existent parce qu'une décision visuelle a cassé quelque chose sur la route, et elles sont aussi contraignantes que les invariants de la section 8.

9.1 · La contrainte qui commande tout — c'est une interface de conduite

Le cahier des charges tient en une phrase

La personne qui regarde cet écran conduit une voiture. Elle a une main, deux secondes, et du soleil sur la vitre. Toute décision de design se juge à ça d'abord, et à l'esthétique ensuite.

Conséquences appliquées dans tout le produit :

  • Un geste, pas un menu. L'appui long pose un signalement « police » en ~450 ms sans rien ouvrir. C'est ~80 % des signalements ; les faire traverser un menu est le vrai défaut de la concurrence.
  • On déduit ce qu'on peut déduire. Le côté de la route n'est jamais demandé : le cap GPS part avec l'alerte. Une question de moins.
  • Une info périmée vaut moins que rien. Les statuts expirent et affichent « inconnu » plutôt qu'un vieux chiffre grisé.
  • Jamais un vide au premier trou de réseau. Un échec de lecture garde la liste précédente. Effacer les épingles ferait clignoter la carte dans les rangs, là où elles servent le plus.
  • Un refus se voit. Un favori qui disparaît en silence, c'est quelqu'un qui arrête d'en poser.
  • Les cibles sont grosses et la pression est interceptée avant le clic — voir S-01, « bloquer la pression, pas le clic ».

9.2 · Palette

Une seule couleur d'accent dans toute l'application, et elle est tenue à un seul code hexadécimal. Trois teintes approchantes (#72A8AC, #02ACB4, #8ACBC4) se contredisaient autrefois entre le meta theme-color et les deux manifestes ; elles ont été réduites à la valeur de styles.css.

#8ACBC4--cyan · accent unique
#7AB4AE--cyan-glow
#0E1117fond sombre
#171B22carte / surface
#1C212Asecondaire
#262C36bordure
#E9EDF3texte
#94A1B2texte atténué
#8A9EAD--steel
#090B0Fpied de page
Le mode clair existe dans les jetons, et il n'est pas un produit

:root définit un jeu clair complet (#F4F5F7, cartes blanches) hérité du gabarit de départ. Street Wizard n'a pas de mode clair : rien dans l'app n'appelle useTheme ni setTheme, il n'y a aucun sélecteur. Le fournisseur de thème reste parce que des composants ui/ l'attendent, mais son défaut a dû être corrigé de light à dark — il réécrivait la classe posée au SSR à l'hydratation et le document repassait au blanc cassé.

Le défaut ne se voyait pas partout, parce que les écrans de l'app sont des fixed inset-0 opaques. Il ressortait exactement là où ils ne couvrent pas : le rebond de défilement sur téléphone, et les routes qui ne peignent pas leur propre fond. D'où « tout est blanc en arrière-plan », un symptôme que personne n'avait relié à un defaultTheme. Dette assumée : un jeu de couleurs mort est un piège pour le prochain développeur. Voir D-2.

9.3 · Typographie

RôlePoliceEmploi
--font-displaySpace Grotesk 500–800Titres, chiffres de tableau de bord, compteur de vitesse
--font-sansChivo 400–900Corps de texte, interface
--font-terminalVT323Fiches de profil et vitrines — « les gens s'expriment en look console »

Chaque famille porte une pile de repli réelle (system-ui, ui-monospace, Menlo) : une police qui ne charge pas ne doit pas décaler la mise en page d'un écran de conduite. Rayon de base --radius: 0.75rem, décliné en sept échelons.

9.4 · Signalétique — le registre des glyphes

Règle absolue — aucun emoji dans la signalétique

Un emoji n'a ni notre couleur, ni notre épaisseur, ni notre géométrie ; il est redessiné par chaque fabricant (Apple ≠ Android ≠ Windows) et porte un registre « clavier » à l'opposé d'un cockpit. La règle a dû être réécrite trois fois dans le dépôt avant d'être tenue.

Tous les tracés vivent dans un seul fichier — components/icons/index.tsx, 975 lignes — et cette source unique sert à la fois React et le DOM pur des marqueurs Mapbox. Aucune dérive possible entre la carte et les feuilles.

FormeSujetCe qu'elle dit
Hexagone pointu, coloréAlerte routièreUrgent
Hexagone pointu, en osServiceCalme, infrastructure
Pastille rondeCommerceCommodité ambiante
Pastille de prixAnnonceInvitation
Écusson (image)PoliceLe seul objet déjà connu de tous
Sprite de véhicule, vue arrièreSoi et les pairsEn chase-cam on voit l'arrière — le 3/4 est réservé à la fiche

Règles communes : boîte de 24 × 24, silhouette pleine plutôt que trait fin, détails intérieurs percés à l'encre du fond plutôt que posés en blanc — un trait fin disparaît au soleil, un détail blanc bave sur un fond clair.

Décision de palette — les services sont en os

Deux teintes de services reprenaient exactement celles d'alertes existantes : un truck stop portait le bleu POLICE et une halte le violet RADAR. Une halte routière et un radar étaient donc le même hexagone violet sur la même carte.

La palette d'alertes occupant déjà bleu, violet, rouge, ambre, orange, jaune et rose, aucun décalage de code hexadécimal ne réglait le problème : toute nouvelle famille finissait collée à l'une d'elles. Les services sont donc en os (#DCE3EC). Conséquence voulue : la seule couleur franche d'une épingle de service est sa pastille de statut — « plein » ou « libre », l'information qui décide si l'on continue.

9.5 · Design sonore

Cinq boucles, 2,3 Mo. Le son n'est pas décoratif : c'est le seul canal de retour disponible quand les yeux sont sur la route. Trois règles :

  • Une seule chose parle à la fois. Toute annonce vocale baisse l'ambiance (duckAmbience) et la rend ensuite. Deux sons qui se battent valent moins que le silence — c'est la raison pour laquelle la voix avait été coupée pendant un temps.
  • L'ambiance suit le monde réel. La boucle change avec la météo au point où l'on roule (Open-Meteo).
  • Rien ne s'empile. Pas de file d'attente vocale : on annule et on dit la chose neuve.

9.6 · Mise en page, zones sûres et cible physique

  • Contrat d'empilement : onglets plein écran en z-90 ; tout ce que rend le moteur de carte reste sous 90 ; bandes de zone sûre en z-210.
  • Barre d'onglets : hauteur fixée par jeton (--sw-tabbar-h: 5.5rem) pour que rien ne calcule une hauteur de pouce à la main.
  • Zones sûres : les extrémités haute et basse du téléphone sont cyan, volontairement — c'est une décision de design, pas un défaut. Voir S-14 pour le mécanisme, et pour la lecture erronée qui les avait rabattues sur le gris.
  • Écran de lancement : l'affiche mobile est ancrée sous env(safe-area-inset-top) et cadrée en object-contain — en inset-0 elle passait sous l'heure et la batterie.
  • Geste retour : vingt-deux surfaces inscrites dans un registre central, pour que le retour Android referme la surface ouverte au lieu de quitter la carte.

9.7 · Langue

Français et anglais, partout, par un fournisseur unique (i18n.tsx) et un helper tr({ fr, en }) appelé au point d'affichage. Il n'y a pas de fichiers de traduction séparés : les deux versions d'une phrase vivent côte à côte dans le composant qui l'affiche. Avantage : une phrase ne peut pas être ajoutée dans une seule langue sans que ça se voie à la relecture. Inconvénient : l'ajout d'une troisième langue demanderait une extraction. Le guidage vocal suit la langue de l'app et choisit une vraie voix système de cette langue.

9.8 · Règles de probité appliquées au design

Trois règles du dépôt sont des règles de design avant d'être des règles de code, et elles sont ce qui distingue le produit :

  1. Une valeur inconnue s'affiche comme inconnue. « NEW », un tiret — jamais un nombre plausible. Un score généré est indistinguable d'une mesure pour la personne qui le lit, et il porte le nom d'une personne réelle.
  2. On n'affiche pas la donnée d'un tiers sous le nom de quelqu'un. Aucune pochette n'est cherchée par titre chez un service de musique : on afficherait celle d'une autre chanson sous le nom d'un rouleur.
  3. Un flou déclaré vaut mieux qu'une fausse précision. La position de stationnement affiche « secteur » plutôt qu'une adresse inventée ; le brouillage des annonces est appliqué au stockage, donc l'écran ne peut pas mentir mieux que la base.

10 Sécurité

Section nouvelle. Elle décrit le modèle de menace réel, les contrôles en place, les données personnelles traitées, et — surtout — ce qui ne tient pas. Un chapitre de sécurité qui ne liste que des contrôles est un argumentaire, pas un audit.

10.1 · Modèle de menace

ActeurCe qu'il peutCe qui l'arrêteRésiduel
Client modifié (console navigateur, app décompilée)Appeler n'importe quelle RPC, mentir sur son rang local, forger un guest_idLe rang est résolu côté serveur depuis le jeton ; les 21 tables scellées refusent tout accès direct ; 188 fonctions SECURITY DEFINER revérifientÉcriture sous une identité non prouvée — F-03
Tricheur à l'odomètreInjecter des kilomètres pour monter de niveauPlafond physique 300 km/h × min(écoulé, 24 h) ; recul ignoré ; refus muet ; niveau recalculé et jamais repris à la baisseFaible — mesuré en production sur ligne jetable
HarceleurSuivre une personne, la contacter, la localiserBlocage privé ; signalement ; la position n'est jamais écrite ; mode fantôme ; fantôme automatique près du domicile ; stationnement décaléFiltrage des bloqués à la lecture — les lignes arrivent au téléphone avant d'être écartées — D-1
Voleur d'identifiantsDeviner ou rejouer une passphraseTables fleet_accounts et fleet_sessions scellées ; jeton de 256 bitsSHA-256 non salé, pas de limitation de débit — F-02
Fuite de la clé anonymeRien qu'un utilisateur normal ne puisse déjà fairePar conception : la clé est publique, elle est dans le bundle. Toute la sécurité est en RLS et en RPC, aucune n'est dans le secret de la cléNul — le modèle assume la publication
Fuite de la clé de serviceToutElle n'est jamais livrée au client. Aucune occurrence dans le bundleNul côté client ; dépend de l'hygiène du tableau de bord d'hébergement
XSS sur la pageLire le jeton en localStorageReact échappe par défaut ; le texte riche passe par TipTapPas de cookie httpOnly — déclaré dans session.ts
Indisponibilité du CDN MapboxRendre la carte inutilisableRienPas de SRI, pas d'hébergement local — F-04

10.2 · Contrôles en place

  • RLS à 100 %. 93 tables sur 93. Aucune table du schéma n'est accessible sans passer par une politique ou une fonction.
  • 21 tables scellées. RLS actif, zéro politique : refus total au client, quel que soit le rôle. Seul chemin : une fonction SECURITY DEFINER.
  • 92,6 % des fonctions en SECURITY DEFINER, chacune revérifiant ses propres règles plutôt que de faire confiance à l'appelant.
  • Aucune identité acceptée en argument sur les chemins privilégiés : le jeton part, le serveur résout le rang. Motif établi sur la console fondateur, généralisé à tout le back office.
  • Jetons de 256 bits tirés par gen_random_bytes, expiration à 90 jours, purge des expirés à chaque ouverture.
  • La position ne s'écrit pas. Elle circule en présence et disparaît à la déconnexion. La seule position écrite (stationnement) est décalée avant publication.
  • Le brouillage des annonces est au stockage, pas à l'affichage : aucun chemin ne peut exposer l'adresse exacte.
  • La maison ne quitte jamais l'appareil.
  • Aucun secret dans le client. Le jeton Mapbox est un jeton public (pk.), protégé par restriction d'URL côté Mapbox ; la clé anonyme Supabase est publique par conception ; la clé de service et la clé Resend ne vivent que côté serveur.
  • Aucune fonction edge : pas de surface serveur supplémentaire à durcir.

10.3 · Données personnelles traitées

DonnéeOù elle vitDuréeSortie possible
Position en directMémoire du serveur temps réel (présence)Le temps de la sessionDiffusée aux pairs, sauf mode fantôme
Position de stationnementfleet_parked, 1 ligneJusqu'au retour en livePublique, décalée si près du domicile
DomicilelocalStorage seulementJusqu'à effacementAucune
Historique de trajetstrip_history — scellée, plafonnéeJusqu'à suppressionRPC propriétaire uniquement
Lieux enregistrésuser_places — scellée, plafonnéeJusqu'à suppressionRPC propriétaire uniquement
Courrielfleet_accounts — scelléeJusqu'à suppression du compteRéinitialisation via Resend
Condensat de passphrasefleet_accounts — scelléeJusqu'à suppressionAucune — mais voir F-02
Messages privésfleet_dm — scelléeJusqu'à suppressionRPC des deux correspondants
Kilométrage, niveau, pseudo, véhiculefleet_scores — accès direct en lecturePermanentPublic — c'est le classement
Musique déposéeSeau fleet-tracksJusqu'à suppressionPublique par URL
Annoncesfleet_marketJusqu'à retraitPublique, position floutée en base

Le produit ne collecte ni analytique tierce, ni pisteur publicitaire, ni identifiant publicitaire : il n'y a aucune dépendance de ce type dans le package.json. La seule télémétrie est interne (fleet_admin_stats, fleet_admin_server) et sert la console fondateur.

10.4 · Ce qui ne tient pas — à lire avant toute discussion de sécurité

1 · La passphrase est un condensat non salé, et il est porteur

hashPass() fait crypto.subtle.digest("SHA-256", …) côté client et envoie l'hexadécimal ; fleet_session_open_nick compare pass_hash = p_hash. Trois conséquences distinctes :

(a) Le condensat est le mot de passe. Qui l'obtient — par une fuite de base, par un XSS, par un journal réseau — s'authentifie sans jamais connaître la passphrase.

(b) Aucun sel. Deux personnes avec la même passphrase ont le même condensat, et un condensat SHA-256 de passphrase humaine se retrouve en table arc-en-ciel publique.

(c) Aucun coût. Pas de bcrypt, d'Argon2 ni de PBKDF2 : une attaque hors ligne progresse à la vitesse du matériel.

Atténuation en place : fleet_accounts est scellée, donc le condensat n'est pas lisible par le client. Atténuation absente : aucune limitation de débit sur fleet_session_open / fleet_session_open_nick, qui sont toutes deux grant execute … to anon. Le bourrage d'identifiants est possible depuis l'extérieur. Voir F-02 et R-1.

2 · Le guest_id n'est pas une preuve, et il circule

C'est une chaîne locale, et elle apparaît dans le roster d'un crew, la liste des réacteurs d'un post, les membres d'un convoi. Les fonctions plafonnent ce qu'un appelant peut faire par unité de temps mais ne prouvent pas qu'il possède la ligne visée. Le remède est connu et déjà appliqué ailleurs : exiger le jeton de session sur toutes les écritures identitaires, comme le back office le fait déjà. Voir F-03 et R-2.

3 · Le blocage filtre à la lecture

Les lignes d'une personne bloquée arrivent au téléphone avant d'être écartées. Le module le déclare lui-même. Le durcissement est de filtrer dans les RPC de flux. Voir D-1.

4 · Le jeton vit dans localStorage

Lisible par tout code exécuté sur la page. C'est le compromis de toute app web sans cookie httpOnly, et c'est très largement au-dessus de « une adresse courriel dans un champ texte », qui était l'état antérieur. Le durcissement suivant demande une route SSR ; il n'est pas fait.

5 · La bibliothèque de carte vient d'un CDN sans contrôle d'intégrité

Mapbox GL JS v3.9.0 est injecté depuis api.mapbox.com, version épinglée par une constante et non par le lockfile, sans attribut integrity. Un compromis du CDN exécuterait du code arbitraire sur la page — la page qui porte le jeton de session. Voir F-04 et R-6.

11 Audit

Section nouvelle. Trois volets : les avis automatisés de la base, l'état de conformité aux boutiques d'applications et à la protection des données, et l'état de la vérification du code.

11.1 · Avis automatisés de la base — relevé intégral

Linter de sécurité Supabase, exécuté le 2026-09-15 à 01:55 UTC sur la base de production. Six familles remontées. Elles sont reproduites toutes, y compris celle qui est attendue et celle qui concerne l'autre produit — un relevé filtré n'est pas un relevé.

NiveauAvisOccurrencesLecture
INFOrls_enabled_no_policy21Attendu — c'est le motif, pas un défaut. Ce sont exactement les 21 tables scellées. Le linter ne peut pas distinguer « scellée volontairement » de « politique oubliée » ; le dossier le peut, et la liste de la section 7 est la preuve d'intention.
WARNfunction_search_path_mutable18 · dont 14 dans wizardopsÀ corriger. Une fonction sans search_path figé peut être détournée par un schéma injecté dans le chemin de résolution. Les 14 concernées sont des fonctions utilitaires et des déclencheurs : xp_post_rules, user_places_cap, trip_history_cap, touch_kpi_updated, touch_updated_at, _specs_clean, fleet_scores_keep_max, fleet_songs_cap, fleet_slugify, level_from_km, report_ttl, road_vote_radius_m, road_status_ttl, _road_dist_m. Correction mécanique : set search_path to 'wizardops'. Voir R-4.
WARNanon_security_definer_function_executable250 remontées (plafond) · 181 dans wizardopsAttendu, avec une nuance à ne pas escamoter. C'est la conséquence directe du modèle : l'app est anonyme, donc les RPC doivent être appelables par anon. La défense n'est pas le grant, elle est dans le corps de chaque fonction. La nuance : cet avis ne vaut que si chaque fonction vérifie réellement — ce qui est vrai pour le back office (jeton) et faux pour les écritures à guest_id (F-03). L'avis et le constat F-03 pointent le même endroit.
WARNauthenticated_security_definer_function_executable250 · idemMême cause, même lecture.
WARNextension_in_public1pg_net installée dans public. Non utilisée par Street Wizard. À déplacer par hygiène.
WARNauth_leaked_password_protection1La vérification HaveIBeenPwned de Supabase Auth est désactivée. Sans effet ici : Street Wizard n'utilise pas Supabase Auth, il a son propre modèle de session. À activer si Supabase Auth est un jour adopté — ce qui est précisément le remède recommandé en R-1.
Ce que le relevé ne dit pas

Les deux avis à 250 occurrences sont plafonnés par l'outil : 250 est la limite de remontée, pas le compte réel. Le compte réel est au moins 250 et n'a pas été mesuré exhaustivement. C'est écrit ici plutôt que reporté comme « 250 » sans mention.

11.2 · Conformité boutiques d'applications

État vis-à-vis des règles que les deux boutiques appliquent en revue. Cette grille n'existait pas à la révision A, et la moitié des lignes étaient alors en défaut.

ExigenceRègleÉtatPreuve
Suppression de compte dans l'appApp Store 5.1.1(v) · Google PlayConformeaccount.ts, fleet_account_delete, migration 0066, route /data-deletion
Signalement de contenuApp Store 1.2Conformefleet_report, fleet_abuse_reports, migration 0067
Blocage d'utilisateurApp Store 1.2Conformefleet_block / fleet_unblock, fleet_blocks
Moyen de joindre l'éditeurApp Store 1.2Conformemoderation.ts
Politique de confidentialité accessibleLes deuxConformeRoute /privacy
Conditions d'utilisationLes deuxConformeRoute /terms
Pas un simple site emballéApp Store 4.2ConformeCoquille pré-rendue embarquée, démarrage hors réseau ; server.url délibérément non utilisé
Marques déposées de véhiculesApp Store 5.2TraitéMigration 0068_ride_names_out_of_trademarks
Textes de justification des permissions iOSApp Store 5.1.1À faireDéclaré dans capacitor.config.ts — position en arrière-plan, audio en veille
Projet natif iOSPrérequis de soumissionÀ faireDossier ios/ absent — exige macOS et CocoaPods
Réinitialisation dans la coquille nativeFonctionnelÀ trancherLa fonction serveur n'existe pas dans la coquille ; soit lui donner l'origine publique, soit garder ce parcours sur le web
Contenu généré — filtrage préventifApp Store 1.2PartielSignalement et blocage en place ; aucune modération automatisée ni file de revue. Acceptable à cette échelle, à revoir avec le volume

11.3 · Protection des données

SujetÉtatDétail
Résidence des donnéesCanadaSupabase, région ca-central-1. Pertinent pour une clientèle québécoise et pour la loi 25.
Droit à l'effacementImplémentéDans l'app, immédiat, serveur puis local. Page publique /data-deletion.
MinimisationStructurelleLa position n'est pas persistée ; le domicile ne quitte pas l'appareil ; la position d'annonce est floutée en base.
Pisteurs tiersAucunAucune dépendance d'analytique ou de publicité dans package.json.
Sous-traitantsQuatre, nommésSupabase (base, temps réel, stockage) · Mapbox (tuiles, recherche, itinéraires) · Open-Meteo (météo, sans clé, sans compte) · Resend (courriel transactionnel).
Portabilité des donnéesNon implémentéeAucun export de compte. Exigible par le RGPD si une diffusion européenne est envisagée. Voir R-8.
Registre de traitementPartielL'inventaire de la section 10.3 en tient lieu ; il n'existe pas de document formel séparé.
MineursNon traitéAucune barrière d'âge, aucune classification déclarée. À trancher avant soumission — les deux boutiques posent la question.

11.4 · État de la vérification du code

Le constat le plus important de cette section

Il n'existe aucune suite de tests automatisés dans le dépôt. Pas de test unitaire, pas de test d'intégration, pas d'exécution en intégration continue. Ce qui a été vérifié l'a été par des exercices navigateur ponctuels et par du SQL en production sur lignes jetables — des campagnes, pas un filet.

C'est la première chose qu'un acheteur technique regarde, et c'est la dette la plus coûteuse à rattraper après un transfert : sans filet, chaque modification faite par une équipe qui n'a pas écrit le code se juge à l'œil.

ContrôleÉtatNote
TypeScript strictActiftsconfig.json. La vérification de types est le principal filet réel du projet.
ESLintConfiguréeslint.config.js
PrettierConfiguré.prettierrc
Tests unitairesAucun—
Tests d'intégrationAucun—
Intégration continueAucunePas de workflow de vérification au dépôt
Journalisation des erreursLocaleerror-capture.ts, error-page.ts — capture la vue en défaut, l'état de route, la trace GPS récente et la pile, pour rejouer en développement. Aucun service d'agrégation externe.
Documentation interneExceptionnelleLe dépôt commente le pourquoi des invariants, souvent sur vingt lignes, avec la panne qui les a causés. C'est ce qui rend ce dossier possible et ce qui atténue partiellement l'absence de tests au transfert.

11.5 · Concentration du code

Deux fichiers concentrent une part importante du produit : NavExperience.tsx (4 648 lignes) et MediaCenter.tsx (2 449), soit 7 097 lignes — 14,3 % du code applicatif. En y ajoutant carte.tsx (2 679) et FleetJoinSheet.tsx (1 629), quatre fichiers font 22,9 % du produit.

Nuance importante — ce n'est pas à corriger

Le dépôt interdit de réécrire un composant de carte pour en changer une partie : les invariants de projection, d'ancre et d'état caméra ont coûté du vrai débogage, et une réécriture « à l'équivalent » les perd silencieusement — la perte se voyant sur un téléphone, dans le trafic, jamais au vérificateur de types. Consigné comme charge de maintenance, pas comme correction à planifier. Voir R-9.

12 Chaînes de données critiques

C-1 · Un kilomètre, du capteur au compteur mondial

GPS (watchPosition)
 └─ filtre de déplacement réel > POSE_DEADBAND_M = 5 m
    └─ odoKm cumulé en mémoire du moteur de carte
       └─ télémétrie 1 Hz → bumpKm → stockage local si +0,05 km
          └─ effet de poussée anti-rebond 800 ms
             └─ RPC update_rider_km
                · refus si drapeau humain posé
                · jamais de recul
                · plafond : 300 km/h × min(écoulé, 24 h)
                · niveau = greatest(niveau, formule) jamais repris
                └─ fleet_scores.km
                   └─ get_community_km = somme
                      └─ tambours mécaniques, extrapolés, JAMAIS en arrière

Points de perte possibles, par conception : mode démo (position simulée, coupée avant bumpKm) · réseau absent (reste local, repart au prochain envoi) · drapeau humain posé (gel silencieux, désormais visible en console fondateur).

C-2 · Une position, du capteur à l'écran d'un pair — et ce qui ne s'écrit jamais

GPS
 └─ mode fantôme actif ?          → STOP, rien ne part
 └─ à moins de PARK_SAFE_M du domicile ? → STOP, rien ne part
    └─ canal wo-fleet-live, track() en PRÉSENCE
       └─ mémoire du serveur temps réel — aucune écriture en table
          └─ pairs abonnés : lissage PEER_POS_RATE = 6
             └─ marqueur DOM au-dessus du canvas
       └─ déconnexion → Supabase purge la clé
          └─ le pin disparaît. Il n'en reste aucune trace.

À l'arrêt, un SEUL chemin écrit :
 └─ fleet_parked, 1 ligne
    └─ si près du domicile : position décalée sur église / caserne / grille ~500 m
       └─ l'écran affiche « secteur », pas une fausse adresse

C-3 · Un accès privilégié, du bouton à la ligne

Bouton de back office
 └─ getToken() — jeton opaque 256 bits, localStorage
    └─ RPC(..., p_token)          aucune identité en argument
       └─ SECURITY DEFINER
          └─ _role_for_token(p_token)
             · session expirée ? → refus
             · rang insuffisant ? → refus
             └─ écriture sur table SCELLÉE
                (inaccessible en direct, quelle que soit la clé)

C'est le seul chemin d'écriture du produit qui prouve son appelant. Le généraliser aux écritures à guest_id est la recommandation R-2.

C-4 · Une passphrase, du champ au refus — et le trou

Champ passphrase
 └─ hashPass() = SHA-256, PAS DE SEL, PAS DE COÛT   ← profile.ts:236
    └─ p_hash envoyé au serveur
       └─ fleet_session_open_nick : pass_hash = p_hash
          · AUCUNE limitation de débit            ← trou F-02
          · grant execute … to anon
          └─ ok → gen_random_bytes(32) → jeton 256 bits, 90 j
          └─ échec → 'bad_credentials'

Deux propriétés de cette chaîne sont bonnes (jeton fort, table scellée) et deux sont mauvaises (condensat porteur non salé, aucun frein au bourrage). Elles vivent dans la même fonction. Voir F-02.

13 Constats classés par sévérité

F-02Le modèle d'authentification n'est pas résistantMajeure

Constat. La passphrase est réduite côté client à un SHA-256 sans sel ni fonction de dérivation, puis comparée telle quelle en base. Le condensat est un porteur. Aucune limitation de débit ne protège fleet_session_open ni fleet_session_open_nick, toutes deux exécutables par anon.

Portée du risque. Un bourrage d'identifiants est réalisable depuis l'extérieur sans aucun frein. Une fuite de fleet_accounts — actuellement protégée par le scellement — donnerait des condensats directement réutilisables, sans même avoir à les casser.

Remède. Par ordre de coût croissant : (1) limitation de débit par identifiant et par IP sur les deux fonctions d'ouverture, avec délai croissant ; (2) re-condensation côté serveur en crypt() / bcrypt avec sel, migration progressive au prochain succès de connexion ; (3) adoption de Supabase Auth, qui apporte au passage la vérification HaveIBeenPwned relevée en 11.1. C'est la première recommandation du dossier — voir R-1.

F-03L'identité n'est pas prouvée en écritureMajeure

Constat. Le guest_id est une chaîne locale, et il circule publiquement (roster de crew, liste de réacteurs, membres de convoi). Les fonctions bornent ce qu'un appelant peut faire par unité de temps mais ne prouvent pas la propriété de la ligne visée.

Atténuation en place. Plafonnement physique de l'odomètre · rang résolu au serveur pour toute modération et tout back office · tables sensibles scellées · plafonds de volume sur lieux, trajets et chansons.

Remède. Exiger le jeton de session sur toutes les écritures identitaires, comme c'est déjà le cas pour le back office. Le motif existe, il est éprouvé, il reste à l'étendre. Voir R-2.

État depuis la révision A : inchangé sur le fond, mais la surface a grandi — 33 RPC de plus, dont plusieurs écrivent à guest_id.

F-01Résidus d'un autre produit dans le schémaMineureRétrogradé

Constat révisé. 50 tables étrangères subsistent dans wizardops. Elles totalisent 116 lignes ; 19 sont vides ; 41 existent à l'identique dans le schéma chantier, où le produit vit désormais.

Ce qui a changé. À la révision A, ce constat était majeure et décrit comme le seul dont le rayon d'explosion dépassait Street Wizard. Le produit a depuis migré vers ses propres schémas. Ce qui reste est un résidu de migration.

Risque résiduel. Une politique RLS trop permissive sur l'une de ces 50 tables reste exposée à la clé publique de Street Wizard ; et un développeur qui écrit dans la mauvaise copie perd sa donnée en silence, ce qui est le mode de défaillance le plus coûteux à diagnostiquer.

Remède. Supprimer les 50 tables résiduelles après confirmation que chantier et public portent bien la donnée vivante. Opération de nettoyage, pas de refonte. Voir R-3.

F-04La carte dépend d'un CDN tiers, sans contrôle d'intégritéMineure

Mapbox GL JS v3.9.0 est injecté à l'exécution depuis api.mapbox.com, version épinglée par une constante du code et non par le lockfile, sans attribut integrity. Aucun poids dans le bundle (bénéfice réel), mais : indisponibilité de la carte si le CDN ne répond pas, et exécution de code arbitraire sur la page porteuse du jeton en cas de compromission du CDN. Remède : ajouter un contrôle d'intégrité de sous-ressource, ou héberger la bibliothèque. Voir R-6.

F-05Collisions de numéros de migrationMineureRétrogradé

Constat. Deux paires partagent un numéro : 0016_dump_tickets_and_undo + 0016_odometer_catchup, et 0017_session_open_gen_random_bytes + 0017_admin_frozen_riders. Deux numéros manquent : 0020 et 0058.

Portée. Sur une base neuve rejouée depuis les fichiers, l'ordre de deux migrations de même numéro est indéterminé. Ici les fonctions sont en create or replace et indépendantes, donc le rejeu resterait correct — mais la garantie tient au contenu, pas à la numérotation.

État. La discipline a tenu sur 50 migrations depuis : aucune nouvelle collision. Les paires historiques n'ont pas été renommées — renommer un fichier déjà appliqué provoquerait un rejeu chez quiconque suit le registre. Décision laissée au propriétaire. Le constat est rétrogradé parce qu'il est historique et clos en pratique.

D-1Le blocage filtre à la lecture, côté clientDette

Les lignes d'une personne bloquée arrivent au téléphone avant d'être écartées. Déclaré dans moderation.ts. Durcissement : filtrer dans les RPC de flux (fleet_wall_feed, dm_inbox, market_near). Voir R-5.

D-2Un jeu de couleurs clair mort dans la feuille de styleDette

:root porte une palette claire complète qu'aucun écran n'utilise, héritée du gabarit de départ. Elle a déjà causé un fond blanc en production, par un defaultTheme="light" qui réécrivait à l'hydratation la classe posée au SSR. Un jeu de couleurs mort est un piège pour le prochain développeur. Retirer, ou assumer un mode clair et le finir.

D-3Code serveur orphelin de features retiréesDette

Quatre fonctions des spots de la route (road_status_ttl, road_vote_radius_m, _road_dist_m, _road_named) survivent à des tables qui n'existent plus. Trois fonctions de notes (note_leave, notes_inbox, notes_mark_read) et la table fleet_notes ne sont appelées par aucun client. fleet_claim est remplacée par fleet_claim2 et n'est plus appelée. fleet_queue est scellée et vestigiale. Surface serveur inutile = surface à auditer sans contrepartie.

D-4Aucune suite de tests, aucune intégration continueDette majeure

Voir 11.4. C'est la dette la plus coûteuse à rattraper après un transfert, et la première question d'une vérification diligente technique. Elle est atténuée — pas compensée — par une documentation interne d'un niveau inhabituel.

D-5Emoji résiduels dans la proseCosmétique

La signalétique est entièrement dessinée. Il subsiste une quinzaine d'emoji dans du texte : descriptions de badges, quelques phrases d'interface, un pied de page. Décision de l'auteur — c'est sa voix, pas du langage visuel.

14 Tableau de certification

Chaque verdict est adossé à une preuve nommée et datée. Un verdict sans preuve n'apparaît pas dans ce tableau. Les verdicts marqués (rév. A) datent du 2026-08-21 et n'ont pas été revérifiés pour cette révision — voir M5.

IDSous-systèmeVerdictPreuve
S-01Navigation & carte 3DOK*Exercices logiques purs sur projection et cap (rév. A). *Non exercé sur canvas vivant — voir N-1.
S-02Flotte temps réelNon mesuréAucune mesure automatisée. Voir §15.
S-03Identité & sessionDéfaut connu4 états rendus, vérifiés en navigateur (rév. A). Mais F-02 est ouvert : le modèle de condensat n'est pas résistant.
S-04Odomètre & anti-tricheOK6 scénarios SQL en production sur ligne jetable, supprimée (rév. A).
S-05Progression & garageOKFormule recoupée par deux chemins (odomètre, niveau) (rév. A).
S-06Signalements routiersOKLivré et en service.
S-07ConvoisNon mesuréLivré ; aucun exercice. Multi-appareils jamais exercé — voir N-2.
S-08Commerces / PlacesPartielSubstitut en place ; URL et parseur à confirmer avec un vrai jeton.
S-09Petites annoncesOKLivré et en service.
S-10SocialOKLivré et en service. Migration 0070 atteste d'un défaut silencieux corrigé depuis.
S-11Média & radioNon mesuré4 390 lignes, aucune mesure. Le plus grand angle mort, et il a triplé. Voir N-3.
S-12ModérationLivréConstat d'usage. Limite déclarée : filtrage à la lecture (D-1).
S-13Console fondateur & back officeOK15 assertions, 4 états, garde-fou anti-faux-vert (rév. A). Les six panneaux ajoutés depuis ne sont pas couverts.
S-14Plateforme & PWAPartielPWA livrée et en service. Conteneur natif : Android construit, iOS absent, permissions à écrire.
S-15Lieux & trajetsLivréConstat d'usage.
S-16Son & voixNon mesuréAucune mesure automatisée.
S-17MonétisationLivréConstat d'usage. Aucun paiement encaissé à ce jour.
S-18Vitrine publiqueLivréConstat d'usage.

Relevé : 9 sous-systèmes OK (dont plusieurs sur constat d'usage), 4 partiels, 4 non mesurés, 1 avec défaut de sécurité connu.

Note de probité — deux choses à ne pas confondre

« Livré et en service » n'est pas une certification de test. C'est un constat d'usage. Il est écrit tel quel plutôt que promu en « OK testé ».

Et un verdict reporté d'une révision antérieure n'est pas un verdict revérifié. Les six verdicts marqués (rév. A) portent sur du code qui a grossi de 76 % depuis. Ils indiquent que quelque chose a été vérifié un jour, pas que le sous-système est vérifié aujourd'hui.

15 Ce qui n'a pas été mesuré

Cette section existe pour que le dossier ne mente pas par omission.

RéfAngle mortRaison
N-1La carte sur un canvas Mapbox vivantL'environnement de rédaction n'a aucun accès sortant sauf le registre npm et l'API Supabase : api.mapbox.com et les aperçus de déploiement répondent « aucune connexion ». Vérifié, pas supposé. Les épingles ont été exercées comme DOM hors carte ; le rendu au-dessus de tuiles réelles n'est pas mesuré.
N-2Le temps réel sous chargePrésence, convois, messagerie, mur et radio n'ont jamais été exercés à plusieurs appareils simultanés. Le comportement à 2 rouleurs est connu par l'usage ; à 50, il est inconnu. C'est l'angle mort le plus important pour un acquéreur, parce que c'est exactement la promesse du produit.
N-3Le sous-système média4 390 lignes, aucune mesure automatisée. Le plus grand angle mort recensé, et il a triplé depuis la révision A. La migration 0070 (likes_were_failing_silently) est la preuve que ce sous-système produit des défauts muets.
N-4Le parcours hors réseau de bout en boutLa résilience est conçue et vérifiée par lecture de code, mais aucun exercice ne coupe réellement le réseau au milieu d'un trajet.
N-5Deux corrections de conduiteRecalcul vers 120 m de marche arrière, et tenue de caméra en courbe après avoir levé le doigt du HUD : corrigés et raisonnés, attendent toujours une confirmation au volant.
N-6Les performances chiffréesAucun profil d'exécution, aucune mesure d'images par seconde, aucun poids de bundle par route, aucun octet par rouleur par minute, aucune latence fil-à-écran. Les affirmations de performance de ce dossier portent sur la structure, pas sur des temps mesurés.
N-7Le conteneur natif sur appareilLe dossier android/ existe ; aucune installation sur appareil réel n'a été observée depuis cet environnement. iOS n'est pas construit du tout.
N-8Le compte réel des fonctions exécutables par anonLe linter plafonne sa remontée à 250. Le compte réel est ≥ 250 et n'a pas été énuméré exhaustivement.
N-9Le coût d'exploitationAucune facture, aucun palier d'abonnement, aucune consommation Mapbox ni Supabase n'a été relevée pour ce dossier. Voir 17.4.

16 Recommandations

RéfPrioritéAction
R-11 — SécuritéTraiter F-02. Dans l'ordre : limitation de débit sur les deux fonctions d'ouverture de session ; sel et fonction de dérivation côté serveur avec migration au prochain succès ; ou adoption de Supabase Auth. Le seul constat du dossier qui expose des comptes réels.
R-21 — SécuritéÉtendre le jeton de session à toutes les écritures identitaires (F-03). Le motif existe et est éprouvé sur le back office.
R-32 — HygièneSupprimer les 50 tables résiduelles de l'autre produit dans wizardops, après confirmation que chantier porte la donnée vivante (F-01). Opération de nettoyage.
R-42 — HygièneFiger search_path sur les 14 fonctions wizardops relevées par le linter. Correction mécanique, une migration.
R-52 — Vie privéeFiltrer les personnes bloquées dans les RPC de flux, pas seulement à la lecture client (D-1).
R-62 — SécuritéContrôle d'intégrité de sous-ressource sur Mapbox GL, ou hébergement local de la bibliothèque (F-04).
R-73 — ProduitConfirmer l'endpoint Places avec un vrai jeton pour clore S-08.
R-83 — ConformitéImplémenter l'export de compte (portabilité) si une diffusion européenne est envisagée. Trancher la question des mineurs avant soumission aux boutiques.
R-91 — LivraisonPoser un premier filet de tests sur les chaînes critiques C-1 à C-4, avant tout autre travail de qualité. Ce ne sont que quatre chaînes, et ce sont celles dont une régression se voit en production plutôt qu'au vérificateur de types (D-4).
R-102 — LivraisonExercer le temps réel à plusieurs appareils (N-2) et couvrir le sous-système média (N-3) — les deux plus grands angles morts.
R-113 — DetteSupprimer le code serveur orphelin (D-3) et le jeu de couleurs clair mort (D-2).
R-123 — BoutiquesÉcrire les textes de justification des permissions iOS, construire le projet ios/, trancher le parcours de réinitialisation dans la coquille native.
R-13Ne pas faireNE PAS réécrire NavExperience.tsx (11.5). Si un découpage est un jour entrepris : mécanique, invariant par invariant, jamais « à l'équivalent ».

17 Dossier partenaire / acquéreur

Section nouvelle. Elle répond aux questions qu'un partenaire ou un acheteur pose en premier et auxquelles un dossier d'architecture ne répond pas.

17.1 · Ce qui est transférable

ActifNatureNote
Code applicatif49 538 lignes TypeScript / React, 152 fichiersPropriété entière de l'auteur. Aucune dépendance sous copyleft dans l'arbre de production.
Schéma et logique serveur93 tables, 203 fonctions, 71 migrations rejouablesLa logique métier est dans la base, pas dans un serveur d'application : elle se transfère avec le schéma.
Médias originaux202 images, 5 boucles audio, 34 MoSprites de véhicules, décors de showroom, glyphes, illustrations. Créés pour le produit.
Système de designJetons, glyphes (975 lignes de tracés), règles §9Source unique servant React et le DOM Mapbox.
DocumentationCe dossier · commentaires de code · supabase/migrations/README.mdLe dépôt documente le pourquoi de ses invariants et les pannes qui les ont causés. C'est l'actif le plus sous-estimé du lot.
Marque et domainesStreet Wizard · streetwizard.net · manifesto.streetwizard.netÀ énumérer au contrat.
Identifiant d'applicationnet.streetwizard.appRéservé, non publié.

17.2 · Ce qui n'est pas transférable tel quel

  • Les comptes de service. Supabase, Mapbox, Resend et l'hébergement sont au nom de l'auteur. Un transfert exige soit le transfert de propriété des projets, soit une migration — techniquement simple pour Mapbox et Resend (changement de clé), plus lourd pour Supabase (restauration de sauvegarde dans un nouveau projet, puis bascule de l'URL).
  • Le schéma partagé. Le projet Supabase cprop héberge aussi un autre produit (schémas chantier et public). Un transfert propre implique d'extraire wizardops dans son propre projet. Voir R-3 — le nettoyage des résidus est un préalable à cette extraction, pas une tâche d'hygiène facultative.
  • La base d'utilisateurs. Elle est vivante et ses positions ne sont pas persistées. Aucune donnée de localisation historique n'existe à transférer, par conception.

17.3 · Dépendances externes et risque fournisseur

FournisseurRôleSubstituable ?Effort
MapboxTuiles, GL JS, Search Box, DirectionsDifficileLe cœur du produit. Les invariants de caméra, de padding et de marqueurs DOM sont écrits contre l'API Mapbox. Une bascule vers MapLibre garderait le style de tuiles mais perdrait Search Box et Directions.
SupabasePostgres, RLS, Realtime, stockageMoyenPostgres et les RPC sont portables partout. Ce qui est spécifique : Realtime (présence et diffusion) et PostgREST. Une réimplémentation coûterait un service temps réel et une passerelle.
Open-MeteoMétéo pour l'ambiance sonoreFacileSans clé, sans compte, non critique. Un échec dégrade l'ambiance et rien d'autre.
ResendCourriel de réinitialisationFacileUn seul point d'appel, en fonction serveur.
CapacitorConteneur natifFacileEnveloppe, pas architecture. Le produit reste une app web.

17.4 · Coût d'exploitation

Non mesuré — et c'est délibérément écrit plutôt qu'estimé

Aucune facture, aucun palier d'abonnement, aucune consommation Mapbox ni Supabase n'a été relevée pour ce dossier (N-9). Ce qui est mesuré et qui informe le coût futur : la base Street Wizard pèse 3,8 Mo pour 43 tables, et la position n'est jamais persistée — le poste de coût qui explose habituellement dans une app de flotte (l'écriture de positions à haute fréquence) n'existe pas ici. Le coût dominant sera le trafic temps réel et les requêtes de tuiles, tous deux fonction du nombre de rouleurs simultanés, pas du nombre de comptes.

Un acquéreur doit exiger : les factures des douze derniers mois des quatre fournisseurs, et une mesure d'octets par rouleur par minute sur le canal de flotte. La seconde n'existe pas encore et devrait être instrumentée avant toute négociation qui s'appuie sur un coût unitaire.

17.5 · Risque personne-clé

Le produit a un seul architecte et un seul auteur. C'est un risque réel et il est nommé ici plutôt que découvert en vérification diligente. Ce qui l'atténue, concrètement :

  • Les invariants sont documentés dans le code, avec la panne qui les a causés — pas seulement le quoi, le pourquoi. C'est ce qui permet à un repreneur de comprendre qu'un padding.top n'est pas un détail.
  • Les migrations sont nommées d'après le constat qu'elles corrigent, ce qui rend l'historique du produit lisible sans son auteur.
  • Ce dossier, à sa deuxième révision, est écrit pour être lu par quelqu'un d'autre.

Ce qui ne l'atténue pas : l'absence de tests (D-4). Un repreneur héritera d'une base très documentée et sans filet. Les deux faits doivent être pesés ensemble.

17.6 · Liste de contrôle de vérification diligente

Ce qu'un acheteur technique devrait demander, dans l'ordre. Chaque ligne indique où la réponse se trouve déjà dans ce dossier.

  • 01Combien de code, réparti comment, et quelle part est générée ? → §1, M1. Le décompte exclut le fichier généré du routeur et le dit.
  • 02Où vit la logique métier ? → §4. Entièrement en PL/pgSQL, dans la même transaction que la donnée. Aucun serveur d'application, aucune fonction edge.
  • 03Quelle est la couverture RLS ? → §7. 93/93, et 21 tables scellées avec la liste et l'intention.
  • 04Quels secrets atteignent le client ? → §10.2. Aucun. La clé anonyme et le jeton Mapbox sont publics par conception, et le modèle ne repose pas sur leur secret.
  • 05Comment l'authentification tient-elle ? → §10.4, F-02. Elle ne tient pas. C'est écrit en clair, avec le chemin de remédiation chiffré en trois options.
  • 06Quelles données personnelles, où, combien de temps ? → §10.3, tableau complet. Et §11.3 pour la résidence canadienne et le droit à l'effacement.
  • 07Quelle est la couverture de tests ? → §11.4. Nulle. Écrit comme dette majeure D-4, pas enfoui.
  • 08Quelles dépendances sont irremplaçables ? → §17.3. Mapbox est difficile, le reste est substituable.
  • 09Qu'est-ce qui a été retiré du produit et pourquoi ? → §3.2. Quatre sous-systèmes, avec la migration qui les a fermés.
  • 10Qu'est-ce qui n'a jamais été vérifié ? → §15. Neuf angles morts nommés, dont le comportement à 50 rouleurs — la promesse même du produit.
  • 11Le produit peut-il être soumis aux boutiques aujourd'hui ? → §11.2. Android oui à trois éléments près ; iOS non, le projet natif n'existe pas.
  • 12Combien coûte l'exploitation ? → §17.4. Non mesuré, et la mesure à exiger est nommée.
Ce que ce dossier prétend, et ce qu'il ne prétend pas

Il prétend décrire exactement l'état du système au commit f7cbf82 et à la base du 2026-09-15, avec une méthode reproductible et ses trous nommés.

Il ne prétend pas que le système est testé, ni qu'il tient sous charge, ni qu'il coûte tel montant à exploiter, ni que son modèle d'authentification est solide. Ces quatre affirmations seraient fausses, et elles sont exactement celles qu'un dossier de vente aurait envie d'écrire.

18 Annexes — inventaires bruts

A-1 · Les 112 fonctions serveur appelées par le client

Mesure M2, deux passes. 19 nouvelles depuis la révision A n'apparaissaient pas à la première passe : elles transitent par les cinq helpers locaux qui masquent le nom.

can_moderate_global      convoy_create            convoy_get               convoy_halt
convoy_join              convoy_join_public       convoy_leave             convoy_mine
convoy_public_list       convoy_set_dest          convoy_set_visibility    cred_mine
cred_tallies             cred_vote                dm_inbox                 dm_mark_read
dm_send                  dm_thread                fleet_account_delete     fleet_admin_server
fleet_admin_stats        fleet_block              fleet_blocked_ids        fleet_claim2
fleet_community_songs    fleet_favorites          fleet_live_queue         fleet_login
fleet_login_nick         fleet_meet_delete        fleet_meet_rsvp_clear    fleet_meet_rsvp_read
fleet_meet_rsvp_set      fleet_meet_update        fleet_nick_status        fleet_org_delete
fleet_org_join           fleet_org_leave          fleet_org_members_list   fleet_org_set_role
fleet_org_stats          fleet_org_update         fleet_owner_song_stats   fleet_profile_card
fleet_profile_save       fleet_report             fleet_reset_confirm      fleet_reset_set
fleet_role_of            fleet_social_counts      fleet_song_state         fleet_unblock
fleet_wall_comment       fleet_wall_comment_delete fleet_wall_comment_pin  fleet_wall_comments
fleet_wall_delete        fleet_wall_feed          fleet_wall_following     fleet_wall_pin
fleet_wall_post          fleet_wall_react         fleet_wall_reactors      fleet_whoami
follow_set               get_community_km         market_get               market_list
market_near              market_post              notifs_list              notifs_mark_read
notifs_unread            org_claim_mine           org_flags_set            org_ride_add
org_ride_claim           org_ride_colors          org_ride_remove          org_rides_list
orgs_admin               orgs_brief               promo_code_delete        promo_code_save
promo_codes_admin        promo_peek               promo_redeem             radio_station_delete
radio_station_save       radio_stations_admin     radio_stations_list      report_add
report_remove            report_vote              reports_near             ride_catalog_admin
ride_catalog_list        ride_delete              ride_grant               ride_grant_revoke
ride_grants_admin        ride_grants_mine         ride_save                rider_xp
riders_search            song_delete              song_detail              song_edit
song_played              song_reorder             song_set_flags           update_rider_km

A-2 · Tables adressées en accès direct (15)

company_settings   fleet_chat         fleet_meet_media   fleet_meet_rsvp
fleet_meets        fleet_org_members  fleet_orgs         fleet_parked
fleet_playlist     fleet_scores       fleet_song_likes   fleet_songs
live_positions     radio_now_playing  vehicles

Correction de la révision A : elle en listait 16, dont fleet_queue — scellée depuis la migration 0064. Un seul seau de stockage est adressé : fleet-tracks.

A-3 · Les 21 tables scellées

app_secrets            fleet_abuse_reports    fleet_accounts         fleet_admins
fleet_blocks           fleet_convoy_members   fleet_convoys          fleet_dm
fleet_notifs           fleet_queue            fleet_roles            fleet_sessions
org_rides              promo_code_rides       promo_codes            promo_redemptions
radio_stations         ride_catalog           ride_grants            trip_history
user_places

A-4 · Migrations versionnées (71)

0001_market_get                      0002_fleet_sessions_and_roles
0003_fleet_session_rpcs              0004_street_cred_votes
0005_notes_and_follows_rpcs          0006_close_notes_and_follows
0007_notifications                   0008_roles_own_moderation
0009_song_edit_rpcs                  0010_close_songs
0011_admin_stats_telemetry           0012_dump_ops
0013_dump_run_close                  0014_dump_live_ping
0015_road_spots                      0016_dump_tickets_and_undo      [collision]
0016_odometer_catchup  [collision]  0017_session_open_gen_random_bytes [collision]
0017_admin_frozen_riders [coll.]   0018_admin_server_telemetry
0019_reports_author_and_staff_remove 0021_admin_frozen_riders
0022_radio_stations                  0023_ride_catalog
0024_ride_grants                     0025_promo_codes
0026_ride_worlds                     0027_org_stats
0028_ride_drivers                    0029_ride_variants
0030_riders_search                   0031_org_exclusives
0032_song_plays                      0033_vip_and_xp
0034_crew_mode_and_claim             0035_crew_op_kind
0036_claim_color_variant             0037_account_nickname_wins
0038_meet_rsvp_read                  0039_user_places
0040_unpoison_rider_names            0041_trip_history
0042_profile_save_rpc                0043_create_crew_with_mode
0044_crew_roster_showcase            0045_worlds_and_kenworth
0046_no_name_is_not_a_name           0047_starter_rides_volvo_gti_civic_si
0048_retire_dump_rust                0049_wall_reactions
0050_shop_notes                      0051_post_specs
0052_chat_log                        0053_convoy_halt
0054_convoy_visibility               0055_report_extinction
0056_haul_roster_cleanup             0057_solo_haul_runs
0059_reactions_like_love             0060_reactions_accept_both_during_rollout
0061_reactions_narrow_after_rollout  0062_civic_ef_is_not_a_starter
0063_wall_reactors                   0064_close_fleet_queue
0065_close_wiper_notes               0066_account_delete
0067_block_and_report                0068_ride_names_out_of_trademarks
0069_song_artwork_and_notes          0070_likes_were_failing_silently
0071_song_detail                     0072_indexes_for_the_hot_polls

Numéros absents : 0020, 0058.

A-5 · Canaux temps réel (6 familles)

CanalModeSujet
wo-fleet-livePrésencePosition et identité des rouleurs en ligne. Clé = guest_id.
wo-reportsDiffusionSignalements routiers Nouveau
wo-convoy-<id>Présence + diffusionMembres en ligne et destination partagée Nouveau
wo-wall-<seq>DiffusionMur d'un crew
wo-radio-<prov>DiffusionRadio de province
dm:<guest>DiffusionMessages privés

Le canal chantier:<org> de la révision A n'existe plus — voir §3.2. Règle : un sujet = un canal. Le canal de présence porte déjà le multijoueur et a cassé une fois pour avoir servi à deux choses.

A-6 · Dépendances de production

PaquetVersionRôle
react · react-dom19.2Rendu
@tanstack/react-router1.170Routage
@tanstack/react-start1.168SSR / Nitro / fonctions serveur
@tanstack/react-query5.101Cache de requêtes
@supabase/supabase-js2.108PostgREST, Realtime, stockage
tailwindcss · @tailwindcss/vite4.2Styles
@capacitor/core · @capacitor/android8.5.1Conteneur natif Nouveau
@tiptap/*3.27Éditeur de texte riche Nouveau
lucide-react0.575Icônes d'interface (la signalétique de carte est maison)
resend4.8Courriel transactionnel Nouveau
zod3.24Validation
lenis · sonner · next-themes · clsx · tailwind-merge · tw-animate-css · @radix-ui/react-slider—Défilement, notifications, thème, utilitaires
Mapbox GL JSv3.9.0Non installée par npm — injectée depuis le CDN à l'exécution. Voir F-04.

Aucune dépendance d'analytique, de publicité ni de pistage. Aucune dépendance sous copyleft dans l'arbre de production.