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