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