[{"data":1,"prerenderedAt":1316},["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\u002Fpyro-radar":127,"projets-navigation-en":314,"i-lucide:arrow-right":1314},{"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":143,"cover":288,"description":289,"extension":290,"featured":124,"gallery":291,"highlights":298,"interface":124,"meta":302,"navigation":124,"path":303,"rang":282,"repo":71,"role":304,"seo":305,"site":306,"status":307,"stem":308,"tags":309,"type":311,"year":312,"__hash__":313},"projets_en\u002Fprojets\u002Fpyro-radar.md","Pyro Radar",[131,134,137,140],{"label":132,"description":133},"Sources","Six satellite feeds aggregated — NASA FIRMS, GOES-18\u002F19, Meteosat MTG, Himawari-9, Sentinel-3.",{"label":135,"description":136},"Ingestion","One ingester per source (CSV, NetCDF, HDF5) normalises into a shared schema of geolocated detections.",{"label":138,"description":139},"Storage","Burned areas estimated as polygons directly in PostGIS, in batches.",{"label":141,"description":142},"Rendering","MapLibre GL vector tiles, real-time delivery over Server-Sent Events.",{"type":144,"value":145,"toc":281},"minimark",[146,151,155,158,162,236,239,243,250,256,262,268,274,278],[147,148,150],"h2",{"id":149},"the-idea","The idea",[152,153,154],"p",{},"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.",[152,156,157],{},"Pyro Radar merges those feeds into a single worldwide map, refreshed continuously.",[147,159,161],{"id":160},"the-sources","The sources",[163,164,165,181],"table",{},[166,167,168],"thead",{},[169,170,171,175,178],"tr",{},[172,173,174],"th",{},"Source",[172,176,177],{},"Coverage",[172,179,180],{},"Cadence",[182,183,184,196,207,217,227],"tbody",{},[169,185,186,190,193],{},[187,188,189],"td",{},"NASA FIRMS (VIIRS \u002F MODIS)",[187,191,192],{},"Worldwide",[187,194,195],{},"Orbital passes",[169,197,198,201,204],{},[187,199,200],{},"GOES-18 \u002F 19",[187,202,203],{},"Americas",[187,205,206],{},"10 min",[169,208,209,212,215],{},[187,210,211],{},"Meteosat MTG (LSA SAF)",[187,213,214],{},"Europe \u002F Africa",[187,216,206],{},[169,218,219,222,225],{},[187,220,221],{},"Himawari-9",[187,223,224],{},"Asia \u002F Oceania",[187,226,206],{},[169,228,229,232,234],{},[187,230,231],{},"Sentinel-3 SLSTR (Copernicus)",[187,233,192],{},[187,235,195],{},[152,237,238],{},"Geostationary satellites bring the frequency, polar orbits bring the resolution.",[147,240,242],{"id":241},"the-technical-work","The technical work",[152,244,245,249],{},[246,247,248],"strong",{},"Ingestion."," CSV for FIRMS, NetCDF for GOES, HDF5 for MTG: one ingester per\nsource normalises into a shared schema of geolocated detections.",[152,251,252,255],{},[246,253,254],{},"Burned areas."," Point detections become estimated polygons directly in\nPostGIS, in batches — loading every zone at once was taking Postgres down.",[152,257,258,261],{},[246,259,260],{},"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.",[152,263,264,267],{},[246,265,266],{},"Real time."," Server-Sent Events: no polling, and no bidirectional WebSocket\nwhere a one-way stream is enough.",[152,269,270,273],{},[246,271,272],{},"Mapping."," MapLibre GL, OpenFreeMap basemap and Esri satellite view — no API\nkey, so no quota and no surprise bill.",[147,275,277],{"id":276},"what-i-took-away","What I took away",[152,279,280],{},"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":282,"depth":282,"links":283},2,[284,285,286,287],{"id":149,"depth":282,"text":150},{"id":160,"depth":282,"text":161},{"id":241,"depth":282,"text":242},{"id":276,"depth":282,"text":277},"\u002Fprojets\u002Fpyro-radar-carte.png","Real-time wildfire tracking worldwide, built from six satellite sources aggregated onto a single vector map.","md",[292,295],{"url":293,"caption":294},"\u002Fprojets\u002Fpyro-radar-points-chauds.png","Hotspots — individual detections with confidence and per-click detail.",{"url":296,"caption":297},"\u002Fprojets\u002Fpyro-radar-historique.png","30-day history — cumulative fire count and estimated burned area.",[299,300,301],"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","Full-stack design and development",{"title":129,"description":289},"https:\u002F\u002Fpyroradar.com","Live","projets\u002Fpyro-radar",[16,20,28,35,34,310],"SSE","Web",2026,"_v_G5VhIz3MViZGbQOxvjVqVhYkR5cKTu99Y43u2FR8",[315,395,538,663,774,879,944,1091],{"id":316,"title":317,"architecture":318,"body":331,"cover":71,"description":379,"extension":290,"featured":118,"gallery":380,"highlights":381,"interface":124,"meta":385,"navigation":124,"path":386,"rang":387,"repo":71,"role":304,"seo":388,"site":389,"status":307,"stem":390,"tags":391,"type":311,"year":393,"__hash__":394},"projets_en\u002Fprojets\u002Fcatan.md","Catan",[319,322,325,328],{"label":320,"description":321},"State","Game state lives entirely on the server, never in the client.",{"label":323,"description":324},"Validation","Every action is checked server-side — turn, resources, legality — before it is applied.",{"label":326,"description":327},"Broadcast","The new state is pushed back to every participant over WebSocket.",{"label":329,"description":330},"Persistence","Games and move history kept in PostgreSQL, environment started with a single docker compose command.",{"type":144,"value":332,"toc":374},[333,337,340,344,350,353,356,360,371],[147,334,336],{"id":335},"the-project","The project",[152,338,339],{},"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.",[147,341,343],{"id":342},"architecture","Architecture",[152,345,346,349],{},[246,347,348],{},"Stack",": Vue 3 · FastAPI · PostgreSQL · WebSocket",[152,351,352],{},"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.",[152,354,355],{},"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.",[147,357,359],{"id":358},"what-the-game-teaches","What the game teaches",[152,361,362,363,366,367,370],{},"Catan is an excellent modelling exercise. The board is a hexagonal tiling where\nsettlements sit on ",[246,364,365],{},"vertices"," and roads on ",[246,368,369],{},"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.",[152,372,373],{},"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":282,"depth":282,"links":375},[376,377,378],{"id":335,"depth":282,"text":336},{"id":342,"depth":282,"text":343},{"id":358,"depth":282,"text":359},"A multiplayer web adaptation of the board game Catan, playable together in a browser in real time.",[],[382,383,384],"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":317,"description":379},"https:\u002F\u002Fcatan.szmygiel.com","projets\u002Fcatan",[16,26,25,34,392,51],"WebSocket",2025,"uC6d3QfDReLfb9V3QMHz_nksiLKIELYYau75-kASmt4",{"id":396,"title":397,"architecture":398,"body":410,"cover":511,"description":512,"extension":290,"featured":124,"gallery":513,"highlights":523,"interface":124,"meta":527,"navigation":124,"path":528,"rang":529,"repo":71,"role":304,"seo":530,"site":71,"status":531,"stem":532,"tags":533,"type":311,"year":393,"__hash__":537},"projets_en\u002Fprojets\u002Festimap.md","Estimap",[399,402,405,407],{"label":400,"description":401},"Frontend","Nuxt 3 + MapLibre GL (PMTiles) — map, property sheet, back office.",{"label":403,"description":404},"Backend","FastAPI + SQLAlchemy, Celery for long-running work, two separate queues (imports\u002Fmutations).",{"label":31,"description":406},"PostgreSQL \u002F PostGIS (~100M rows) for the spatial layer, Redis as broker and cache.",{"label":408,"description":409},"Infra","Docker Compose, workers split by queue.",{"type":144,"value":411,"toc":503},[412,416,419,422,426,429,439,441,465,477,484,488,491,495,498,500],[147,413,415],{"id":414},"the-problem","The problem",[152,417,418],{},"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.",[152,420,421],{},"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.",[147,423,425],{"id":424},"the-product-goal","The product goal",[152,427,428],{},"A property price estimation engine — the map is the exploration surface, but the\npoint is to feed a model with genuinely cross-referenced features.",[152,430,431,434,435,438],{},[246,432,433],{},"Phase 1"," (weighted kNN comparables, progressive radii, plausibility guards)\nis in production and answers 100% of estimation requests today. ",[246,436,437],{},"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.",[147,440,343],{"id":342},[442,443,444,450,455,460],"ul",{},[445,446,447,449],"li",{},[246,448,400],{},": Nuxt 3 + MapLibre GL, PMTiles — map, property sheet, back office",[445,451,452,454],{},[246,453,403],{},": FastAPI + SQLAlchemy, with Celery for long-running work",[445,456,457,459],{},[246,458,31],{},": PostgreSQL \u002F PostGIS for the spatial layer, Redis as broker and cache",[445,461,462,464],{},[246,463,408],{},": docker-compose, workers split by queue",[152,466,467,468,472,473,476],{},"Workers are split across two queues: ",[469,470,471],"code",{},"imports"," for source ingestion, and\n",[469,474,475],{},"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.",[152,478,479,480,483],{},"An ",[469,481,482],{},"\u002Fadmin"," back office drives 15 source connectors and tracks import state.",[147,485,487],{"id":486},"what-the-map-covers","What the map covers",[152,489,490],{},"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.",[147,492,494],{"id":493},"method","Method",[152,496,497],{},"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.",[147,499,277],{"id":276},[152,501,502],{},"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":282,"depth":282,"links":504},[505,506,507,508,509,510],{"id":414,"depth":282,"text":415},{"id":424,"depth":282,"text":425},{"id":342,"depth":282,"text":343},{"id":486,"depth":282,"text":487},{"id":493,"depth":282,"text":494},{"id":276,"depth":282,"text":277},"\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.",[514,517,520],{"url":515,"caption":516},"\u002Fprojets\u002Festimap-fiche-bien.png","Property sheet — sale, energy rating, official hazard exposure, context and nearby amenities.",{"url":518,"caption":519},"\u002Fprojets\u002Festimap-tableau-de-bord.png","Back office — service health, sales tiles and live job tracking.",{"url":521,"caption":522},"\u002Fprojets\u002Festimap-sources.png","Back office — driving the 15 data-source connectors.",[524,525,526],"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,{"title":397,"description":512},"Completed","projets\u002Festimap",[534,20,26,535,35,37,536],"Nuxt 3","Celery","LightGBM","r1KLUD8K6yLxger7FFaaA_6RvAUCkR2_Jt1TMYJEcQM",{"id":539,"title":540,"architecture":541,"body":552,"cover":646,"description":647,"extension":290,"featured":118,"gallery":648,"highlights":649,"interface":118,"meta":653,"navigation":124,"path":654,"rang":655,"repo":71,"role":304,"seo":656,"site":657,"status":307,"stem":658,"tags":659,"type":311,"year":312,"__hash__":662},"projets_en\u002Fprojets\u002Fglobe-satellites.md","Globe Satellites",[542,544,547,550],{"label":132,"description":543},"TLEs refreshed every 2h from Celestrak, with automatic failover to a backup proxy when the network blocks them.",{"label":545,"description":546},"Compute","Vectorised SGP4 propagation (SatrecArray + NumPy) with Skyfield — every satellite recomputed in a single call every 60 seconds.",{"label":548,"description":549},"Delivery","One shared computation broadcast over WebSocket to every connected client, in parallel.",{"label":141,"description":551},"3D globe (Globe.gl \u002F Three.js), a single InstancedMesh for all satellites, ARM64 Docker image on a Raspberry Pi 4.",{"type":144,"value":553,"toc":639},[554,556,559,562,566,592,596,607,611,624,628],[147,555,336],{"id":335},[152,557,558],{},"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.",[152,560,561],{},"Globe Satellites shows that ballet on a 3D globe, refreshed every sixty seconds.",[147,563,565],{"id":564},"features","Features",[442,567,568,574,580,586],{},[445,569,570,573],{},[246,571,572],{},"Filtering by category"," — starlink, oneweb, communications, navigation,\nstations, weather, Earth observation, science, other — clickable in the HUD,\nwith \"Show all\" \u002F \"Hide all\" buttons",[445,575,576,579],{},[246,577,578],{},"Hover tooltip"," — name, altitude and category for each satellite",[445,581,582,585],{},[246,583,584],{},"Logarithmic radial scale"," — visibly separates low, medium and\ngeostationary orbits instead of stacking them on top of each other",[445,587,588,591],{},[246,589,590],{},"Automatic WebSocket reconnection"," (exponential backoff), with a watchdog\nthat catches a frozen connection with no clean TCP close",[147,593,595],{"id":594},"computing-on-the-server","Computing on the server",[152,597,598,599,602,603,606],{},"The structural choice is to ",[246,600,601],{},"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 (",[469,604,605],{},"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.",[147,608,610],{"id":609},"network-resilience","Network resilience",[152,612,613,614,617,618,623],{},"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 ",[469,615,616],{},"\u002Fhealth"," endpoint, which pushes best-effort\n",[619,620,622],"a",{"href":621},"\u002Fen\u002Fprojects\u002Fvigie","Vigie"," monitoring for the refresh cycle.",[147,625,627],{"id":626},"the-hardware-constraint","The hardware constraint",[152,629,630,631,634,635,638],{},"The project is built for a ",[246,632,633],{},"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 ",[469,636,637],{},"InstancedMesh"," rather\nthan one object per satellite, and broadcasting one shared state rather than\ncomputing per connected client.",{"title":71,"searchDepth":282,"depth":282,"links":640},[641,642,643,644,645],{"id":335,"depth":282,"text":336},{"id":564,"depth":282,"text":565},{"id":594,"depth":282,"text":595},{"id":609,"depth":282,"text":610},{"id":626,"depth":282,"text":627},"\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.",[],[650,651,652],"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":540,"description":647},"https:\u002F\u002Fglobe.szmygiel.com","projets\u002Fglobe-satellites",[16,660,26,25,392,51,661],"Globe.gl","Raspberry Pi","TS8Sx0gdo3anp3sYPh3mls3nfLiVHR3E-exVBbiOAZM",{"id":128,"title":129,"architecture":664,"body":669,"cover":288,"description":289,"extension":290,"featured":124,"gallery":767,"highlights":770,"interface":124,"meta":771,"navigation":124,"path":303,"rang":282,"repo":71,"role":304,"seo":772,"site":306,"status":307,"stem":308,"tags":773,"type":311,"year":312,"__hash__":313},[665,666,667,668],{"label":132,"description":133},{"label":135,"description":136},{"label":138,"description":139},{"label":141,"description":142},{"type":144,"value":670,"toc":761},[671,673,675,677,679,733,735,737,741,745,749,753,757,759],[147,672,150],{"id":149},[152,674,154],{},[152,676,157],{},[147,678,161],{"id":160},[163,680,681,691],{},[166,682,683],{},[169,684,685,687,689],{},[172,686,174],{},[172,688,177],{},[172,690,180],{},[182,692,693,701,709,717,725],{},[169,694,695,697,699],{},[187,696,189],{},[187,698,192],{},[187,700,195],{},[169,702,703,705,707],{},[187,704,200],{},[187,706,203],{},[187,708,206],{},[169,710,711,713,715],{},[187,712,211],{},[187,714,214],{},[187,716,206],{},[169,718,719,721,723],{},[187,720,221],{},[187,722,224],{},[187,724,206],{},[169,726,727,729,731],{},[187,728,231],{},[187,730,192],{},[187,732,195],{},[152,734,238],{},[147,736,242],{"id":241},[152,738,739,249],{},[246,740,248],{},[152,742,743,255],{},[246,744,254],{},[152,746,747,261],{},[246,748,260],{},[152,750,751,267],{},[246,752,266],{},[152,754,755,273],{},[246,756,272],{},[147,758,277],{"id":276},[152,760,280],{},{"title":71,"searchDepth":282,"depth":282,"links":762},[763,764,765,766],{"id":149,"depth":282,"text":150},{"id":160,"depth":282,"text":161},{"id":241,"depth":282,"text":242},{"id":276,"depth":282,"text":277},[768,769],{"url":293,"caption":294},{"url":296,"caption":297},[299,300,301],{},{"title":129,"description":289},[16,20,28,35,34,310],{"id":775,"title":776,"architecture":777,"body":789,"cover":71,"description":863,"extension":290,"featured":118,"gallery":864,"highlights":865,"interface":124,"meta":869,"navigation":124,"path":870,"rang":871,"repo":71,"role":872,"seo":873,"site":71,"status":531,"stem":874,"tags":875,"type":56,"year":312,"__hash__":878},"projets_en\u002Fprojets\u002Frootine.md","Rootine",[778,780,783,786],{"label":329,"description":779},"Habits and check-offs stored locally with SwiftData, with no external dependency.",{"label":781,"description":782},"Check-off","A Yesterday \u002F Today toggle to check off retroactively, without breaking streaks.",{"label":784,"description":785},"Visualisation","Monthly heatmap calendar where colour intensity reflects the completion rate.",{"label":787,"description":788},"Interface","Spring animations and haptic feedback on every check, in SwiftUI.",{"type":144,"value":790,"toc":857},[791,797,799,815,817,823,829,835,841,845,848,851,854],[792,793,794],"blockquote",{},[152,795,796],{},"Habits are the roots of who you become.",[147,798,336],{"id":335},[152,800,801,802,806,807,810,811,814],{},"Rootine — a play on ",[803,804,805],"em",{},"root"," and ",[803,808,809],{},"routine"," — is a habit tracker built around one\nsimple principle: ",[246,812,813],{},"consistency beats intensity",". Ten minutes of exercise every\nday is worth more than one two-hour session a week.",[147,816,565],{"id":564},[152,818,819,822],{},[246,820,821],{},"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.",[152,824,825,828],{},[246,826,827],{},"Routines"," — creating, editing and deleting the habits you track.",[152,830,831,834],{},[246,832,833],{},"Calendar"," — a monthly heatmap where colour intensity reflects that day's\ncompletion rate. Consistency becomes visible at a glance.",[152,836,837,840],{},[246,838,839],{},"Statistics"," — current streaks, completion rate, trends over time.",[147,842,844],{"id":843},"the-stance","The stance",[152,846,847],{},"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.",[152,849,850],{},"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.",[147,852,348],{"id":853},"stack",[152,855,856],{},"SwiftUI and SwiftData, with no external dependency. All data stays on the device.",{"title":71,"searchDepth":282,"depth":282,"links":858},[859,860,861,862],{"id":335,"depth":282,"text":336},{"id":564,"depth":282,"text":565},{"id":843,"depth":282,"text":844},{"id":853,"depth":282,"text":348},"A minimal, forgiving iOS habit tracker — today's checklist, a heatmap calendar and consistency statistics.",[],[866,867,868],"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":776,"description":863},"projets\u002Frootine",[59,60,61,876,877],"Swift","Charts","-jczwI2MfId-BcdlMwAxDkg111FvQlpAM7nKRbRdCv8",{"id":880,"title":881,"architecture":882,"body":893,"cover":71,"description":930,"extension":290,"featured":118,"gallery":931,"highlights":932,"interface":124,"meta":936,"navigation":124,"path":937,"rang":938,"repo":71,"role":872,"seo":939,"site":71,"status":531,"stem":940,"tags":941,"type":56,"year":312,"__hash__":943},"projets_en\u002Fprojets\u002Fsofa.md","Sofa",[883,886,888,891],{"label":884,"description":885},"Search","TMDB API queries for title search and detail sheets.",{"label":138,"description":887},"Library and watch history persisted with SwiftData, readable offline.",{"label":889,"description":890},"Enrichment","Available streaming platforms pulled from TMDB.",{"label":787,"description":892},"Dark design system inspired by cinema auditoriums, in native SwiftUI.",{"type":144,"value":894,"toc":924},[895,897,900,902,909,912,916,919,921],[147,896,336],{"id":335},[152,898,899],{},"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.",[147,901,348],{"id":853},[152,903,904,905,908],{},"A ",[246,906,907],{},"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.",[152,910,911],{},"SwiftData handles local persistence, which makes the library readable offline —\nonly search and detail refreshes need the network.",[147,913,915],{"id":914},"design","Design",[152,917,918],{},"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.",[147,920,277],{"id":276},[152,922,923],{},"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":282,"depth":282,"links":925},[926,927,928,929],{"id":335,"depth":282,"text":336},{"id":853,"depth":282,"text":348},{"id":914,"depth":282,"text":915},{"id":276,"depth":282,"text":277},"A native iOS watchlist app for films and series — TMDB search, personal library, watch tracking and ratings.",[],[933,934,935],"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":881,"description":930},"projets\u002Fsofa",[59,60,61,942,876],"TMDB API","L9BjW5Ev8RnMN90W7J3oAsl5_rNEZpLJ4xhveeWZqgw",{"id":945,"title":946,"architecture":947,"body":958,"cover":1072,"description":1073,"extension":290,"featured":118,"gallery":1074,"highlights":1075,"interface":118,"meta":1079,"navigation":124,"path":1080,"rang":1081,"repo":71,"role":1082,"seo":1083,"site":71,"status":531,"stem":1084,"tags":1085,"type":31,"year":393,"__hash__":1090},"projets_en\u002Fprojets\u002Fthanos.md","Thanos",[948,950,953,956],{"label":31,"description":949},"Data Manager — fetching and caching historical series per exchange and timeframe.",{"label":951,"description":952},"Indicators","Custom Indicators — technical indicators implemented by hand rather than imported.",{"label":954,"description":955},"Backtest","Backtest Analysis — performance and risk metrics across five strategy families.",{"label":784,"description":957},"Plot Analysis — entries, exits and equity curves.",{"type":144,"value":959,"toc":1066},[960,962,965,969,1023,1026,1030,1056,1060,1063],[147,961,336],{"id":335},[152,963,964],{},"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\".",[147,966,968],{"id":967},"strategies-implemented","Strategies implemented",[163,970,971,981],{},[166,972,973],{},[169,974,975,978],{},[172,976,977],{},"Strategy",[172,979,980],{},"Principle",[182,982,983,991,999,1007,1015],{},[169,984,985,988],{},[187,986,987],{},"SMA",[187,989,990],{},"Simple moving average crossover",[169,992,993,996],{},[187,994,995],{},"EMA",[187,997,998],{},"Exponential moving average crossover",[169,1000,1001,1004],{},[187,1002,1003],{},"Momentum (RSI)",[187,1005,1006],{},"Entries on oversold \u002F overbought zones",[169,1008,1009,1012],{},[187,1010,1011],{},"SMA-EMA combined",[187,1013,1014],{},"Cross-confirmation between the two moving average families",[169,1016,1017,1020],{},[187,1018,1019],{},"TRIX",[187,1021,1022],{},"Triple exponential smoothing oscillator",[152,1024,1025],{},"They all share the same interface, which makes it possible to evaluate them on\nidentical ground — same data, same fees, same metrics.",[147,1027,1029],{"id":1028},"modules","Modules",[442,1031,1032,1038,1044,1050],{},[445,1033,1034,1037],{},[246,1035,1036],{},"Data Manager"," — fetching and caching historical series per exchange and timeframe",[445,1039,1040,1043],{},[246,1041,1042],{},"Custom Indicators"," — technical indicators implemented by hand rather than imported",[445,1045,1046,1049],{},[246,1047,1048],{},"Backtest Analysis"," — performance and risk metrics",[445,1051,1052,1055],{},[246,1053,1054],{},"Plot Analysis"," — visualising entries, exits and equity curves",[147,1057,1059],{"id":1058},"the-real-lesson","The real lesson",[152,1061,1062],{},"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.",[152,1064,1065],{},"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":282,"depth":282,"links":1067},[1068,1069,1070,1071],{"id":335,"depth":282,"text":336},{"id":967,"depth":282,"text":968},{"id":1028,"depth":282,"text":1029},{"id":1058,"depth":282,"text":1059},"\u002Fprojets\u002Fthanos-cover.svg","A research, backtesting and analysis platform for trading strategies on cryptocurrency markets.",[],[1076,1077,1078],"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":946,"description":1073},"projets\u002Fthanos",[25,1086,1087,1088,1089],"Pandas","NumPy","Backtesting","Data analysis","dSBxOYef84ONZdN6irpkgiOWjftugwl2o8W9AqvRZUY",{"id":1092,"title":622,"architecture":1093,"body":1104,"cover":1289,"description":1290,"extension":290,"featured":124,"gallery":1291,"highlights":1301,"interface":124,"meta":1306,"navigation":124,"path":1307,"rang":1308,"repo":71,"role":304,"seo":1309,"site":1310,"status":307,"stem":1311,"tags":1312,"type":311,"year":312,"__hash__":1313},"projets_en\u002Fprojets\u002Fvigie.md",[1094,1097,1099,1101],{"label":1095,"description":1096},"API","AdonisJS v6, TypeScript, Lucid — business logic and endpoints.",{"label":31,"description":1098},"PostgreSQL 17, progressive rollup (fine → hourly → daily) to keep retention affordable.",{"label":787,"description":1100},"Vue 3, Vite, Pinia — dashboard and public status page.",{"label":1102,"description":1103},"Exposure","Docker Compose on a VPS (Caddy, GitHub Actions), plus a read-only MCP server to query monitoring from the editor.",{"type":144,"value":1105,"toc":1281},[1106,1108,1111,1114,1118,1150,1153,1155,1202,1222,1226,1229,1243,1247,1254,1272,1274],[147,1107,415],{"id":414},[152,1109,1110],{},"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.",[152,1112,1113],{},"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.",[147,1115,1117],{"id":1116},"what-vigie-does","What Vigie does",[442,1119,1120,1126,1132,1138,1144],{},[445,1121,1122,1125],{},[246,1123,1124],{},"Visit analytics"," with no cookie and no persistent identifier — no consent banner required",[445,1127,1128,1131],{},[246,1129,1130],{},"Uptime monitoring",": regular HTTP probes with history and uptime calculation",[445,1133,1134,1137],{},[246,1135,1136],{},"Scheduled-job supervision",": a cron that hasn't checked in within its window raises an alert",[445,1139,1140,1143],{},[246,1141,1142],{},"Error tracking"," for the application errors reported by the connected projects",[445,1145,1146,1149],{},[246,1147,1148],{},"A public status page",", fed by the same data as the internal dashboard",[152,1151,1152],{},"Every monitored project has its own API key, and never sees another project's data.",[147,1154,343],{"id":342},[163,1156,1157,1167],{},[166,1158,1159],{},[169,1160,1161,1164],{},[172,1162,1163],{},"Layer",[172,1165,1166],{},"Choice",[182,1168,1169,1176,1184,1191],{},[169,1170,1171,1173],{},[187,1172,1095],{},[187,1174,1175],{},"AdonisJS v6, TypeScript, Lucid",[169,1177,1178,1181],{},[187,1179,1180],{},"Database",[187,1182,1183],{},"PostgreSQL 17",[169,1185,1186,1188],{},[187,1187,787],{},[187,1189,1190],{},"Vue 3, Vite, Pinia, TypeScript",[169,1192,1193,1196],{},[187,1194,1195],{},"Deployment",[187,1197,1198,1199],{},"Docker Compose on a VPS, Caddy in front, GitHub Actions on ",[469,1200,1201],{},"main",[152,1203,1204,1205,1208,1209,1208,1212,1208,1215,1208,1218,1221],{},"Everything runs in containers — ",[469,1206,1207],{},"postgres",", ",[469,1210,1211],{},"redis",[469,1213,1214],{},"api",[469,1216,1217],{},"worker",[469,1219,1220],{},"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.",[147,1223,1225],{"id":1224},"the-client-side-agent","The client-side agent",[152,1227,1228],{},"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.",[152,1230,1231,1232,806,1235,1238,1239,1242],{},"It wraps ",[469,1233,1234],{},"pushState",[469,1236,1237],{},"replaceState"," to catch navigations in a single-page\napp, then restores the original methods: nothing is permanently altered for the\nhost application. A ",[469,1240,1241],{},"@vigie\u002Fjs"," plugin hooks straight into the Vue 3 router\ninstead of guessing from history events.",[147,1244,1246],{"id":1245},"an-mcp-server-to-stay-in-the-editor","An MCP server, to stay in the editor",[152,1248,1249,1250,1253],{},"What's broken? Is this error new? Did last night's cron run? Vigie exposes six\nread-only tools over ",[246,1251,1252],{},"MCP"," to answer those questions without opening the\ndashboard.",[152,1255,1256,1257,1260,1261,1208,1264,1267,1268,1271],{},"Two deliberate choices: ",[246,1258,1259],{},"no writes at all"," — an assistant that reads gets the\ndiagnosis wrong, an assistant that writes gets production wrong; and ",[246,1262,1263],{},"internal\ncodes translated on the way out",[469,1265,1266],{},"cause: \"silent agent\""," rather than\n",[469,1269,1270],{},"reason: 5",", because a model facing an unknown code doesn't stop, it guesses.",[147,1273,277],{"id":276},[152,1275,1276,1277,1280],{},"The hard part wasn't collection but ",[246,1278,1279],{},"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":282,"depth":282,"links":1282},[1283,1284,1285,1286,1287,1288],{"id":414,"depth":282,"text":415},{"id":1116,"depth":282,"text":1117},{"id":342,"depth":282,"text":343},{"id":1224,"depth":282,"text":1225},{"id":1245,"depth":282,"text":1246},{"id":276,"depth":282,"text":277},"\u002Fprojets\u002Fvigie-dashboard.png","Unified monitoring for my web projects — cookieless analytics, uptime checks, scheduled-job tracking and a public status page.",[1292,1295,1298],{"url":1293,"caption":1294},"\u002Fprojets\u002Fvigie-erreurs.png","Application errors — grouped by fingerprint, with latest occurrences and stack trace.",{"url":1296,"caption":1297},"\u002Fprojets\u002Fvigie-taches.png","Scheduled jobs — state, run history and an incident when one fails.",{"url":1299,"caption":1300},"\u002Fprojets\u002Fvigie-disponibilite.png","Uptime — URL checks with response time and 30-day history.",[1302,1303,1304,1305],"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":622,"description":1290},"https:\u002F\u002Fvigie.watch","projets\u002Fvigie",[28,16,18,34,37,51,1252],"RiBHY7jRZ-V3wJoap5kgVKpqBMn-pw7QB4wK5yabSNI",{"left":116,"top":116,"width":117,"height":117,"rotate":116,"vFlip":118,"hFlip":118,"body":1315},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M5 12h14m-7-7l7 7l-7 7\"\u002F>",1788530203853]