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