STREET WIZARD
Dossier d'architecture, de design, de sécurité et d'audit — tous sous-systèmes
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
| Répartition du code | Lignes | Unités | Rév. A |
|---|---|---|---|
Moteurs — src/lib | 13 757 | 66 modules | 8 709 / 56 |
Composants — src/components | 30 749 | 75 composants | 17 147 / 77 |
Routes — src/routes | 4 937 | 7 routes | 2 234 / 3 |
| Total applicatif | 49 538 | 152 fichiers | 28 090 / 136 |
| Migrations SQL versionnées | — | 71 fichiers | 21 |
Médias livrés — public/ | 34 Mo | 202 images · 5 boucles audio | non 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éf | Famille | Source de la mesure |
|---|---|---|
| M1 | Volume de code | find 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. |
| M2 | Surface d'appel client | Extraction 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. |
| M3 | Surface serveur | Requêtes sur pg_class, pg_proc, pg_policies, pg_indexes, pg_trigger, pg_namespace, sur la base de production. |
| M4 | Constantes | Lecture 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. |
| M5 | Comportement | SQL 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. |
| M6 | Avis automatisés | Linter 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. |
| M7 | Poids des médias | du -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èces | Ce que ça ouvre |
|---|---|---|
| Nouveau Conteneur natif | Capacitor 8.5.1 · capacitor.config.ts · dossier android/ · identifiant net.streetwizard.app | App 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 Convois | convoy.ts 362 · ConvoySheet.tsx 647 · canal wo-convoy-<id> · 11 RPC | Rouler 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ération | moderation.ts 165 · fleet_blocks · fleet_abuse_reports · migration 0067 | Bloquer, 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 compte | account.ts 83 · fleet_account_delete · migration 0066 · route /data-deletion | Exigence 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 office | 6 panneaux : rides 638 · promo 468 · radio 429 · grants 414 · serveur 360 · orgs 333 · ride-admin, radio-admin, orgs-admin, ride-grants | Distribution 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 promo | promo.ts 190 · PromoSheet.tsx 238 · 3 tables · migration 0025 | Deux temps séparés à dessein : regarder puis échanger. Un choix irréversible ne se fait pas les yeux fermés. |
| Nouveau XP sociale et VIP | xp.ts 52 · rider_xp, xp_is_vip, xp_post_rules · migration 0033 | Deuxiè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 vocal | voice.ts 149 | Remis 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 Travel | user-places.ts 115 · trip-history.ts 97 · home.ts 113 · QuickTravel.tsx 300 · migrations 0039, 0041 | Favoris 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 Stationnement | parking.ts 139 · table fleet_parked | Le 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 publique | Route /org/$slug 1 546 · presence.ts 140 · directory.ts 143 | Page 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 chanson | id3.ts 387 · AlbumDetailModal.tsx 367 · song_detail · migrations 0069, 0071 | Le 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 partageables | share-card.ts 198 · meet-card.ts 216 | Rendu <canvas> 1200×630 côté client, export JPEG, partage natif. Zéro rendu serveur, zéro dépendance. |
| Nouveau Ambiance météo | weather-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 171 | Prérequis de soumission des deux boutiques et base de toute discussion de conformité. Voir section 11. |
| Nouveau Premier lancement | GlobeIntro.tsx 652 · SignupGate.tsx 452 · FirstRunTips.tsx 308 · demo-tour.ts 56 · onboarding.ts 72 | Séquence d'entrée cinématique, porte d'inscription, conseils au premier lancement, tour de démonstration. |
| Nouveau Éditeur riche | TipTap 3.27 · RichEditor.tsx 192 · richtext.ts 71 · migration 0051 | Posts 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é | Était | Fermé par | Ce qui reste |
|---|---|---|---|
| S-12 Chantier / benne | Benne, voyages, équipes, géorepérage, 9 RPC, 3 tables scellées, canal chantier:<org>, 1 238 lignes | 0048_retire_dump_rust, puis 0057_solo_haul_runs | Rien 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 route | Spots communautaires, vote de proximité 200 m, statut à 2 h, enterrement à −3, 660 lignes | Tables road_spots, road_spot_votes, road_spot_status absentes de la base | Quatre 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 meet | File collaborative avec vote d'ordre, uploadTrack, fetchQueue, voteTrack, table fleet_queue en accès direct | 0064_close_fleet_queue | Le 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-glace | Mot laissé sur un pare-brise — note_leave, notes_inbox, notes_mark_read | 0065_close_wiper_notes | Table 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ément | Rév. A | Rév. B | Lecture |
|---|---|---|---|
| Lignes applicatives | 28 090 | 49 538 | +76 % en 25 jours de calendrier |
Tables wizardops | 88 | 93 | +5 net, après retrait de 6 tables (dump, spots) et ajout de 11 |
| Fonctions serveur | 160 | 203 | +43 |
Part SECURITY DEFINER | 91,9 % | 92,6 % | 188 / 203 |
| Tables scellées | 15 | 21 | +6 — le motif s'étend |
| Politiques RLS | 112 | 109 | −3, cohérent avec 6 tables passées en scellé (zéro politique) |
| Index | 161 | 180 | +19, dont le lot 0072_indexes_for_the_hot_polls |
| Tables étrangères au schéma | 54 | 50 | Et surtout : 116 lignes au total, 19 tables vides, 41 dupliquées ailleurs |
| Accès direct en table | 16 | 15 | fleet_queue scellée ; réduction de surface |
COCKPIT_PITCH | 66 | 70 | Plus 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
- PostgreSQL 17.6.1 / Supabase · projet
cprop· région ca-central-1 · schémawizardops - 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.
supabase-js2.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)
- 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.
- 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.
| ID | Sous-système | Modules principaux | Lignes | État |
|---|---|---|---|---|
| S-01 | Navigation & carte 3D | mapbox, NavExperience, camera, route-track, nav-view, cluster | 6 663 | OK* |
| S-02 | Flotte temps réel & présence | fleet, chat, presence, parking | 1 180 | Non mesuré |
| S-03 | Identité, session, rangs | session, profile, account, fleet-reset | 858 | Constat F-02 |
| S-04 | Odomètre & anti-triche | community, profile, update_rider_km | ~290 | OK |
| S-05 | Progression, garage, véhicules | garage, GarageView, ride-catalog, ride-grants, spots | 2 616 | OK |
| S-06 | Signalements routiers | reports, ReportGlyph, ReportDial | 844 | OK |
| S-07 | Convois | convoy, ConvoySheet | 1 009 | Non mesuré |
| S-08 | Commerces (POI / Places) | places, PoiGlyph, PlaceSheet | 811 | Partiel |
| S-09 | Petites annonces | market, MarketSheet, MarketComposer, MarketCard, MarketGlyph | 2 111 | OK |
| S-10 | Social — crews, murs, privé, réputation | orgs, wall, dm, social, notifs, street-cred, xp, riders, leaderboard | 2 617 | OK |
| S-11 | Média — radio, playlist, ID3 | radio, playlist, playlist-player, id3, provinceRadio, MediaCenter | 4 390 | Non mesuré |
| S-12 | Modération & sécurité communautaire | moderation | 165 | Livré |
| S-13 | Console fondateur & back office | admin-stats, ride-admin, radio-admin, orgs-admin, 6 panneaux | 3 480 | OK |
| S-14 | Plateforme, PWA, conteneur natif | streetwizard-pwa, wake-lock, graphics, nav-layers, Capacitor | 415 | Livré |
| S-15 | Lieux, trajets, Quick Travel | user-places, trip-history, home, QuickTravel | 625 | Livré |
| S-16 | Son, voix, ambiance | sfx, voice, weather-sfx, media-fx, juice | 708 | Non mesuré |
| S-17 | Monétisation — promo, grants, VIP | promo, ride-grants, PromoSheet, AdminPromoPanel, AdminGrantsPanel | 1 505 | Livré |
| S-18 | Vitrine publique & partage | Route /org/$slug, directory, share-card, meet-card, pages légales | 2 522 | Livré |
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
- 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.ts756 ·NavExperience.tsx4 648 ·camera.ts251 ·route-track.ts171 ·nav-view.ts68 ·cluster.ts69 ·Speedometer.tsx551 ·HighwaySignBoard.tsx160
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ùclickse déclenche, la carte a déjà bougé. Tout contrôle dans un marqueur interceptepointerdown+mousedown+touchstart(non passif) etclick. - 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 danscamera.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).
- Canal
wo-fleet-live, mode présence, clé =guest_idde l'appareil- Pièces
fleet.ts754 ·chat.ts147 ·presence.ts140 ·parking.ts139
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.
- Pièces
session.ts253 ·profile.ts456 ·account.ts83 ·fleet-reset.ts66- 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.
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é.
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.
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.
- 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.
« 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.
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.
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.
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é.
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.
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.
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.
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.
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 :
- 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.
- 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.
- Elle a une priorité. Un virage passe devant une alerte routière ; une alerte ne coupe jamais une consigne de manœuvre.
- Elle parle la langue de l'app (fr/en), pas un
fr-CAcodé 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.
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.
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
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ée | Ce qu'elle protège | Nature |
|---|---|---|
app_secrets | Secrets applicatifs | Secret |
fleet_sessions | Jetons de session (256 bits, 90 j) | Identité |
fleet_accounts | Comptes réclamés, condensats de passphrase | Identité |
fleet_roles · fleet_admins | Rangs et administration | Identité |
fleet_dm · fleet_notifs | Contenu privé | Vie privée |
fleet_blocks · fleet_abuse_reports | Blocages et signalements Nouveau | Vie privée |
fleet_convoys · fleet_convoy_members | Convois Nouveau | Vie privée |
trip_history · user_places | Trajets et lieux d'une personne Nouveau | Vie privée |
promo_codes · promo_code_rides · promo_redemptions | Codes et échanges Nouveau | Valeur |
ride_catalog · ride_grants · org_rides | Catalogue et attributions Nouveau | Valeur |
radio_stations | Grille de diffusion Nouveau | Valeur |
fleet_queue | Ancienne file du meet, fermée par 0064 | Vestige 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
| Constante | Valeur | Raison d'être |
|---|---|---|
| COCKPIT_PITCH | 70 | Inclinaison en cockpit — 66 à la rév. A, relevée pour rendre l'horizon |
| COCKPIT_PITCH_MIN / _MAX | 45 / 80 | Le doigt ne peut mettre la caméra ni sous le sol ni à l'horizontale Nouveau |
| WIDE_PITCH / WIDE_ZOOM | 62 / 16,2 | Vue large non-cockpit (démo, présentation) Nouveau |
| COCKPIT_ZOOM_STILL / _FAST | 18,2 / 16,8 | Le zoom recule à ≥ 90 km/h |
| BROWSE_ZOOM_HI / _LO | 16,5 / 12 | Seuils de bascule entre vue de conduite et carte à plat Nouveau |
| VEHICLE_ANCHOR_TOP | 0,34 | Ancre haute — jamais « bas ». Inversion déjà commise une fois |
| MOVE_ROT_KMH | 3 | Sous cette vitesse, pas de rotation |
| STOP_KMH | 2,5 | Seuil « arrêté » Nouveau |
| POS_RATE / POS_RATE_IDLE | 3,5 / 0,6 | Gain doux à la sortie du bruit GPS (ease-in, anti-à-coup) |
| PEER_POS_RATE | 6 | Les pairs se rattrapent plus vite que soi Nouveau |
| BEARING_RATE / ZOOM_RATE | 3,0 / 2,0 | Cap, par l'arc le plus court ; zoom |
| SNAP_METERS | 250 | Au-delà : saut, pas de glissement (1er fix, téléportation GPS) |
| GPS_NOISE_MIN_M / MAX_M | 4 / 30 | On 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_BLEND | 1,4 / 4 / 1,2 / 0,45 | Dead-reckoning : horizon de prédiction, plancher, armement, lissage EMA |
| POSE_DEADBAND_M | 5 | Seuil de déplacement réel |
| SPRITE_BOX_PX / PEER_BOX_PX | 118 / 96 | Boîte du sprite véhicule et du pin de pair Nouveau |
Trajet, odomètre, signalements, vie privée
| Constante | Valeur | Raison d'être |
|---|---|---|
| WRONGWAY_M | 120 m | Recul cumulé avant recalcul — l'écart latéral reste à 0 pendant un demi-tour |
| WRONGWAY_MIN_KMH | 15 km/h | Plancher de vitesse |
| HEADING_MIN_KMH / _M | 8 / 8 | Anti-girouette à l'arrêt |
| Plafond odomètre | 300 km/h | Plafond physique, pas légal |
| Fenêtre max | 24 h | 7 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 annonce | 400 m – 1,2 km | Appliqué au stockage, pas à l'affichage |
| PARK_SAFE_M | seuil « près de la maison » | En dessous : décalage sur église / caserne / grille ~500 m |
| Expiration de session | 90 jours | Purge des expirées à chaque ouverture |
| Jeton de session | 256 bits | gen_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.
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ôle | Police | Emploi |
|---|---|---|
--font-display | Space Grotesk 500–800 | Titres, chiffres de tableau de bord, compteur de vitesse |
--font-sans | Chivo 400–900 | Corps de texte, interface |
--font-terminal | VT323 | Fiches 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.
| Forme | Sujet | Ce qu'elle dit |
|---|---|---|
| Hexagone pointu, coloré | Alerte routière | Urgent |
| Hexagone pointu, en os | Service | Calme, infrastructure |
| Pastille ronde | Commerce | Commodité ambiante |
| Pastille de prix | Annonce | Invitation |
| Écusson (image) | Police | Le seul objet déjà connu de tous |
| Sprite de véhicule, vue arrière | Soi et les pairs | En 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 enobject-contain— eninset-0elle 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 :
- 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.
- 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.
- 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
| Acteur | Ce qu'il peut | Ce qui l'arrête | Résiduel |
|---|---|---|---|
| Client modifié (console navigateur, app décompilée) | Appeler n'importe quelle RPC, mentir sur son rang local, forger un guest_id | Le 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ètre | Injecter des kilomètres pour monter de niveau | Plafond physique 300 km/h × min(écoulé, 24 h) ; recul ignoré ; refus muet ; niveau recalculé et jamais repris à la baisse | Faible — mesuré en production sur ligne jetable |
| Harceleur | Suivre une personne, la contacter, la localiser | Blocage 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'identifiants | Deviner ou rejouer une passphrase | Tables fleet_accounts et fleet_sessions scellées ; jeton de 256 bits | SHA-256 non salé, pas de limitation de débit — F-02 |
| Fuite de la clé anonyme | Rien qu'un utilisateur normal ne puisse déjà faire | Par 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 service | Tout | Elle n'est jamais livrée au client. Aucune occurrence dans le bundle | Nul côté client ; dépend de l'hygiène du tableau de bord d'hébergement |
| XSS sur la page | Lire le jeton en localStorage | React échappe par défaut ; le texte riche passe par TipTap | Pas de cookie httpOnly — déclaré dans session.ts |
| Indisponibilité du CDN Mapbox | Rendre la carte inutilisable | Rien | Pas 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ée | Où elle vit | Durée | Sortie possible |
|---|---|---|---|
| Position en direct | Mémoire du serveur temps réel (présence) | Le temps de la session | Diffusée aux pairs, sauf mode fantôme |
| Position de stationnement | fleet_parked, 1 ligne | Jusqu'au retour en live | Publique, décalée si près du domicile |
| Domicile | localStorage seulement | Jusqu'à effacement | Aucune |
| Historique de trajets | trip_history — scellée, plafonnée | Jusqu'à suppression | RPC propriétaire uniquement |
| Lieux enregistrés | user_places — scellée, plafonnée | Jusqu'à suppression | RPC propriétaire uniquement |
| Courriel | fleet_accounts — scellée | Jusqu'à suppression du compte | Réinitialisation via Resend |
| Condensat de passphrase | fleet_accounts — scellée | Jusqu'à suppression | Aucune — mais voir F-02 |
| Messages privés | fleet_dm — scellée | Jusqu'à suppression | RPC des deux correspondants |
| Kilométrage, niveau, pseudo, véhicule | fleet_scores — accès direct en lecture | Permanent | Public — c'est le classement |
| Musique déposée | Seau fleet-tracks | Jusqu'à suppression | Publique par URL |
| Annonces | fleet_market | Jusqu'à retrait | Publique, 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é.
| Niveau | Avis | Occurrences | Lecture |
|---|---|---|---|
| INFO | rls_enabled_no_policy | 21 | Attendu — 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. |
| WARN | function_search_path_mutable | 18 · 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. |
| WARN | anon_security_definer_function_executable | 250 remontées (plafond) · 181 dans wizardops | Attendu, 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. |
| WARN | authenticated_security_definer_function_executable | 250 · idem | Même cause, même lecture. |
| WARN | extension_in_public | 1 | pg_net installée dans public. Non utilisée par Street Wizard. À déplacer par hygiène. |
| WARN | auth_leaked_password_protection | 1 | La 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.
| Exigence | Règle | État | Preuve |
|---|---|---|---|
| Suppression de compte dans l'app | App Store 5.1.1(v) · Google Play | Conforme | account.ts, fleet_account_delete, migration 0066, route /data-deletion |
| Signalement de contenu | App Store 1.2 | Conforme | fleet_report, fleet_abuse_reports, migration 0067 |
| Blocage d'utilisateur | App Store 1.2 | Conforme | fleet_block / fleet_unblock, fleet_blocks |
| Moyen de joindre l'éditeur | App Store 1.2 | Conforme | moderation.ts |
| Politique de confidentialité accessible | Les deux | Conforme | Route /privacy |
| Conditions d'utilisation | Les deux | Conforme | Route /terms |
| Pas un simple site emballé | App Store 4.2 | Conforme | Coquille pré-rendue embarquée, démarrage hors réseau ; server.url délibérément non utilisé |
| Marques déposées de véhicules | App Store 5.2 | Traité | Migration 0068_ride_names_out_of_trademarks |
| Textes de justification des permissions iOS | App Store 5.1.1 | À faire | Déclaré dans capacitor.config.ts — position en arrière-plan, audio en veille |
| Projet natif iOS | Prérequis de soumission | À faire | Dossier ios/ absent — exige macOS et CocoaPods |
| Réinitialisation dans la coquille native | Fonctionnel | À trancher | La 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éventif | App Store 1.2 | Partiel | Signalement 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 | État | Détail |
|---|---|---|
| Résidence des données | Canada | Supabase, région ca-central-1. Pertinent pour une clientèle québécoise et pour la loi 25. |
| Droit à l'effacement | Implémenté | Dans l'app, immédiat, serveur puis local. Page publique /data-deletion. |
| Minimisation | Structurelle | La position n'est pas persistée ; le domicile ne quitte pas l'appareil ; la position d'annonce est floutée en base. |
| Pisteurs tiers | Aucun | Aucune dépendance d'analytique ou de publicité dans package.json. |
| Sous-traitants | Quatre, nommés | Supabase (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ées | Non implémentée | Aucun export de compte. Exigible par le RGPD si une diffusion européenne est envisagée. Voir R-8. |
| Registre de traitement | Partiel | L'inventaire de la section 10.3 en tient lieu ; il n'existe pas de document formel séparé. |
| Mineurs | Non 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 | État | Note |
|---|---|---|
| TypeScript strict | Actif | tsconfig.json. La vérification de types est le principal filet réel du projet. |
| ESLint | Configuré | eslint.config.js |
| Prettier | Configuré | .prettierrc |
| Tests unitaires | Aucun | — |
| Tests d'intégration | Aucun | — |
| Intégration continue | Aucune | Pas de workflow de vérification au dépôt |
| Journalisation des erreurs | Locale | error-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 interne | Exceptionnelle | Le 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èrePoints 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 adresseC-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é
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.
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.
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.
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.
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.
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.
: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.
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.
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.
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.
| ID | Sous-système | Verdict | Preuve |
|---|---|---|---|
| S-01 | Navigation & carte 3D | OK* | Exercices logiques purs sur projection et cap (rév. A). *Non exercé sur canvas vivant — voir N-1. |
| S-02 | Flotte temps réel | Non mesuré | Aucune mesure automatisée. Voir §15. |
| S-03 | Identité & session | Défaut connu | 4 é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-04 | Odomètre & anti-triche | OK | 6 scénarios SQL en production sur ligne jetable, supprimée (rév. A). |
| S-05 | Progression & garage | OK | Formule recoupée par deux chemins (odomètre, niveau) (rév. A). |
| S-06 | Signalements routiers | OK | Livré et en service. |
| S-07 | Convois | Non mesuré | Livré ; aucun exercice. Multi-appareils jamais exercé — voir N-2. |
| S-08 | Commerces / Places | Partiel | Substitut en place ; URL et parseur à confirmer avec un vrai jeton. |
| S-09 | Petites annonces | OK | Livré et en service. |
| S-10 | Social | OK | Livré et en service. Migration 0070 atteste d'un défaut silencieux corrigé depuis. |
| S-11 | Média & radio | Non mesuré | 4 390 lignes, aucune mesure. Le plus grand angle mort, et il a triplé. Voir N-3. |
| S-12 | Modération | Livré | Constat d'usage. Limite déclarée : filtrage à la lecture (D-1). |
| S-13 | Console fondateur & back office | OK | 15 assertions, 4 états, garde-fou anti-faux-vert (rév. A). Les six panneaux ajoutés depuis ne sont pas couverts. |
| S-14 | Plateforme & PWA | Partiel | PWA livrée et en service. Conteneur natif : Android construit, iOS absent, permissions à écrire. |
| S-15 | Lieux & trajets | Livré | Constat d'usage. |
| S-16 | Son & voix | Non mesuré | Aucune mesure automatisée. |
| S-17 | Monétisation | Livré | Constat d'usage. Aucun paiement encaissé à ce jour. |
| S-18 | Vitrine publique | Livré | 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éf | Angle mort | Raison |
|---|---|---|
| N-1 | La carte sur un canvas Mapbox vivant | L'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-2 | Le temps réel sous charge | Pré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-3 | Le sous-système média | 4 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-4 | Le parcours hors réseau de bout en bout | La 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-5 | Deux corrections de conduite | Recalcul 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-6 | Les performances chiffrées | Aucun 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-7 | Le conteneur natif sur appareil | Le 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-8 | Le compte réel des fonctions exécutables par anon | Le linter plafonne sa remontée à 250. Le compte réel est ≥ 250 et n'a pas été énuméré exhaustivement. |
| N-9 | Le coût d'exploitation | Aucune facture, aucun palier d'abonnement, aucune consommation Mapbox ni Supabase n'a été relevée pour ce dossier. Voir 17.4. |
16 Recommandations
| Réf | Priorité | Action |
|---|---|---|
| R-1 | 1 — 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-2 | 1 — Sécurité | Étendre le jeton de session à toutes les écritures identitaires (F-03). Le motif existe et est éprouvé sur le back office. |
| R-3 | 2 — Hygiène | Supprimer 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-4 | 2 — Hygiène | Figer search_path sur les 14 fonctions wizardops relevées par le linter. Correction mécanique, une migration. |
| R-5 | 2 — Vie privée | Filtrer les personnes bloquées dans les RPC de flux, pas seulement à la lecture client (D-1). |
| R-6 | 2 — Sécurité | Contrôle d'intégrité de sous-ressource sur Mapbox GL, ou hébergement local de la bibliothèque (F-04). |
| R-7 | 3 — Produit | Confirmer l'endpoint Places avec un vrai jeton pour clore S-08. |
| R-8 | 3 — 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-9 | 1 — Livraison | Poser 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-10 | 2 — Livraison | Exercer 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-11 | 3 — Dette | Supprimer le code serveur orphelin (D-3) et le jeu de couleurs clair mort (D-2). |
| R-12 | 3 — Boutiques | Écrire les textes de justification des permissions iOS, construire le projet ios/, trancher le parcours de réinitialisation dans la coquille native. |
| R-13 | Ne pas faire | NE 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
| Actif | Nature | Note |
|---|---|---|
| Code applicatif | 49 538 lignes TypeScript / React, 152 fichiers | Propriété entière de l'auteur. Aucune dépendance sous copyleft dans l'arbre de production. |
| Schéma et logique serveur | 93 tables, 203 fonctions, 71 migrations rejouables | La logique métier est dans la base, pas dans un serveur d'application : elle se transfère avec le schéma. |
| Médias originaux | 202 images, 5 boucles audio, 34 Mo | Sprites de véhicules, décors de showroom, glyphes, illustrations. Créés pour le produit. |
| Système de design | Jetons, glyphes (975 lignes de tracés), règles §9 | Source unique servant React et le DOM Mapbox. |
| Documentation | Ce dossier · commentaires de code · supabase/migrations/README.md | Le 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 domaines | Street Wizard · streetwizard.net · manifesto.streetwizard.net | À énumérer au contrat. |
| Identifiant d'application | net.streetwizard.app | Ré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
cprophéberge aussi un autre produit (schémaschantieretpublic). Un transfert propre implique d'extrairewizardopsdans 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
| Fournisseur | Rôle | Substituable ? | Effort |
|---|---|---|---|
| Mapbox | Tuiles, GL JS, Search Box, Directions | Difficile | Le 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. |
| Supabase | Postgres, RLS, Realtime, stockage | Moyen | Postgres 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-Meteo | Météo pour l'ambiance sonore | Facile | Sans clé, sans compte, non critique. Un échec dégrade l'ambiance et rien d'autre. |
| Resend | Courriel de réinitialisation | Facile | Un seul point d'appel, en fonction serveur. |
| Capacitor | Conteneur natif | Facile | Enveloppe, 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.topn'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)
| Canal | Mode | Sujet |
|---|---|---|
wo-fleet-live | Présence | Position et identité des rouleurs en ligne. Clé = guest_id. |
wo-reports | Diffusion | Signalements routiers Nouveau |
wo-convoy-<id> | Présence + diffusion | Membres en ligne et destination partagée Nouveau |
wo-wall-<seq> | Diffusion | Mur d'un crew |
wo-radio-<prov> | Diffusion | Radio de province |
dm:<guest> | Diffusion | Messages 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
| Paquet | Version | Rôle |
|---|---|---|
react · react-dom | 19.2 | Rendu |
@tanstack/react-router | 1.170 | Routage |
@tanstack/react-start | 1.168 | SSR / Nitro / fonctions serveur |
@tanstack/react-query | 5.101 | Cache de requêtes |
@supabase/supabase-js | 2.108 | PostgREST, Realtime, stockage |
tailwindcss · @tailwindcss/vite | 4.2 | Styles |
@capacitor/core · @capacitor/android | 8.5.1 | Conteneur natif Nouveau |
@tiptap/* | 3.27 | Éditeur de texte riche Nouveau |
lucide-react | 0.575 | Icônes d'interface (la signalétique de carte est maison) |
resend | 4.8 | Courriel transactionnel Nouveau |
zod | 3.24 | Validation |
lenis · sonner · next-themes · clsx · tailwind-merge · tw-animate-css · @radix-ui/react-slider | — | Défilement, notifications, thème, utilitaires |
| Mapbox GL JS | v3.9.0 | Non 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.