[{"data":1,"prerenderedAt":1287},["ShallowReactive",2],{"profil-header-en":3,"profil-footer-en":93,"i-lucide:menu":115,"i-simple-icons:github":120,"i-simple-icons:linkedin":122,"i-lucide:mail":125,"projet-courant-en-\u002Fprojets\u002Festimap":127,"projets-navigation-en":283,"i-lucide:arrow-right":1285},{"id":4,"accroche":5,"apropos":6,"competences":11,"cv":71,"email":72,"extension":73,"localisation":74,"meta":75,"nom":76,"reseaux":77,"stem":90,"titre":91,"__hash__":92},"profil_en\u002Fen\u002Fprofil.yml","I design and build web applications end to end — from the database to the pixel — and the AI tooling that plugs into them. Vue and Nuxt on the interface, Python and Node on the server, Docker and a VPS to ship it.",[7,8,9,10],"I'm a full-stack developer, and I build whole products rather than pieces of products: data model, API, interface, deployment and monitoring. That wide view is what interests me — understanding how a schema decision ripples all the way out to the user experience.","My personal projects tend to revolve around geographic data and real time: mapping wildfires from satellite feeds, merging public real-estate sources, tracking satellites in orbit. I like subjects where the raw data is thankless and the work consists of making it readable.","I work in Docker end to end, I host on my own servers, and I document what I build — because a project you can't pick back up six months later isn't really finished.","I didn't come to development through computing, but through objects: industrial product design first, then real-time 3D. That's where I picked up a taste for hard constraints and geometry, and the habit of starting from a brief rather than from a technology. Code showed up as the most direct way to automate whatever was slowing me down — and never left.",[12,21,30,39,47,55,62],{"categorie":13,"icon":14,"items":15},"Front-end","lucide:layout-dashboard",[16,17,18,19,20],"Vue 3","Nuxt","TypeScript","Tailwind CSS","MapLibre GL",{"categorie":22,"icon":23,"items":24},"Back-end","lucide:server",[25,26,27,28,29],"Python","FastAPI","Node.js","AdonisJS","PHP",{"categorie":31,"icon":32,"items":33},"Data","lucide:database",[34,35,36,37,38],"PostgreSQL","PostGIS","MongoDB","Redis","SQLite",{"categorie":40,"icon":41,"items":42},"AI & Data science","lucide:brain-circuit",[43,44,45,46],"TensorFlow","Keras","Vertex AI","pandas",{"categorie":48,"icon":49,"items":50},"Infrastructure","lucide:container",[51,52,53,54],"Docker","nginx","Caddy","GitHub Actions",{"categorie":56,"icon":57,"items":58},"Mobile","lucide:smartphone",[59,60,61],"SwiftUI","SwiftData","iOS",{"categorie":63,"icon":64,"items":65},"Method & product","lucide:git-branch",[66,67,68,69,70],"GitHub Flow","Testing","Documentation","Product Ownership","Agile","","rs.szmygiel@gmail.com","yml","Bordeaux, France",{},"Ronan Szmygiel",[78,82,86],{"nom":79,"url":80,"icon":81},"GitHub","https:\u002F\u002Fgithub.com\u002Fnanro22","simple-icons:github",{"nom":83,"url":84,"icon":85},"LinkedIn","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fszmygiel-ronan\u002F","simple-icons:linkedin",{"nom":87,"url":88,"icon":89},"Email","mailto:rs.szmygiel@gmail.com","lucide:mail","en\u002Fprofil","AI & Web Developer \u002F Product Owner","ZIzTObRSPg6iBvcjNxFpN9tVpN-w_PoqFkwZVJ9VFb8",{"id":4,"accroche":5,"apropos":94,"competences":95,"cv":71,"email":72,"extension":73,"localisation":74,"meta":110,"nom":76,"reseaux":111,"stem":90,"titre":91,"__hash__":92},[7,8,9,10],[96,98,100,102,104,106,108],{"categorie":13,"icon":14,"items":97},[16,17,18,19,20],{"categorie":22,"icon":23,"items":99},[25,26,27,28,29],{"categorie":31,"icon":32,"items":101},[34,35,36,37,38],{"categorie":40,"icon":41,"items":103},[43,44,45,46],{"categorie":48,"icon":49,"items":105},[51,52,53,54],{"categorie":56,"icon":57,"items":107},[59,60,61],{"categorie":63,"icon":64,"items":109},[66,67,68,69,70],{},[112,113,114],{"nom":79,"url":80,"icon":81},{"nom":83,"url":84,"icon":85},{"nom":87,"url":88,"icon":89},{"left":116,"top":116,"width":117,"height":117,"rotate":116,"vFlip":118,"hFlip":118,"body":119},0,24,false,"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 5h16M4 12h16M4 19h16\"\u002F>",{"left":116,"top":116,"width":117,"height":117,"rotate":116,"vFlip":118,"hFlip":118,"body":121},"\u003Cpath fill=\"currentColor\" d=\"M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\"\u002F>",{"left":116,"top":116,"width":117,"height":117,"rotate":116,"vFlip":118,"hFlip":118,"body":123,"hidden":124},"\u003Cpath fill=\"currentColor\" d=\"M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037c-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85c3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.06 2.06 0 0 1-2.063-2.065a2.064 2.064 0 1 1 2.063 2.065m1.782 13.019H3.555V9h3.564zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0z\"\u002F>",true,{"left":116,"top":116,"width":117,"height":117,"rotate":116,"vFlip":118,"hFlip":118,"body":126},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"m22 7l-8.991 5.727a2 2 0 0 1-2.009 0L2 7\"\u002F>\u003Crect width=\"20\" height=\"16\" x=\"2\" y=\"4\" rx=\"2\"\u002F>\u003C\u002Fg>",{"id":128,"title":129,"architecture":130,"body":142,"cover":252,"description":253,"extension":254,"featured":124,"gallery":255,"highlights":265,"interface":124,"meta":269,"navigation":124,"path":270,"rang":271,"repo":71,"role":272,"seo":273,"site":71,"status":274,"stem":275,"tags":276,"type":280,"year":281,"__hash__":282},"projets_en\u002Fprojets\u002Festimap.md","Estimap",[131,134,137,139],{"label":132,"description":133},"Frontend","Nuxt 3 + MapLibre GL (PMTiles) — map, property sheet, back office.",{"label":135,"description":136},"Backend","FastAPI + SQLAlchemy, Celery for long-running work, two separate queues (imports\u002Fmutations).",{"label":31,"description":138},"PostgreSQL \u002F PostGIS (~100M rows) for the spatial layer, Redis as broker and cache.",{"label":140,"description":141},"Infra","Docker Compose, workers split by queue.",{"type":143,"value":144,"toc":243},"minimark",[145,150,154,157,161,164,175,179,203,215,222,226,229,233,236,240],[146,147,149],"h2",{"id":148},"the-problem","The problem",[151,152,153],"p",{},"French real-estate data is public and free. It is also scattered: transactions\nsit in the DVF sales register, parcel geometry in the cadastre, building\ncharacteristics in the BDNB registry, energy performance in the DPE dataset,\ndemographics at INSEE, hazard exposure somewhere else again — and plenty more:\nlocal amenities, transport, fibre coverage, schools, building permits,\ncondominiums.",[151,155,156],{},"Each has its own format, its own granularity and its own pivot identifier.\nCross-referencing \"the price per square metre of houses built before 1975 with\nan F or G energy rating in a flood-prone area\" therefore takes a considerable\namount of upfront merging.",[146,158,160],{"id":159},"the-product-goal","The product goal",[151,162,163],{},"A property price estimation engine — the map is the exploration surface, but the\npoint is to feed a model with genuinely cross-referenced features.",[151,165,166,170,171,174],{},[167,168,169],"strong",{},"Phase 1"," (weighted kNN comparables, progressive radii, plausibility guards)\nis in production and answers 100% of estimation requests today. ",[167,172,173],{},"Phase 2","\n(LightGBM, 27 features) is in final training before promotion, behind an\nautomatic gate that compares any new model against the one currently served.",[146,176,178],{"id":177},"architecture","Architecture",[180,181,182,188,193,198],"ul",{},[183,184,185,187],"li",{},[167,186,132],{},": Nuxt 3 + MapLibre GL, PMTiles — map, property sheet, back office",[183,189,190,192],{},[167,191,135],{},": FastAPI + SQLAlchemy, with Celery for long-running work",[183,194,195,197],{},[167,196,31],{},": PostgreSQL \u002F PostGIS for the spatial layer, Redis as broker and cache",[183,199,200,202],{},[167,201,140],{},": docker-compose, workers split by queue",[151,204,205,206,210,211,214],{},"Workers are split across two queues: ",[207,208,209],"code",{},"imports"," for source ingestion, and\n",[207,212,213],{},"mutations"," for rebuilds, enrichment, tile generation and model training. That\nseparation keeps a multi-hour import from blocking a tile rebuild that takes a\nfew minutes.",[151,216,217,218,221],{},"An ",[207,219,220],{},"\u002Fadmin"," back office drives 15 source connectors and tracks import state.",[146,223,225],{"id":224},"what-the-map-covers","What the map covers",[151,227,228],{},"Around 7 million property sales (18M raw rows in the sales register) shown as\npoints coloured by price per square metre or as a heatmap, filterable by type,\nprice, energy rating and pool. A click opens the property sheet: sale history,\nextended energy rating, building context (outbuildings, fibre, school with its\nsocial index, condominium, recent permits, parks, water, noise), live regulatory\nhazards, and nearby amenities that can be layered onto the map.",[146,230,232],{"id":231},"method","Method",[151,234,235],{},"The project rests on documentation in ten files — architecture, sources, data\nmodel, pipelines, API, frontend, back office, runbook, roadmap — kept current\nwith every significant change. The runbook in particular records the\ninfrastructure traps learned the hard way, which saves relearning them.",[146,237,239],{"id":238},"what-i-took-away","What I took away",[151,241,242],{},"On this kind of project the difficulty is never the algorithm: it's reconciling\nidentifiers across registries that were never designed to talk to each other. A\ncadastral parcel and a building record share no key — it has to be rebuilt\nspatially, and you have to accept an imperfect match rate. And a quality metric\n(a backtest median absolute percentage error, say) is only worth what its\nprotocol is worth: a temporal leak between comparables and the tested sale\nskewed the headline number for a long time.",{"title":71,"searchDepth":244,"depth":244,"links":245},2,[246,247,248,249,250,251],{"id":148,"depth":244,"text":149},{"id":159,"depth":244,"text":160},{"id":177,"depth":244,"text":178},{"id":224,"depth":244,"text":225},{"id":231,"depth":244,"text":232},{"id":238,"depth":244,"text":239},"\u002Fprojets\u002Festimap-carte.png","An interactive map of France merging 16 public real-estate data sources — sales records, cadastre, building registry, energy ratings, census, hazards, schools, transport — behind a price estimation engine.","md",[256,259,262],{"url":257,"caption":258},"\u002Fprojets\u002Festimap-fiche-bien.png","Property sheet — sale, energy rating, official hazard exposure, context and nearby amenities.",{"url":260,"caption":261},"\u002Fprojets\u002Festimap-tableau-de-bord.png","Back office — service health, sales tiles and live job tracking.",{"url":263,"caption":264},"\u002Fprojets\u002Festimap-sources.png","Back office — driving the 15 data-source connectors.",[266,267,268],"16 public data sources merged across ~7 million sales","Estimation engine in production (kNN comparables, LightGBM in the wings)","A 14-step enrichment pipeline driven from a back office",{},"\u002Fprojets\u002Festimap",3,"Full-stack design and development",{"title":129,"description":253},"Completed","projets\u002Festimap",[277,20,26,278,35,37,279],"Nuxt 3","Celery","LightGBM","Web",2025,"r1KLUD8K6yLxger7FFaaA_6RvAUCkR2_Jt1TMYJEcQM",[284,362,444,572,745,850,915,1062],{"id":285,"title":286,"architecture":287,"body":300,"cover":71,"description":346,"extension":254,"featured":118,"gallery":347,"highlights":348,"interface":124,"meta":352,"navigation":124,"path":353,"rang":354,"repo":71,"role":272,"seo":355,"site":356,"status":357,"stem":358,"tags":359,"type":280,"year":281,"__hash__":361},"projets_en\u002Fprojets\u002Fcatan.md","Catan",[288,291,294,297],{"label":289,"description":290},"State","Game state lives entirely on the server, never in the client.",{"label":292,"description":293},"Validation","Every action is checked server-side — turn, resources, legality — before it is applied.",{"label":295,"description":296},"Broadcast","The new state is pushed back to every participant over WebSocket.",{"label":298,"description":299},"Persistence","Games and move history kept in PostgreSQL, environment started with a single docker compose command.",{"type":143,"value":301,"toc":341},[302,306,309,311,317,320,323,327,338],[146,303,305],{"id":304},"the-project","The project",[151,307,308],{},"A digital version of Catan, playable multiplayer in the browser: randomly\ngenerated board, settlement and road placement, resource production, trading\nbetween players, the robber and victory point counting.",[146,310,178],{"id":177},[151,312,313,316],{},[167,314,315],{},"Stack",": Vue 3 · FastAPI · PostgreSQL · WebSocket",[151,318,319],{},"Game state lives on the server, never in the client. Every action a player sends\nis validated server-side — is it their turn, do they have the resources, is the\nspot legal — before it is applied and pushed back to every participant.",[151,321,322],{},"It's the only defensible architecture for a multiplayer game: a client that\nworks out for itself what it's allowed to do is a client you can tamper with.",[146,324,326],{"id":325},"what-the-game-teaches","What the game teaches",[151,328,329,330,333,334,337],{},"Catan is an excellent modelling exercise. The board is a hexagonal tiling where\nsettlements sit on ",[167,331,332],{},"vertices"," and roads on ",[167,335,336],{},"edges"," — so three coordinate\nsystems coexist, and half the work is moving cleanly between them: which tiles\ntouch this vertex, which edges start here, which vertices are too close to take\na new settlement.",[151,339,340],{},"Once that geometry is laid down correctly, the game rules become almost trivial\nto implement. Laid down crooked, every rule becomes a special case.",{"title":71,"searchDepth":244,"depth":244,"links":342},[343,344,345],{"id":304,"depth":244,"text":305},{"id":177,"depth":244,"text":178},{"id":325,"depth":244,"text":326},"A multiplayer web adaptation of the board game Catan, playable together in a browser in real time.",[],[349,350,351],"Multiplayer games synchronised in real time over WebSocket","Game rules and move validation entirely server-side","Full stack up and running with a single docker compose command",{},"\u002Fprojets\u002Fcatan",5,{"title":286,"description":346},"https:\u002F\u002Fcatan.szmygiel.com","Live","projets\u002Fcatan",[16,26,25,34,360,51],"WebSocket","uC6d3QfDReLfb9V3QMHz_nksiLKIELYYau75-kASmt4",{"id":128,"title":129,"architecture":363,"body":368,"cover":252,"description":253,"extension":254,"featured":124,"gallery":436,"highlights":440,"interface":124,"meta":441,"navigation":124,"path":270,"rang":271,"repo":71,"role":272,"seo":442,"site":71,"status":274,"stem":275,"tags":443,"type":280,"year":281,"__hash__":282},[364,365,366,367],{"label":132,"description":133},{"label":135,"description":136},{"label":31,"description":138},{"label":140,"description":141},{"type":143,"value":369,"toc":428},[370,372,374,376,378,380,386,388,406,412,416,418,420,422,424,426],[146,371,149],{"id":148},[151,373,153],{},[151,375,156],{},[146,377,160],{"id":159},[151,379,163],{},[151,381,382,170,384,174],{},[167,383,169],{},[167,385,173],{},[146,387,178],{"id":177},[180,389,390,394,398,402],{},[183,391,392,187],{},[167,393,132],{},[183,395,396,192],{},[167,397,135],{},[183,399,400,197],{},[167,401,31],{},[183,403,404,202],{},[167,405,140],{},[151,407,205,408,210,410,214],{},[207,409,209],{},[207,411,213],{},[151,413,217,414,221],{},[207,415,220],{},[146,417,225],{"id":224},[151,419,228],{},[146,421,232],{"id":231},[151,423,235],{},[146,425,239],{"id":238},[151,427,242],{},{"title":71,"searchDepth":244,"depth":244,"links":429},[430,431,432,433,434,435],{"id":148,"depth":244,"text":149},{"id":159,"depth":244,"text":160},{"id":177,"depth":244,"text":178},{"id":224,"depth":244,"text":225},{"id":231,"depth":244,"text":232},{"id":238,"depth":244,"text":239},[437,438,439],{"url":257,"caption":258},{"url":260,"caption":261},{"url":263,"caption":264},[266,267,268],{},{"title":129,"description":253},[277,20,26,278,35,37,279],{"id":445,"title":446,"architecture":447,"body":460,"cover":554,"description":555,"extension":254,"featured":118,"gallery":556,"highlights":557,"interface":118,"meta":561,"navigation":124,"path":562,"rang":563,"repo":71,"role":272,"seo":564,"site":565,"status":357,"stem":566,"tags":567,"type":280,"year":570,"__hash__":571},"projets_en\u002Fprojets\u002Fglobe-satellites.md","Globe Satellites",[448,451,454,457],{"label":449,"description":450},"Sources","TLEs refreshed every 2h from Celestrak, with automatic failover to a backup proxy when the network blocks them.",{"label":452,"description":453},"Compute","Vectorised SGP4 propagation (SatrecArray + NumPy) with Skyfield — every satellite recomputed in a single call every 60 seconds.",{"label":455,"description":456},"Delivery","One shared computation broadcast over WebSocket to every connected client, in parallel.",{"label":458,"description":459},"Rendering","3D globe (Globe.gl \u002F Three.js), a single InstancedMesh for all satellites, ARM64 Docker image on a Raspberry Pi 4.",{"type":143,"value":461,"toc":547},[462,464,467,470,474,500,504,515,519,532,536],[146,463,305],{"id":304},[151,465,466],{},"Several thousand active satellites circle above our heads, and their orbits are\npublic. Celestrak publishes orbital elements in the TLE format (Two-Line\nElement), which is enough to compute the position of any object at any instant.",[151,468,469],{},"Globe Satellites shows that ballet on a 3D globe, refreshed every sixty seconds.",[146,471,473],{"id":472},"features","Features",[180,475,476,482,488,494],{},[183,477,478,481],{},[167,479,480],{},"Filtering by category"," — starlink, oneweb, communications, navigation,\nstations, weather, Earth observation, science, other — clickable in the HUD,\nwith \"Show all\" \u002F \"Hide all\" buttons",[183,483,484,487],{},[167,485,486],{},"Hover tooltip"," — name, altitude and category for each satellite",[183,489,490,493],{},[167,491,492],{},"Logarithmic radial scale"," — visibly separates low, medium and\ngeostationary orbits instead of stacking them on top of each other",[183,495,496,499],{},[167,497,498],{},"Automatic WebSocket reconnection"," (exponential backoff), with a watchdog\nthat catches a frozen connection with no clean TCP close",[146,501,503],{"id":502},"computing-on-the-server","Computing on the server",[151,505,506,507,510,511,514],{},"The structural choice is to ",[167,508,509],{},"compute server-side",". SGP4 orbital propagation\nfor several thousand objects, once a minute, is not work for the browser — all\nthe more so since every connected client shares exactly the same result. The\nserver computes once (",[207,512,513],{},"SatrecArray"," + NumPy, a single vectorised call rather\nthan a Python loop — roughly 24x faster), broadcasts to everyone over WebSocket\nin parallel, and the client only renders.",[146,516,518],{"id":517},"network-resilience","Network resilience",[151,520,521,522,525,526,531],{},"TLEs lose accuracy as the days pass — propagating from week-old elements drifts\nnoticeably — hence an automatic refresh every two hours. Loading follows a\nthree-tier chain: the full Celestrak group first, a fallback to 37 groups\ndownloaded in parallel if that fails or returns a 403, then a backup proxy if\nboth fail — a sign of a network block rather than an isolated incident. It\nreturns to the direct route on its own, with no manual action. Data freshness is\nexposed through a ",[207,523,524],{},"\u002Fhealth"," endpoint, which pushes best-effort\n",[527,528,530],"a",{"href":529},"\u002Fen\u002Fprojects\u002Fvigie","Vigie"," monitoring for the refresh cycle.",[146,533,535],{"id":534},"the-hardware-constraint","The hardware constraint",[151,537,538,539,542,543,546],{},"The project is built for a ",[167,540,541],{},"Raspberry Pi 4 on ARM64",". That constraint drove\nseveral decisions: a multi-architecture Docker image, vectorised NumPy\ncomputation rather than a Python loop, a single Three.js ",[207,544,545],{},"InstancedMesh"," rather\nthan one object per satellite, and broadcasting one shared state rather than\ncomputing per connected client.",{"title":71,"searchDepth":244,"depth":244,"links":548},[549,550,551,552,553],{"id":304,"depth":244,"text":305},{"id":472,"depth":244,"text":473},{"id":502,"depth":244,"text":503},{"id":517,"depth":244,"text":518},{"id":534,"depth":244,"text":535},"\u002Fprojets\u002Fglobe-satellites-carte.png","An interactive 3D globe showing the live position of active satellites in Earth orbit, built to run on a Raspberry Pi.",[],[558,559,560],"Positions recomputed every 60 seconds, vectorised SGP4 propagation (~24x faster than a loop)","Automatic failover to a backup proxy if Celestrak becomes unreachable","Every satellite rendered in a single draw call (Three.js InstancedMesh)",{},"\u002Fprojets\u002Fglobe-satellites",4,{"title":446,"description":555},"https:\u002F\u002Fglobe.szmygiel.com","projets\u002Fglobe-satellites",[16,568,26,25,360,51,569],"Globe.gl","Raspberry Pi",2026,"TS8Sx0gdo3anp3sYPh3mls3nfLiVHR3E-exVBbiOAZM",{"id":573,"title":574,"architecture":575,"body":586,"cover":724,"description":725,"extension":254,"featured":124,"gallery":726,"highlights":733,"interface":124,"meta":737,"navigation":124,"path":738,"rang":244,"repo":71,"role":272,"seo":739,"site":740,"status":357,"stem":741,"tags":742,"type":280,"year":570,"__hash__":744},"projets_en\u002Fprojets\u002Fpyro-radar.md","Pyro Radar",[576,578,581,584],{"label":449,"description":577},"Six satellite feeds aggregated — NASA FIRMS, GOES-18\u002F19, Meteosat MTG, Himawari-9, Sentinel-3.",{"label":579,"description":580},"Ingestion","One ingester per source (CSV, NetCDF, HDF5) normalises into a shared schema of geolocated detections.",{"label":582,"description":583},"Storage","Burned areas estimated as polygons directly in PostGIS, in batches.",{"label":458,"description":585},"MapLibre GL vector tiles, real-time delivery over Server-Sent Events.",{"type":143,"value":587,"toc":718},[588,592,595,598,602,676,679,683,689,695,701,707,713,715],[146,589,591],{"id":590},"the-idea","The idea",[151,593,594],{},"Fire detection data is public, free and abundant — NASA, ESA, NOAA and JAXA\nbroadcast it continuously. It also comes in heterogeneous formats, at different\ncadences, with geographic coverage that only partly overlaps.",[151,596,597],{},"Pyro Radar merges those feeds into a single worldwide map, refreshed continuously.",[146,599,601],{"id":600},"the-sources","The sources",[603,604,605,621],"table",{},[606,607,608],"thead",{},[609,610,611,615,618],"tr",{},[612,613,614],"th",{},"Source",[612,616,617],{},"Coverage",[612,619,620],{},"Cadence",[622,623,624,636,647,657,667],"tbody",{},[609,625,626,630,633],{},[627,628,629],"td",{},"NASA FIRMS (VIIRS \u002F MODIS)",[627,631,632],{},"Worldwide",[627,634,635],{},"Orbital passes",[609,637,638,641,644],{},[627,639,640],{},"GOES-18 \u002F 19",[627,642,643],{},"Americas",[627,645,646],{},"10 min",[609,648,649,652,655],{},[627,650,651],{},"Meteosat MTG (LSA SAF)",[627,653,654],{},"Europe \u002F Africa",[627,656,646],{},[609,658,659,662,665],{},[627,660,661],{},"Himawari-9",[627,663,664],{},"Asia \u002F Oceania",[627,666,646],{},[609,668,669,672,674],{},[627,670,671],{},"Sentinel-3 SLSTR (Copernicus)",[627,673,632],{},[627,675,635],{},[151,677,678],{},"Geostationary satellites bring the frequency, polar orbits bring the resolution.",[146,680,682],{"id":681},"the-technical-work","The technical work",[151,684,685,688],{},[167,686,687],{},"Ingestion."," CSV for FIRMS, NetCDF for GOES, HDF5 for MTG: one ingester per\nsource normalises into a shared schema of geolocated detections.",[151,690,691,694],{},[167,692,693],{},"Burned areas."," Point detections become estimated polygons directly in\nPostGIS, in batches — loading every zone at once was taking Postgres down.",[151,696,697,700],{},[167,698,699],{},"Wind."," GFS data is only decoded around active fires. The worldwide grid\nwould have weighed several gigabytes per cycle for information that only matters\nover a few dozen square kilometres.",[151,702,703,706],{},[167,704,705],{},"Real time."," Server-Sent Events: no polling, and no bidirectional WebSocket\nwhere a one-way stream is enough.",[151,708,709,712],{},[167,710,711],{},"Mapping."," MapLibre GL, OpenFreeMap basemap and Esri satellite view — no API\nkey, so no quota and no surprise bill.",[146,714,239],{"id":238},[151,716,717],{},"Adding a coverage area is one line of SQL. That design constraint, set early,\nkept the geographic logic from scattering across the application code.",{"title":71,"searchDepth":244,"depth":244,"links":719},[720,721,722,723],{"id":590,"depth":244,"text":591},{"id":600,"depth":244,"text":601},{"id":681,"depth":244,"text":682},{"id":238,"depth":244,"text":239},"\u002Fprojets\u002Fpyro-radar-carte.png","Real-time wildfire tracking worldwide, built from six satellite sources aggregated onto a single vector map.",[727,730],{"url":728,"caption":729},"\u002Fprojets\u002Fpyro-radar-points-chauds.png","Hotspots — individual detections with confidence and per-click detail.",{"url":731,"caption":732},"\u002Fprojets\u002Fpyro-radar-historique.png","30-day history — cumulative fire count and estimated burned area.",[734,735,736],"Six satellite sources aggregated, from polar orbits to geostationary","Burned-area estimates computed directly in PostGIS","Real-time delivery over Server-Sent Events, no polling",{},"\u002Fprojets\u002Fpyro-radar",{"title":574,"description":725},"https:\u002F\u002Fpyroradar.com","projets\u002Fpyro-radar",[16,20,28,35,34,743],"SSE","_v_G5VhIz3MViZGbQOxvjVqVhYkR5cKTu99Y43u2FR8",{"id":746,"title":747,"architecture":748,"body":760,"cover":71,"description":834,"extension":254,"featured":118,"gallery":835,"highlights":836,"interface":124,"meta":840,"navigation":124,"path":841,"rang":842,"repo":71,"role":843,"seo":844,"site":71,"status":274,"stem":845,"tags":846,"type":56,"year":570,"__hash__":849},"projets_en\u002Fprojets\u002Frootine.md","Rootine",[749,751,754,757],{"label":298,"description":750},"Habits and check-offs stored locally with SwiftData, with no external dependency.",{"label":752,"description":753},"Check-off","A Yesterday \u002F Today toggle to check off retroactively, without breaking streaks.",{"label":755,"description":756},"Visualisation","Monthly heatmap calendar where colour intensity reflects the completion rate.",{"label":758,"description":759},"Interface","Spring animations and haptic feedback on every check, in SwiftUI.",{"type":143,"value":761,"toc":828},[762,768,770,786,788,794,800,806,812,816,819,822,825],[763,764,765],"blockquote",{},[151,766,767],{},"Habits are the roots of who you become.",[146,769,305],{"id":304},[151,771,772,773,777,778,781,782,785],{},"Rootine — a play on ",[774,775,776],"em",{},"root"," and ",[774,779,780],{},"routine"," — is a habit tracker built around one\nsimple principle: ",[167,783,784],{},"consistency beats intensity",". Ten minutes of exercise every\nday is worth more than one two-hour session a week.",[146,787,473],{"id":472},[151,789,790,793],{},[167,791,792],{},"Home"," — the checklist of today's habits, with an animated checkbox (spring\nand haptic feedback), an overall progress bar and a congratulations message when\neverything is done. A Yesterday \u002F Today toggle allows checking off retroactively.",[151,795,796,799],{},[167,797,798],{},"Routines"," — creating, editing and deleting the habits you track.",[151,801,802,805],{},[167,803,804],{},"Calendar"," — a monthly heatmap where colour intensity reflects that day's\ncompletion rate. Consistency becomes visible at a glance.",[151,807,808,811],{},[167,809,810],{},"Statistics"," — current streaks, completion rate, trends over time.",[146,813,815],{"id":814},"the-stance","The stance",[151,817,818],{},"Most habit apps punish forgetting: broken streaks, lost badges, guilt-tripping\nnotifications. That lever motivates for a week, then produces the opposite\neffect — you uninstall rather than face the dashboard.",[151,820,821],{},"Rootine takes the opposite stance. You can check off yesterday, no streak breaks\nfor good, and the tone stays encouraging. The point is to keep going for months,\nnot to win a score.",[146,823,315],{"id":824},"stack",[151,826,827],{},"SwiftUI and SwiftData, with no external dependency. All data stays on the device.",{"title":71,"searchDepth":244,"depth":244,"links":829},[830,831,832,833],{"id":304,"depth":244,"text":305},{"id":472,"depth":244,"text":473},{"id":814,"depth":244,"text":815},{"id":824,"depth":244,"text":315},"A minimal, forgiving iOS habit tracker — today's checklist, a heatmap calendar and consistency statistics.",[],[837,838,839],"Retroactive check-off for yesterday, without the guilt trip","Monthly heatmap calendar and trend statistics","Spring animations and haptic feedback on every check",{},"\u002Fprojets\u002Frootine",8,"iOS design and development",{"title":747,"description":834},"projets\u002Frootine",[59,60,61,847,848],"Swift","Charts","-jczwI2MfId-BcdlMwAxDkg111FvQlpAM7nKRbRdCv8",{"id":851,"title":852,"architecture":853,"body":864,"cover":71,"description":901,"extension":254,"featured":118,"gallery":902,"highlights":903,"interface":124,"meta":907,"navigation":124,"path":908,"rang":909,"repo":71,"role":843,"seo":910,"site":71,"status":274,"stem":911,"tags":912,"type":56,"year":570,"__hash__":914},"projets_en\u002Fprojets\u002Fsofa.md","Sofa",[854,857,859,862],{"label":855,"description":856},"Search","TMDB API queries for title search and detail sheets.",{"label":582,"description":858},"Library and watch history persisted with SwiftData, readable offline.",{"label":860,"description":861},"Enrichment","Available streaming platforms pulled from TMDB.",{"label":758,"description":863},"Dark design system inspired by cinema auditoriums, in native SwiftUI.",{"type":143,"value":865,"toc":895},[866,868,871,873,880,883,887,890,892],[146,867,305],{"id":304},[151,869,870],{},"Sofa answers an everyday problem: knowing what to watch, and above all\nremembering what you meant to watch. The app lets you search for a film or\nseries through the TMDB API, add it to your library, mark what you've seen, rate\ntitles and see where to stream them.",[146,872,315],{"id":824},[151,874,875,876,879],{},"A ",[167,877,878],{},"100% native iOS"," app, written in SwiftUI with SwiftData for persistence. No\ncross-platform framework, no WebView: the animations, the gestures and the\nsystem behaviour are those of an iOS app, because that is precisely what\nseparates an app you keep from an app you delete.",[151,881,882],{},"SwiftData handles local persistence, which makes the library readable offline —\nonly search and detail refreshes need the network.",[146,884,886],{"id":885},"design","Design",[151,888,889],{},"A dark interface, inspired by the atmosphere of a cinema: deep backgrounds,\nposters given room to breathe, high-contrast typography. The design system is\ndefined once — colours, spacing, type — then applied consistently across every\nscreen.",[146,891,239],{"id":238},[151,893,894],{},"Coming from the web, SwiftUI asks you to unlearn a few reflexes. The layout\nsystem doesn't work like Flexbox, and state propagation follows its own rules.\nIn exchange, the integration with the system — widgets, sharing, dark mode,\naccessibility — is on a different level from anything you get in a web app.",{"title":71,"searchDepth":244,"depth":244,"links":896},[897,898,899,900],{"id":304,"depth":244,"text":305},{"id":824,"depth":244,"text":315},{"id":885,"depth":244,"text":886},{"id":238,"depth":244,"text":239},"A native iOS watchlist app for films and series — TMDB search, personal library, watch tracking and ratings.",[],[904,905,906],"Local persistence with SwiftData, usable offline","TMDB integration — search, detail sheets and streaming platforms","Dark design system inspired by cinema auditoriums",{},"\u002Fprojets\u002Fsofa",7,{"title":852,"description":901},"projets\u002Fsofa",[59,60,61,913,847],"TMDB API","L9BjW5Ev8RnMN90W7J3oAsl5_rNEZpLJ4xhveeWZqgw",{"id":916,"title":917,"architecture":918,"body":929,"cover":1043,"description":1044,"extension":254,"featured":118,"gallery":1045,"highlights":1046,"interface":118,"meta":1050,"navigation":124,"path":1051,"rang":1052,"repo":71,"role":1053,"seo":1054,"site":71,"status":274,"stem":1055,"tags":1056,"type":31,"year":281,"__hash__":1061},"projets_en\u002Fprojets\u002Fthanos.md","Thanos",[919,921,924,927],{"label":31,"description":920},"Data Manager — fetching and caching historical series per exchange and timeframe.",{"label":922,"description":923},"Indicators","Custom Indicators — technical indicators implemented by hand rather than imported.",{"label":925,"description":926},"Backtest","Backtest Analysis — performance and risk metrics across five strategy families.",{"label":755,"description":928},"Plot Analysis — entries, exits and equity curves.",{"type":143,"value":930,"toc":1037},[931,933,936,940,994,997,1001,1027,1031,1034],[146,932,305],{"id":304},[151,934,935],{},"Thanos is a complete environment for prototyping, testing and comparing trading\nstrategies across several exchanges and timeframes. The goal isn't to trade:\nit's to have an honest test bed for the question \"would this idea have worked,\nand at what cost in risk\".",[146,937,939],{"id":938},"strategies-implemented","Strategies implemented",[603,941,942,952],{},[606,943,944],{},[609,945,946,949],{},[612,947,948],{},"Strategy",[612,950,951],{},"Principle",[622,953,954,962,970,978,986],{},[609,955,956,959],{},[627,957,958],{},"SMA",[627,960,961],{},"Simple moving average crossover",[609,963,964,967],{},[627,965,966],{},"EMA",[627,968,969],{},"Exponential moving average crossover",[609,971,972,975],{},[627,973,974],{},"Momentum (RSI)",[627,976,977],{},"Entries on oversold \u002F overbought zones",[609,979,980,983],{},[627,981,982],{},"SMA-EMA combined",[627,984,985],{},"Cross-confirmation between the two moving average families",[609,987,988,991],{},[627,989,990],{},"TRIX",[627,992,993],{},"Triple exponential smoothing oscillator",[151,995,996],{},"They all share the same interface, which makes it possible to evaluate them on\nidentical ground — same data, same fees, same metrics.",[146,998,1000],{"id":999},"modules","Modules",[180,1002,1003,1009,1015,1021],{},[183,1004,1005,1008],{},[167,1006,1007],{},"Data Manager"," — fetching and caching historical series per exchange and timeframe",[183,1010,1011,1014],{},[167,1012,1013],{},"Custom Indicators"," — technical indicators implemented by hand rather than imported",[183,1016,1017,1020],{},[167,1018,1019],{},"Backtest Analysis"," — performance and risk metrics",[183,1022,1023,1026],{},[167,1024,1025],{},"Plot Analysis"," — visualising entries, exits and equity curves",[146,1028,1030],{"id":1029},"the-real-lesson","The real lesson",[151,1032,1033],{},"The hard part of a backtest isn't making it run: it's stopping it from lying.\nBiases slip in easily — using data that wasn't yet available at decision time,\nignoring fees and slippage, or tuning parameters on the same period used to\nvalidate the strategy.",[151,1035,1036],{},"A strategy that posts a spectacular backtest result is almost always the sign of\na data leak somewhere, not of a discovery.",{"title":71,"searchDepth":244,"depth":244,"links":1038},[1039,1040,1041,1042],{"id":304,"depth":244,"text":305},{"id":938,"depth":244,"text":939},{"id":999,"depth":244,"text":1000},{"id":1029,"depth":244,"text":1030},"\u002Fprojets\u002Fthanos-cover.svg","A research, backtesting and analysis platform for trading strategies on cryptocurrency markets.",[],[1047,1048,1049],"Five strategy families comparable on a single test bed","Custom technical indicators implemented by hand","Historical data cached across multiple exchanges and timeframes",{},"\u002Fprojets\u002Fthanos",6,"Design and development",{"title":917,"description":1044},"projets\u002Fthanos",[25,1057,1058,1059,1060],"Pandas","NumPy","Backtesting","Data analysis","dSBxOYef84ONZdN6irpkgiOWjftugwl2o8W9AqvRZUY",{"id":1063,"title":530,"architecture":1064,"body":1075,"cover":1260,"description":1261,"extension":254,"featured":124,"gallery":1262,"highlights":1272,"interface":124,"meta":1277,"navigation":124,"path":1278,"rang":1279,"repo":71,"role":272,"seo":1280,"site":1281,"status":357,"stem":1282,"tags":1283,"type":280,"year":570,"__hash__":1284},"projets_en\u002Fprojets\u002Fvigie.md",[1065,1068,1070,1072],{"label":1066,"description":1067},"API","AdonisJS v6, TypeScript, Lucid — business logic and endpoints.",{"label":31,"description":1069},"PostgreSQL 17, progressive rollup (fine → hourly → daily) to keep retention affordable.",{"label":758,"description":1071},"Vue 3, Vite, Pinia — dashboard and public status page.",{"label":1073,"description":1074},"Exposure","Docker Compose on a VPS (Caddy, GitHub Actions), plus a read-only MCP server to query monitoring from the editor.",{"type":143,"value":1076,"toc":1252},[1077,1079,1082,1085,1089,1121,1124,1126,1173,1193,1197,1200,1214,1218,1225,1243,1245],[146,1078,149],{"id":148},[151,1080,1081],{},"I had a dozen projects online and no overall view: did last night's cron run? Is\nthe API still answering? How many visitors this week? Every answer meant logging\ninto a different server.",[151,1083,1084],{},"Plugging a third-party service into each project would have multiplied the\naccounts, the costs and the cookie banners. I preferred to build the single tool\nI was missing.",[146,1086,1088],{"id":1087},"what-vigie-does","What Vigie does",[180,1090,1091,1097,1103,1109,1115],{},[183,1092,1093,1096],{},[167,1094,1095],{},"Visit analytics"," with no cookie and no persistent identifier — no consent banner required",[183,1098,1099,1102],{},[167,1100,1101],{},"Uptime monitoring",": regular HTTP probes with history and uptime calculation",[183,1104,1105,1108],{},[167,1106,1107],{},"Scheduled-job supervision",": a cron that hasn't checked in within its window raises an alert",[183,1110,1111,1114],{},[167,1112,1113],{},"Error tracking"," for the application errors reported by the connected projects",[183,1116,1117,1120],{},[167,1118,1119],{},"A public status page",", fed by the same data as the internal dashboard",[151,1122,1123],{},"Every monitored project has its own API key, and never sees another project's data.",[146,1125,178],{"id":177},[603,1127,1128,1138],{},[606,1129,1130],{},[609,1131,1132,1135],{},[612,1133,1134],{},"Layer",[612,1136,1137],{},"Choice",[622,1139,1140,1147,1155,1162],{},[609,1141,1142,1144],{},[627,1143,1066],{},[627,1145,1146],{},"AdonisJS v6, TypeScript, Lucid",[609,1148,1149,1152],{},[627,1150,1151],{},"Database",[627,1153,1154],{},"PostgreSQL 17",[609,1156,1157,1159],{},[627,1158,758],{},[627,1160,1161],{},"Vue 3, Vite, Pinia, TypeScript",[609,1163,1164,1167],{},[627,1165,1166],{},"Deployment",[627,1168,1169,1170],{},"Docker Compose on a VPS, Caddy in front, GitHub Actions on ",[207,1171,1172],{},"main",[151,1174,1175,1176,1179,1180,1179,1183,1179,1186,1179,1189,1192],{},"Everything runs in containers — ",[207,1177,1178],{},"postgres",", ",[207,1181,1182],{},"redis",[207,1184,1185],{},"api",[207,1187,1188],{},"worker",[207,1190,1191],{},"web"," —\nwith no Node required on the host machine. The worker handles probes and\nrollups outside the request cycle, which keeps the API responsive no matter how\nmany projects are being monitored.",[146,1194,1196],{"id":1195},"the-client-side-agent","The client-side agent",[151,1198,1199],{},"Collecting means putting code inside someone else's page: a bug here doesn't\ndegrade Vigie, it degrades the site being monitored. So the script stays under\ntwo kilobytes, with no cookie and no browser fingerprint — and the budget is\nchecked at build time, so the build fails rather than shipping something heavier.",[151,1201,1202,1203,777,1206,1209,1210,1213],{},"It wraps ",[207,1204,1205],{},"pushState",[207,1207,1208],{},"replaceState"," to catch navigations in a single-page\napp, then restores the original methods: nothing is permanently altered for the\nhost application. A ",[207,1211,1212],{},"@vigie\u002Fjs"," plugin hooks straight into the Vue 3 router\ninstead of guessing from history events.",[146,1215,1217],{"id":1216},"an-mcp-server-to-stay-in-the-editor","An MCP server, to stay in the editor",[151,1219,1220,1221,1224],{},"What's broken? Is this error new? Did last night's cron run? Vigie exposes six\nread-only tools over ",[167,1222,1223],{},"MCP"," to answer those questions without opening the\ndashboard.",[151,1226,1227,1228,1231,1232,1179,1235,1238,1239,1242],{},"Two deliberate choices: ",[167,1229,1230],{},"no writes at all"," — an assistant that reads gets the\ndiagnosis wrong, an assistant that writes gets production wrong; and ",[167,1233,1234],{},"internal\ncodes translated on the way out",[207,1236,1237],{},"cause: \"silent agent\""," rather than\n",[207,1240,1241],{},"reason: 5",", because a model facing an unknown code doesn't stop, it guesses.",[146,1244,239],{"id":238},[151,1246,1247,1248,1251],{},"The hard part wasn't collection but ",[167,1249,1250],{},"retention",": keeping years of measurement\npoints without blowing up the database. The answer was progressive rollup —\nfine-grained data ages into hourly averages, then daily ones.",{"title":71,"searchDepth":244,"depth":244,"links":1253},[1254,1255,1256,1257,1258,1259],{"id":148,"depth":244,"text":149},{"id":1087,"depth":244,"text":1088},{"id":177,"depth":244,"text":178},{"id":1195,"depth":244,"text":1196},{"id":1216,"depth":244,"text":1217},{"id":238,"depth":244,"text":239},"\u002Fprojets\u002Fvigie-dashboard.png","Unified monitoring for my web projects — cookieless analytics, uptime checks, scheduled-job tracking and a public status page.",[1263,1266,1269],{"url":1264,"caption":1265},"\u002Fprojets\u002Fvigie-erreurs.png","Application errors — grouped by fingerprint, with latest occurrences and stack trace.",{"url":1267,"caption":1268},"\u002Fprojets\u002Fvigie-taches.png","Scheduled jobs — state, run history and an incident when one fails.",{"url":1270,"caption":1271},"\u002Fprojets\u002Fvigie-disponibilite.png","Uptime — URL checks with response time and 30-day history.",[1273,1274,1275,1276],"Multi-project with strict data isolation per API key","Cookieless visit analytics, so no consent banner","Alerts and an automatically generated public status page","Read-only MCP server — monitoring you can query from the editor",{},"\u002Fprojets\u002Fvigie",1,{"title":530,"description":1261},"https:\u002F\u002Fvigie.watch","projets\u002Fvigie",[28,16,18,34,37,51,1223],"RiBHY7jRZ-V3wJoap5kgVKpqBMn-pw7QB4wK5yabSNI",{"left":116,"top":116,"width":117,"height":117,"rotate":116,"vFlip":118,"hFlip":118,"body":1286},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 12h14m-7-7l7 7l-7 7\"\u002F>",1788530203853]