[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fFqZj5xfff7RmekG2CxhUR9pWPqDj-SDeVZgPp6WdMms":3},[4,23,42,64,77,96,112,129],{"id":5,"slug":6,"titleFr":7,"titleEn":8,"titleKm":9,"descriptionFr":10,"descriptionEn":11,"descriptionKm":12,"tags":13,"githubUrl":19,"demoUrl":20,"imageUrl":19,"featured":21,"createdAt":22},56,"play-arcade","play.chetana.fr — L'arcade de Chet & Lys","play.chetana.fr — Chet & Lys Arcade","play.chetana.fr — ល្បែង Chet & Lys","Une arcade de jeux web où ma femme et moi sommes les héros — des personnages manga générés par IA. Un projet fait pour le plaisir, la fête, et un peu la démo technique.\n\n## Le concept\n\n**Chet & Lys Adventures** : plusieurs jeux dans un seul hub — un beat'em up, une course en scooter dans les rues de Phnom Penh, et une série de mini-jeux. Le tout jouable directement dans le navigateur, sans installation.\n\n## Les sprites, générés par IA\n\nNos avatars manga et les décors ont été produits avec **Gemini 2.5 Flash Image** (\"Nano Banana\"), avec un travail de cohérence de personnage (mêmes visages d'un sprite à l'autre via des images de référence).\n\n## La stack\n\nHTML5 Canvas + nginx, conteneurisé et déployé en **Serverless Container Scaleway** (fr-par, scale-to-zero). Sprites servis en statique, zéro backend.","A web game arcade where my wife and I are the heroes — AI-generated manga characters. A project made for fun, celebration, and a bit of technical showcase.\n\n## The concept\n\n**Chet & Lys Adventures**: several games in a single hub — a beat'em up, a scooter ride through the streets of Phnom Penh, and a series of mini-games. All playable straight in the browser, no install.\n\n## The sprites, AI-generated\n\nOur manga avatars and the backdrops were produced with **Gemini 2.5 Flash Image** (\"Nano Banana\"), with character-consistency work (same faces from one sprite to the next via reference images).\n\n## The stack\n\nHTML5 Canvas + nginx, containerized and deployed as a **Scaleway Serverless Container** (fr-par, scale-to-zero). Sprites served statically, zero backend.","Une arcade de jeux web où ma femme et moi sommes les héros — des personnages manga générés par IA. Un projet fait pour le plaisir, la fête, et un peu la démo technique.",[14,15,16,17,18],"HTML5 Canvas","Gemini (Nano Banana)","nginx","Scaleway","jeu",null,"https://play.chetana.fr",true,"2026-07-06T17:21:54.974895",{"id":24,"slug":25,"titleFr":26,"titleEn":27,"titleKm":28,"descriptionFr":29,"descriptionEn":30,"descriptionKm":31,"tags":32,"githubUrl":19,"demoUrl":40,"imageUrl":19,"featured":21,"createdAt":41},55,"chetana-learn","learn.chetana.fr — Ne jamais perdre pied","learn.chetana.fr — Never fall behind","learn.chetana.fr — កុំបាត់បង់ជំហាន","Le métier d'ingénieur évolue plus vite que jamais. **learn.chetana.fr** est né d'une conviction simple : pour ne pas perdre pied, il faut continuer à nourrir son cerveau — de façon structurée, pas au hasard des onglets ouverts.\n\n## La motivation\n\nVoir le monde avancer si vite peut donner le vertige. Plutôt que de subir, j'ai voulu me construire un espace à moi pour apprendre en continu, garder l'élan, et transformer la veille passive en compétences durables.\n\n## Au programme\n\n- Premier cours : **Backend Developer → ML Engineer** — 10 modules, ~60 leçons\n- Des **analogies backend** pour rendre le ML accessible quand on vient du dev\n- L'ambition, à terme, d'**ouvrir ces cours à d'autres** ingénieurs\n\n## La stack\n\nNuxt 3 + Nuxt Content (front, i18n fr/en) · API **Rust / Axum** + sqlx (progression) · **Serverless SQL** · auth Logto — 100% **souverain sur Scaleway**, développé en **TDD**.","The engineering craft evolves faster than ever. **learn.chetana.fr** was born from a simple conviction: to avoid falling behind, you have to keep feeding your brain — in a structured way, not at the mercy of open browser tabs.\n\n## The motivation\n\nWatching the world move this fast can be dizzying. Rather than endure it, I wanted my own space to learn continuously, keep the momentum, and turn passive reading into lasting skills.\n\n## What's inside\n\n- First course: **Backend Developer → ML Engineer** — 10 modules, ~60 lessons\n- **Backend analogies** to make ML approachable when you come from dev\n- The long-term ambition to **open these courses to other** engineers\n\n## The stack\n\nNuxt 3 + Nuxt Content (front, fr/en i18n) · **Rust / Axum** + sqlx API (progress) · **Serverless SQL** · Logto auth — 100% **sovereign on Scaleway**, built with **TDD**.","វេទិកាសិក្សាផ្ទាល់ខ្លួន កើតចេញពីការសង្កេតសាមញ្ញ៖ វិស័យវិស្វកម្មវិវឌ្ឍលឿនជាងពេលណាៗ ដូច្នេះត្រូវបន្តចិញ្ចឹមខួរក្បាល។ ចាប់ផ្ដើមដោយការផ្លាស់ប្ដូរ Backend Developer → ML Engineer។ Stack៖ Nuxt 3, Rust/Axum, Serverless SQL, Logto — លើ Scaleway។",[33,34,35,36,37,17,38,39],"Nuxt 3","Nuxt Content","Rust / Axum","Serverless SQL","Logto","TDD","Machine Learning","https://learn.chetana.fr","2026-07-06T17:06:52.139514",{"id":43,"slug":44,"titleFr":45,"titleEn":46,"titleKm":47,"descriptionFr":48,"descriptionEn":49,"descriptionKm":50,"tags":51,"githubUrl":61,"demoUrl":62,"imageUrl":19,"featured":21,"createdAt":63},54,"chetaku-rs","chetaku-rs — Backend Rust personnel","chetaku-rs — Personal Rust Backend","chetaku-rs — Backend Rust ផ្ទាល់ខ្លួន","Backend de mon portfolio écrit en Rust (Axum 0.8 + sqlx + Tokio), déployé sur GCP Cloud Run avec base Neon PostgreSQL. Intégrations : Strava API (cyclisme, course, natation), Jikan (anime), RAWG (jeux vidéo), TMDB (films/séries). Frontend Nuxt 4 + TypeScript consommant l'API. Cache intelligent avec TTL 30s en base (stats_cache pattern).","My portfolio backend written in Rust (Axum 0.8 + sqlx + Tokio), deployed on GCP Cloud Run with Neon PostgreSQL. Integrations: Strava API (cycling, running, swimming), Jikan (anime), RAWG (video games), TMDB (movies/series). Nuxt 4 + TypeScript frontend consuming the API. Smart caching with 30s TTL in database (stats_cache pattern).","Backend portfolio របស់ខ្ញុំសរសេរជា Rust (Axum 0.8 + sqlx + Tokio) ដាក់ពង្រាយនៅ GCP Cloud Run ជាមួយ Neon PostgreSQL។ ការរួមបញ្ចូល៖ Strava API Jikan RAWG TMDB។ Frontend Nuxt 4 + TypeScript។",[52,53,54,55,56,57,58,59,60],"Rust","Axum","Tokio","PostgreSQL","GCP Cloud Run","Strava API","Jikan","RAWG","TMDB","https://github.com/chetana/chetaku-rs","https://chetana.dev","2026-04-10T23:25:12.207445",{"id":65,"slug":66,"titleFr":67,"titleEn":68,"titleKm":69,"descriptionFr":70,"descriptionEn":71,"descriptionKm":19,"tags":72,"githubUrl":61,"demoUrl":75,"imageUrl":19,"featured":21,"createdAt":76},53,"medialist","Médiathèque","Media Library","បណ្ណាល័យ","## Pourquoi ce projet ?\n\nLa Médiathèque est née d'une envie simple en tant qu'Engineering Manager : construire quelque chose de suffisamment complexe pour être réellement formateur, mais suffisamment personnel pour rester motivant sur la durée.\n\nTracker des animés, jeux, films et séries, ça semble anodin. Mais dès qu'on creuse, les problèmes intéressants s'accumulent : comment agréger des données de trois APIs aux structures différentes ? Comment modéliser un suivi d'épisodes qui fonctionne aussi bien pour 12 épisodes que pour 1 000 ? Comment produire des statistiques personnelles qui révèlent quelque chose de vrai sur vos préférences, plutôt que juste compter des entrées ? Et quel rôle donner à l'IA dans l'enrichissement de chaque fiche ?\n\nCe sont des problèmes d'ingénierie réels — pas des exercices de formation, mais des questions auxquelles il faut répondre si on veut que l'outil soit utile.\n\n---\n\n## Architecture : deux couches distinctes\n\nLe projet repose sur une séparation nette entre le stockage des données et leur présentation.\n\n### chetaku-rs — le backend Rust\n\n`chetaku-rs` est une API REST écrite en **Rust avec Axum**, déployée sur **Google Scaleway** (serverless, région Europe-West1). Elle gère l'intégralité des données persistantes :\n\n- **Base de données** : PostgreSQL (Neon serverless)\n- **ORM** : SQLx (requêtes paramétrées, validation SQL au build)\n- **Authentification** : clé API statique (`x-api-key`) pour les opérations d'écriture\n- **CORS** : restreint à `chetana.fr` et `localhost:3000`\n\nLes routes principales :\n\n```\nGET    /media                      → liste paginée et filtrée\nGET    /media/{type}/{externalId}  → entrée unique par type + ID externe\nPATCH  /media/{id}                 → mise à jour (status, score, notes, épisodes)\nDELETE /media/{id}                 → suppression (clé API requise)\nGET    /stats                      → statistiques globales pondérées\nPOST   /sync/anime                 → synchronisation depuis MyAnimeList\nPOST   /sync/game                  → synchronisation depuis RAWG\nPOST   /sync/movie                 → synchronisation depuis TMDB\nPOST   /sync/series                → synchronisation depuis TMDB\n```\n\n### chetana-dev — le frontend Nuxt 3\n\nLe frontend est intégré dans le portfolio **chetana.fr** (Nuxt 3 / Nitro). Il joue le rôle de couche d'orchestration : il récupère les données stockées depuis `chetaku-rs`, puis les enrichit à la volée en interrogeant les APIs tierces selon le type de média.\n\n---\n\n## Modèle de données\n\nChaque entrée dans `media_entries` stocke les données de suivi personnel — pas les métadonnées publiques, qui restent côté APIs :\n\n| Champ | Type | Description |\n| --- | --- | --- |\n| `media_type` | TEXT | `anime` / `game` / `movie` / `series` |\n| `external_id` | TEXT | ID dans l'API source (MAL ID, RAWG slug, TMDB ID) |\n| `status` | TEXT | `watching` / `completed` / `plan_to_watch` / etc. |\n| `score` | SMALLINT | Note personnelle (1–10), nullable |\n| `episodes_watched` | INTEGER | Épisodes vus (anime et séries) |\n| `playtime_hours` | INTEGER | Heures jouées (jeux) |\n| `genres` | TEXT[] | Genres (dénormalisés pour les requêtes stats) |\n| `creator` | TEXT | Studio, développeur, réalisateur ou showrunner |\n| `notes` | TEXT | Notes personnelles libres |\n\nLa donnée enrichie — synopsis, cast, épisodes, captures d'écran — n'est jamais stockée. Elle est fetched à la demande sur la page de détail, ce qui maintient la base légère et les APIs comme source de vérité.\n\n---\n\n## Orchestration multi-API : le vrai problème intéressant\n\nChaque type de média a sa propre source de données, avec des structures différentes et des contraintes différentes :\n\n### Anime — Jikan (MyAnimeList)\nJikan retourne synopsis, score, studios, liste d'épisodes avec flags `filler` et `recap`, et trailer YouTube. La pagination des épisodes (100 par page) impose de gérer le cas `has_next_page`.\n\nLes arcs narratifs sont **hardcodés côté serveur** dans `server/utils/anime-arcs.ts` — un objet indexé par MAL ID. Alternative fragile : scraper un wiki. Alternative choisie : données stables, contrôlées, maintenables.\n\n### Jeux — RAWG\nRAWG fournit description, score Metacritic, équipes de développement, éditeurs, captures d'écran in-game. L'identifiant externe est un **slug** (texte), pas un entier — ce qui impose un typage cohérent dans toute la chaîne.\n\n### Films & Séries — TMDB\nTMDB pose le défi le plus intéressant pour les séries : récupérer la liste complète des épisodes impose un appel **par saison**, en parallèle, avec gestion des séries à 15+ saisons. La solution : `Promise.allSettled` sur un maximum de 15 saisons, avec dégradation gracieuse si un appel échoue.\n\nPour les films, TMDB fournit aussi le cast (top 10 avec photos), le réalisateur, le tagline et la durée — des données qui rendent chaque fiche nettement plus riche qu'une simple jaquette + score.\n\n---\n\n## Suivi d'épisodes et logique \"vous êtes ici\"\n\nSuivre des épisodes de manière significative est plus complexe qu'un simple compteur. L'interface affiche :\n\n- **Barre de progression** globale (épisodes vus / total)\n- **Indicateur par arc** (anime) : quels arcs sont complétés, en cours, non commencés\n- **Indicateur par saison** (séries) : quelle saison correspond à l'épisode actuel\n\nPour les saisons, l'algorithme calcule un **offset cumulatif** : la somme des épisodes de toutes les saisons précédentes. Si j'ai vu 45 épisodes et que les saisons font respectivement 10, 13 et 26 épisodes, je suis en saison 3, épisode 22. Ce calcul est fait côté client sur les données TMDB, sans aucun appel supplémentaire.\n\n---\n\n## Statistiques pondérées : le love_score\n\nL'endpoint `/stats` calcule des métriques avancées directement en SQL, en parallèle avec `tokio::join!` :\n\n- **Distribution des scores** : histogramme 1–10 par type de média\n- **Genres préférés** : pondérés par `love_score = COUNT(*) × AVG(score)` — un genre vu 12 fois avec une note moyenne de 9.2 est mieux classé qu'un genre vu 30 fois avec une note de 5.8\n- **Studios / Devs favoris** : top 6 par fréquence puis par note moyenne\n- **Statuts** : répartition watching / completed / plan_to_watch\n\nLe `love_score` est la métrique centrale. Un simple compteur ne dit pas grand chose — il reflète l'exposition, pas l'appréciation. Le `love_score` force une balance entre fréquence et qualité perçue, ce qui donne un profil de préférences nettement plus honnête.\n\n---\n\n## Pourquoi Rust ?\n\nRust n'était pas le choix pragmatique ici — Node.js aurait suffi. C'était un choix **délibéré de montée en compétence**.\n\nEn tant qu'Engineering Manager, je suis régulièrement amené à évaluer des choix d'architecture impliquant Rust : performance critique, sécurité mémoire, workloads embarqués. Mais recommander ou challenger un choix Rust sans l'avoir pratiqué soi-même sur un vrai projet reste une position fragile. Ce projet était l'occasion de combler cet écart.\n\nLe borrow checker, les lifetimes, le modèle d'ownership, les traits async — tout ça ne se comprend vraiment qu'en les rencontrant sur du code réel, pas en lisant de la documentation. Après ce projet, je peux discuter des frictions réelles de Rust avec mes équipes à partir d'une expérience concrète, pas d'une lecture.\n\nRésultat opérationnel : `chetaku-rs` tourne sur Scaleway Free Tier, démarre en sous-seconde, consomme ~15 Mo de RAM, et n'a eu aucun crash depuis son déploiement. Le compilateur Rust a éliminé à la conception les classes entières de bugs qui auraient pu apparaître en production.\n\n---\n\n## Sécurité et accès\n\nLa médiathèque est **en lecture publique** : la liste et les fiches sont accessibles à tous. Les opérations d'écriture (ajout, édition, suppression) sont réservées au propriétaire authentifié via Google OAuth. Les appels vers `chetaku-rs` transitent par une clé API interne jamais exposée côté client.","## Why this project?\n\nThe Media Library started from a simple goal as an Engineering Manager: build something complex enough to be genuinely instructive, but personal enough to stay motivating over time.\n\nTracking anime, games, movies and series sounds trivial. But as soon as you dig in, interesting problems accumulate: how do you aggregate data from three APIs with different structures? How do you model episode tracking that works equally well for 12 episodes and 1,000? How do you produce personal statistics that reveal something true about your preferences, rather than just counting entries? And what role should AI play in enriching each entry?\n\nThese are real engineering problems — not training exercises, but questions that need answers if the tool is going to be useful.\n\n---\n\n## Architecture: two distinct layers\n\nThe project relies on a clean separation between data storage and its presentation.\n\n### chetaku-rs — the Rust backend\n\n`chetaku-rs` is a REST API written in **Rust with Axum**, deployed on **Google Scaleway** (serverless, Europe-West1 region). It manages all persistent data:\n\n- **Database**: PostgreSQL (Neon serverless)\n- **ORM**: SQLx (parameterised queries, SQL validated at build time)\n- **Authentication**: static API key (`x-api-key`) for write operations\n- **CORS**: restricted to `chetana.fr` and `localhost:3000`\n\nMain routes:\n\n```\nGET    /media                      → paginated and filtered list\nGET    /media/{type}/{externalId}  → single entry by type + external ID\nPATCH  /media/{id}                 → update (status, score, notes, episodes)\nDELETE /media/{id}                 → deletion (API key required)\nGET    /stats                      → weighted global statistics\nPOST   /sync/anime                 → sync from MyAnimeList\nPOST   /sync/game                  → sync from RAWG\nPOST   /sync/movie                 → sync from TMDB\nPOST   /sync/series                → sync from TMDB\n```\n\n### chetana-dev — the Nuxt 3 frontend\n\nThe frontend lives inside the **chetana.fr** portfolio (Nuxt 3 / Nitro). It acts as an orchestration layer: it fetches stored data from `chetaku-rs`, then enriches it on the fly by querying third-party APIs depending on the media type.\n\n---\n\n## Data Model\n\nEach entry in `media_entries` stores personal tracking data — not public metadata, which stays on the API side:\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `media_type` | TEXT | `anime` / `game` / `movie` / `series` |\n| `external_id` | TEXT | ID in the source API (MAL ID, RAWG slug, TMDB ID) |\n| `status` | TEXT | `watching` / `completed` / `plan_to_watch` / etc. |\n| `score` | SMALLINT | Personal rating (1–10), nullable |\n| `episodes_watched` | INTEGER | Episodes watched (anime and series) |\n| `playtime_hours` | INTEGER | Hours played (games) |\n| `genres` | TEXT[] | Genres (denormalised for stats queries) |\n| `creator` | TEXT | Studio, developer, director or showrunner |\n| `notes` | TEXT | Free personal notes |\n\nEnriched data — synopsis, cast, episodes, screenshots — is never stored. It's fetched on demand on the detail page, keeping the database light and the APIs as the source of truth.\n\n---\n\n## Multi-API orchestration: the interesting problem\n\nEach media type has its own data source, with different structures and different constraints:\n\n### Anime — Jikan (MyAnimeList)\nJikan returns synopsis, score, studios, episode list with `filler` and `recap` flags, and YouTube trailer. Episode pagination (100 per page) requires handling the `has_next_page` case.\n\nNarrative arcs are **hardcoded server-side** in `server/utils/anime-arcs.ts` — an object indexed by MAL ID. The fragile alternative: scraping a wiki. The chosen alternative: stable, controlled, maintainable data.\n\n### Games — RAWG\nRAWG provides description, Metacritic score, development teams, publishers, in-game screenshots. The external identifier is a **slug** (text), not an integer — requiring consistent typing throughout the chain.\n\n### Movies & Series — TMDB\nTMDB poses the most interesting challenge for series: retrieving the complete episode list requires one call **per season**, in parallel, handling series with 15+ seasons. The solution: `Promise.allSettled` over a maximum of 15 seasons, with graceful degradation if a call fails.\n\nFor movies, TMDB also provides cast (top 10 with photos), director, tagline and runtime — data that makes each entry significantly richer than a cover image and a score.\n\n---\n\n## Episode tracking and the \"you are here\" logic\n\nTracking episodes meaningfully is more complex than a simple counter. The interface shows:\n\n- **Global progress bar** (episodes watched / total)\n- **Per-arc indicator** (anime): which arcs are completed, in progress, not started\n- **Per-season indicator** (series): which season the current episode falls in\n\nFor seasons, the algorithm calculates a **cumulative offset**: the sum of all episodes in previous seasons. If you've watched 45 episodes and the seasons have 10, 13 and 26 episodes respectively, you're in season 3, episode 22. This is computed client-side from TMDB data, with no additional API call.\n\n---\n\n## Weighted statistics: the love_score\n\nThe `/stats` endpoint calculates advanced metrics directly in SQL, in parallel with `tokio::join!`:\n\n- **Score distribution**: 1–10 histogram per media type\n- **Favourite genres**: weighted by `love_score = COUNT(*) × AVG(score)` — a genre watched 12 times with an average rating of 9.2 ranks higher than one watched 30 times with a rating of 5.8\n- **Top studios / devs**: top 6 ranked by frequency then by average score\n- **Statuses**: breakdown of watching / completed / plan_to_watch\n\nThe `love_score` is the central metric. A simple count says little — it reflects exposure, not appreciation. The `love_score` forces a balance between frequency and perceived quality, producing a significantly more honest preference profile.\n\n---\n\n## Why Rust?\n\nRust wasn't the pragmatic choice here — Node.js would have been perfectly fine. It was a **deliberate skill-building decision**.\n\nAs an Engineering Manager, I'm regularly called on to evaluate architectural choices involving Rust: performance-critical paths, memory safety requirements, embedded workloads. But recommending or challenging a Rust decision without having shipped a real project in it myself remains a fragile position. This project was the opportunity to close that gap.\n\nThe borrow checker, lifetimes, ownership model, async traits — none of it is truly understood until you encounter it in real code, not documentation. After this project, I can discuss the real friction points of Rust with my teams from concrete experience, not from reading.\n\nOperational result: `chetaku-rs` runs on Scaleway Free Tier, starts in under a second, uses ~15 MB of RAM, and has had zero crashes since deployment. The Rust compiler eliminated entire classes of bugs at design time that would otherwise have surfaced in production.\n\n---\n\n## Security and access\n\nThe media library is **publicly readable**: the list and detail pages are accessible to everyone. Write operations (add, edit, delete) are restricted to the authenticated owner via Google OAuth. Calls from the frontend to `chetaku-rs` go through an internal API key never exposed to the client.",[52,53,55,33,60,58,59,17,73,74],"TypeScript","Engineering Manager","https://chetana.fr/passions/medialist","2026-03-08T13:44:14.752208",{"id":78,"slug":79,"titleFr":80,"titleEn":81,"titleKm":82,"descriptionFr":83,"descriptionEn":84,"descriptionKm":19,"tags":85,"githubUrl":93,"demoUrl":94,"imageUrl":19,"featured":21,"createdAt":95},50,"chet-health-strong","Suivi pompes quotidien","Daily Pushup Tracker","តាមដានកិច្ចការរាំងដៃប្រចាំថ្ងៃ","## L'idée\n\nDébut janvier 2026, je me suis lancé un défi simple : **faire des pompes tous les jours**. Pas un programme de musculation complexe — juste une habitude quotidienne, mesurable, avec un objectif clair. Et comme je suis développeur, j'ai naturellement construit un écosystème complet pour ça.\n\n## Le concept : gamification à la Duolingo\n\nL'inspiration vient directement de Duolingo et de sa mécanique de **streak** (série de jours consécutifs). Le principe est psychologiquement puissant : une fois qu'on a 30 jours de streak, on ne veut surtout pas casser la chaîne.\n\n## L'application web (portfolio)\n\nLe tracker est d'abord né comme un composant intégré au portfolio :\n- **Un stepper quotidien** : +5 / -5 pompes pour ajuster rapidement\n- **Un calendrier visuel** : chaque jour validé est coché, les jours manqués sont marqués\n- **Un compteur de streak** : le nombre de jours consécutifs sans interruption\n- **Des statistiques** : total, moyenne, progression mensuelle\n\n## L'application Android native\n\nPour rendre la validation encore plus simple au quotidien, j'ai développé une **application Android native en Kotlin** :\n- **Architecture MVVM** avec ViewModel, LiveData et Room pour le cache local\n- **Retrofit 2** pour la synchronisation avec le backend\n- **Widget home screen** : un widget qui affiche le streak et le statut du jour directement sur l'écran d'accueil — impossible de l'ignorer en allumant le téléphone\n- **WorkManager** : synchronisation en arrière-plan toutes les 30 minutes\n- **Swipe to refresh** : pull-to-refresh pour forcer la synchronisation\n- **Light mode** : design épuré avec le même style beige/or que le portfolio\n\n## Authentification Google OAuth\n\nL'application a évolué d'un outil purement personnel vers un système **multi-utilisateur** avec authentification :\n- **Google Sign-In** via Credential Manager sur Android\n- **Vérification stateless** des Google ID Tokens côté backend (compatible serverless Vercel)\n- **Upsert automatique** des utilisateurs à la première connexion\n- **Données scopées** : chaque utilisateur ne voit que ses propres données\n- Pas de sessions, pas de cookies — juste un token dans le header `Authorization: Bearer`\n\n## Progression\n\n- **Janvier 2026** : objectif de 20 pompes/jour — phase d'installation de l'habitude\n- **Février 2026** : passage à 25 pompes/jour — le corps s'adapte, on monte la barre\n- L'objectif augmentera progressivement au fil des mois\n\n## Stack technique\n\n- **Backend** : Nuxt 3 / Nitro, Drizzle ORM, PostgreSQL Neon serverless, déployé sur Vercel\n- **Auth** : Google OAuth 2.0 (google-auth-library)\n- **Android** : Kotlin, MVVM, Retrofit 2, Room 2.6, Credential Manager, WorkManager\n- **Web** : composant Vue avec stepper interactif et calendrier responsive","## The Idea\n\nEarly January 2026, I set myself a simple challenge: **do pushups every day**. Not a complex workout program — just a daily, measurable habit with a clear goal. And since I'm a developer, I naturally built a complete ecosystem for it.\n\n## The Concept: Duolingo-Style Gamification\n\nThe inspiration comes directly from Duolingo and its **streak** mechanics (consecutive day series). The principle is psychologically powerful: once you have a 30-day streak, you really don't want to break the chain.\n\n## The Web App (Portfolio)\n\nThe tracker was first born as a component integrated into the portfolio:\n- **A daily stepper**: +5 / -5 pushups for quick adjustment\n- **A visual calendar**: each validated day is checked, missed days are marked\n- **A streak counter**: consecutive days without interruption\n- **Statistics**: total, average, monthly progression\n\n## The Native Android App\n\nTo make daily validation even simpler, I built a **native Android app in Kotlin**:\n- **MVVM architecture** with ViewModel, LiveData and Room for local caching\n- **Retrofit 2** for backend synchronization\n- **Home screen widget**: a widget that displays the streak and today's status right on the home screen — impossible to ignore when unlocking the phone\n- **WorkManager**: background sync every 30 minutes\n- **Swipe to refresh**: pull-to-refresh to force sync\n- **Light mode**: clean design matching the portfolio's beige/gold style\n\n## Google OAuth Authentication\n\nThe app evolved from a purely personal tool into a **multi-user system** with authentication:\n- **Google Sign-In** via Credential Manager on Android\n- **Stateless verification** of Google ID Tokens on the backend (Vercel serverless compatible)\n- **Automatic upsert** of users on first login\n- **Scoped data**: each user only sees their own data\n- No sessions, no cookies — just a token in the `Authorization: Bearer` header\n\n## Progression\n\n- **January 2026**: target of 20 pushups/day — habit installation phase\n- **February 2026**: up to 25 pushups/day — the body adapts, we raise the bar\n- The target will gradually increase over the months\n\n## Tech Stack\n\n- **Backend**: Nuxt 3 / Nitro, Drizzle ORM, PostgreSQL Neon serverless, deployed on Vercel\n- **Auth**: Google OAuth 2.0 (google-auth-library)\n- **Android**: Kotlin, MVVM, Retrofit 2, Room 2.6, Credential Manager, WorkManager\n- **Web**: Vue component with interactive stepper and responsive calendar",[86,87,88,89,90,91,92],"Vue","Nuxt","Kotlin","Android","OAuth","Health","Gamification","https://github.com/chetana/dailypushup","https://chetana.fr/projects/health","2026-03-07T00:52:45.036875",{"id":97,"slug":98,"titleFr":99,"titleEn":99,"titleKm":99,"descriptionFr":100,"descriptionEn":101,"descriptionKm":19,"tags":102,"githubUrl":19,"demoUrl":110,"imageUrl":19,"featured":21,"createdAt":111},48,"babel-duo","PolyGloChet","## Qu'est-ce que PolyGloChet ?\n\nPolyGloChet est une application de messagerie bilingue conçue pour faciliter l'apprentissage du français et du khmer entre deux personnes distantes. Chaque message envoyé est automatiquement analysé, corrigé et traduit dans les trois langues par Gemini AI — sans interrompre la conversation.\n\n## Correction grammaticale en temps réel\n\nDès que l'utilisateur commence à taper, un debounce d'une seconde déclenche une analyse Gemini :\n\n- **Détection automatique** de la langue du message (FR, EN ou KH)\n- **Suggestion de correction** affichée dans une popup inline avant l'envoi\n- **Explication pédagogique** : Gemini justifie chaque modification dans la langue natale de l'auteur\n- L'utilisateur peut **accepter** la correction (texte remplacé automatiquement), la **refuser** ou l'**ignorer**\n- Si la phrase est correcte, Gemini répond simplement \"Parfait !\" avec un encouragement\n\n## Leçons granulaires — une entrée par faute\n\nContrairement à une correction globale, PolyGloChet décompose chaque message en **autant de leçons qu'il y a de fautes** :\n\n- Exemple : \"je veut manger du riz a la maison\" → 2 leçons distinctes : \"veut → veux\" et \"a → à\"\n- Chaque leçon contient : l'original, la version corrigée, et l'explication grammaticale\n- Les leçons sont générées dans la **langue natale de l'auteur** (français pour Chet, khmer pour Lys)\n- Une leçon n'est générée que si une vraie faute est détectée — pas de bruit inutile\n\n## Historique des corrections (GCS)\n\nToutes les leçons sont persistées dans Google Cloud Storage (`chat/lessons.json`) au moment de l'envoi du message :\n\n- **Indépendant de la suggestion** : les leçons sont sauvegardées qu'on accepte ou refuse la correction\n- **Consultable à tout moment** via le panel 📖 dans l'application ou via la démo portfolio\n- Chaque entrée conserve : l'auteur, la langue, l'original, la correction, l'explication et la date\n- Ordre antichronologique (plus récent en premier)\n\n## Traductions trilingues automatiques\n\nChaque message est traduit en français, anglais et khmer par Gemini dès l'envoi :\n\n- Affichage des 3 traductions sous chaque bulle de message\n- **Flag de langue** (🇫🇷 🇬🇧 🇰🇭) indiquant la langue originale du message\n- Si la suggestion a déjà fourni les traductions, elles sont réutilisées — pas d'appel Gemini double\n- Traduction absente pour les messages trop courts (\u003C 2 caractères)\n\n## Reconnaissance vocale (VAD)\n\nL'envoi vocal repose sur une pipeline de détection d'activité vocale entièrement côté client :\n\n- **Silero VAD v5** via ONNX Runtime Web — détection de début/fin de parole sans serveur\n- Trois états visuels : ⏳ initialisation ONNX, 🔴 écoute active (pulsant), 🟢 voix détectée (pulsant)\n- Segments \u003C 0.8s filtrés (évite les bruits courts parasites)\n- Audio capturé en base64 → envoyé à Gemini (`geminiTranscribeAndTranslate`) → texte + traductions\n- Badge 🎤 affiché sur les bulles de messages vocaux\n\n## Coffre multimédia\n\nLes images et vidéos partagées dans le chat sont stockées dans Google Cloud Storage dans le même format que le coffre (`YYYY/MM/DD/filename`) :\n\n- **Compression automatique** côté client (WebP/JPEG, max 2048px via Canvas API)\n- **Signed URLs** pour upload et téléchargement sécurisé (expiration 1h, cache Map)\n- Les images partagées apparaissent automatiquement dans le coffre photo — pas de synchronisation nécessaire\n- Semaphore (max 3 requêtes concurrentes) pour éviter la saturation des signed URLs\n\n## Actions sur les messages\n\nUne barre d'actions apparaît sur sélection d'un message :\n\n- **Copier** : copie le texte original + toutes les traductions dans le presse-papiers\n- **TTS** (Text-To-Speech) : lecture à voix haute via Web Speech API en 🇫🇷 fr-FR, 🇬🇧 en-US ou 🇰🇭 km-KH\n- **Supprimer** : uniquement par l'auteur du message (vérifié côté backend)\n\n## Navigation et interface\n\n- **Navigation historique** : boutons ← → pour consulter les messages des jours précédents\n- **Double horloge** : heure de Paris (Europe/Paris) et de Phnom Penh (Asia/Phnom_Penh) sous chaque bulle\n- **Polling** toutes les 8s pour les messages du jour (nouveaux messages de l'autre côté)\n- **Interface bilingue** FR + Khmer dans toute l'UI\n- **PWA installable** : fonctionne hors-ligne, icône sur l'écran d'accueil\n\n## Architecture technique\n\n- **Frontend** : SvelteKit 5 + Svelte 5 runes (`$state`, `$derived`, `$effect`) + TypeScript\n- **Backend** : Nuxt 3 / Nitro sur Vercel (serverless)\n- **Stockage** : Google Cloud Storage — messages `chat/YYYY/MM/DD.json`, leçons `chat/lessons.json`\n- **AI** : Vertex AI Gemini 2.5 Flash — traduction, correction, transcription audio\n- **Auth** : Google Identity Services (FedCM) — JWT vérifié stateless côté backend\n- **VAD** : @ricky0123/vad-web + onnxruntime-web (Silero VAD v5)","## What is PolyGloChet?\n\nPolyGloChet is a bilingual messaging app designed to facilitate French and Khmer learning between two people at a distance. Every message sent is automatically analyzed, corrected and translated into three languages by Gemini AI — without interrupting the conversation.\n\n## Real-Time Grammar Correction\n\nAs soon as the user starts typing, a one-second debounce triggers a Gemini analysis:\n\n- **Automatic language detection** of the message (FR, EN or KH)\n- **Correction suggestion** displayed in an inline popup before sending\n- **Educational explanation**: Gemini justifies each change in the author's native language\n- The user can **accept** the correction (text replaced automatically), **reject** it or **ignore** it\n- If the sentence is correct, Gemini simply responds \"Perfect!\" with an encouragement\n\n## Granular Lessons — One Entry Per Error\n\nUnlike a global correction, PolyGloChet breaks down each message into **as many lessons as there are errors**:\n\n- Example: \"je veut manger du riz a la maison\" → 2 distinct lessons: \"veut → veux\" and \"a → à\"\n- Each lesson contains: the original, the corrected version, and the grammatical explanation\n- Lessons are generated in the **author's native language** (French for Chet, Khmer for Lys)\n- A lesson is only generated when a real error is detected — no unnecessary noise\n\n## Corrections History (GCS)\n\nAll lessons are persisted in Google Cloud Storage (`chat/lessons.json`) at message send time:\n\n- **Independent of the suggestion**: lessons are saved whether you accept or reject the correction\n- **Consultable at any time** via the 📖 panel in the app or through the portfolio demo\n- Each entry stores: author, language, original, correction, explanation and date\n- Reverse-chronological order (most recent first)\n\n## Automatic Trilingual Translations\n\nEvery message is translated into French, English and Khmer by Gemini upon sending:\n\n- All 3 translations displayed under each message bubble\n- **Language flag** (🇫🇷 🇬🇧 🇰🇭) indicating the original language of the message\n- If the suggestion already provided translations, they are reused — no double Gemini call\n- Translation skipped for very short messages (\u003C 2 characters)\n\n## Voice Recognition (VAD)\n\nVoice sending relies on a fully client-side voice activity detection pipeline:\n\n- **Silero VAD v5** via ONNX Runtime Web — start/end of speech detection with no server\n- Three visual states: ⏳ ONNX initializing, 🔴 active listening (pulsing), 🟢 voice detected (pulsing)\n- Segments \u003C 0.8s filtered out (avoids short background noises)\n- Audio captured as base64 → sent to Gemini (`geminiTranscribeAndTranslate`) → text + translations\n- 🎤 badge displayed on voice message bubbles\n\n## Media Vault\n\nImages and videos shared in chat are stored in Google Cloud Storage in the same format as the vault (`YYYY/MM/DD/filename`):\n\n- **Automatic client-side compression** (WebP/JPEG, max 2048px via Canvas API)\n- **Signed URLs** for secure upload and download (1h expiry, Map cache)\n- Shared images appear automatically in the photo vault — no synchronization needed\n- Semaphore (max 3 concurrent requests) to prevent signed URL saturation\n\n## Message Actions\n\nAn action bar appears when a message is selected:\n\n- **Copy**: copies the original text + all translations to clipboard\n- **TTS** (Text-To-Speech): reads aloud via Web Speech API in 🇫🇷 fr-FR, 🇬🇧 en-US or 🇰🇭 km-KH\n- **Delete**: only by the message author (verified server-side)\n\n## Navigation & Interface\n\n- **History navigation**: ← → buttons to browse previous days' messages\n- **Dual clock**: Paris time (Europe/Paris) and Phnom Penh time (Asia/Phnom_Penh) under each bubble\n- **Polling** every 8s for today's messages (new messages from the other side)\n- **Bilingual UI** FR + Khmer throughout the entire interface\n- **Installable PWA**: works offline, home screen icon\n\n## Technical Architecture\n\n- **Frontend**: SvelteKit 5 + Svelte 5 runes (`$state`, `$derived`, `$effect`) + TypeScript\n- **Backend**: Nuxt 3 / Nitro on Vercel (serverless)\n- **Storage**: Google Cloud Storage — messages `chat/YYYY/MM/DD.json`, lessons `chat/lessons.json`\n- **AI**: Vertex AI Gemini 2.5 Flash — translation, correction, audio transcription\n- **Auth**: Google Identity Services (FedCM) — stateless JWT verification server-side\n- **VAD**: @ricky0123/vad-web + onnxruntime-web (Silero VAD v5)",[103,104,105,73,106,107,108,109],"SvelteKit","Svelte 5","Gemini AI","GCS","PWA","VAD","Web Speech","https://chetana.fr/projects/polyglochet","2026-03-07T00:46:53.857159",{"id":113,"slug":114,"titleFr":115,"titleEn":116,"titleKm":117,"descriptionFr":118,"descriptionEn":119,"descriptionKm":120,"tags":121,"githubUrl":126,"demoUrl":127,"imageUrl":19,"featured":21,"createdAt":128},44,"chetana-dev","chetana.fr — Portfolio dynamique","chetana.fr — Dynamic Portfolio","chetana.fr — ផលប័ត្រថាមវន្ត","## Le projet\n\nCe site est mon portfolio personnel, mais aussi un terrain d'expérimentation technique. Plutôt que d'utiliser un template, j'ai voulu construire quelque chose de A à Z — du schema de base de données au déploiement continu.\n\n## Stack technique\n\n- **Frontend** : Nuxt 4 (Vue 3.5) + TypeScript — pour le SSR, le routing automatique et les composables\n- **Base de données** : Neon PostgreSQL (serverless) + Drizzle ORM — schema typé, migrations simples, connection pooling automatique\n- **Déploiement** : Vercel avec auto-deploy sur push to main — zéro config serveur\n- **SEO** : @nuxtjs/seo pour sitemap, robots.txt, schema.org (JSON-LD) sur chaque page\n\n## Fonctionnalités\n\n- **Trilingue** : français, anglais et khmer — avec un composable `useLocale()` maison et fallback FR\n- **Blog dynamique** : articles stockés en base avec rendu markdown, commentaires modérés, tags\n- **CV interactif** : page dédiée avec export PDF, alimentée par la base de données\n- **Formulaire de contact** : avec honeypot anti-spam et stockage en base\n- **Health tracker** : suivi quotidien de pompes style Duolingo avec streaks et calendrier\n\n## Architecture\n\nLe site suit une architecture full-stack unifiée grâce à Nitro (le moteur serveur de Nuxt). Les API REST sont des fichiers dans `server/api/`, le schema Drizzle définit les types partagés entre front et back, et tout est déployé comme une seule application serverless.\n\n## Pourquoi ce projet ?\n\nEn tant qu'Engineering Manager, je code moins au quotidien qu'avant. Ce portfolio me permet de garder la main sur les technologies modernes, d'expérimenter (Nuxt 4, Drizzle, Neon serverless), et de documenter mes réflexions via le blog. C'est aussi un exercice de product ownership : je suis à la fois le dev, le PM et l'utilisateur final.","## The Project\n\nThis site is my personal portfolio, but also a technical playground. Rather than using a template, I wanted to build something from scratch — from the database schema to continuous deployment.\n\n## Tech Stack\n\n- **Frontend**: Nuxt 4 (Vue 3.5) + TypeScript — for SSR, automatic routing and composables\n- **Database**: Neon PostgreSQL (serverless) + Drizzle ORM — typed schema, simple migrations, automatic connection pooling\n- **Deployment**: Vercel with auto-deploy on push to main — zero server config\n- **SEO**: @nuxtjs/seo for sitemap, robots.txt, schema.org (JSON-LD) on every page\n\n## Features\n\n- **Trilingual**: French, English and Khmer — with a custom `useLocale()` composable and FR fallback\n- **Dynamic blog**: articles stored in database with markdown rendering, moderated comments, tags\n- **Interactive CV**: dedicated page with PDF export, fed from the database\n- **Contact form**: with honeypot anti-spam and database storage\n- **Health tracker**: Duolingo-style daily pushup tracker with streaks and calendar\n\n## Architecture\n\nThe site follows a unified full-stack architecture thanks to Nitro (Nuxt's server engine). REST APIs are files in `server/api/`, the Drizzle schema defines shared types between front and back, and everything deploys as a single serverless application.\n\n## Why This Project?\n\nAs an Engineering Manager, I code less daily than I used to. This portfolio lets me keep my hands on modern technologies, experiment (Nuxt 4, Drizzle, Neon serverless), and document my thoughts through the blog. It's also a product ownership exercise: I'm the dev, the PM and the end user all at once.","ផលប័ត្រ/CV ផ្ទាល់ខ្លួនបង្កើតជាមួយ Nuxt 4, Neon PostgreSQL និង Drizzle ORM។ ដាក់ពង្រាយនៅ Vercel ជាមួយការគាំទ្រភាសា FR/EN/KM។",[122,73,123,124,125],"Nuxt 4","Neon","Drizzle","Vercel","https://github.com/chetana-dev/chetana-dev","https://chetana.fr","2026-03-07T00:46:44.319463",{"id":130,"slug":131,"titleFr":132,"titleEn":133,"titleKm":134,"descriptionFr":135,"descriptionEn":136,"descriptionKm":137,"tags":138,"githubUrl":19,"demoUrl":19,"imageUrl":19,"featured":21,"createdAt":128},45,"claude-code-skills","Claude Code Skills — Écosystème IA","Claude Code Skills — AI Ecosystem","Claude Code Skills — ប្រព័ន្ធអេកូ AI","## Le concept\n\nChez DJUST, j'ai développé un écosystème de **25+ skills personnalisés** pour Claude Code, transformant l'IA d'un simple assistant de code en un véritable membre de l'équipe. Chaque skill est un prompt spécialisé qui encode nos conventions, notre architecture et nos processus métier.\n\n## Cas d'usage concrets\n\n- **Code Review automatisée** : analyse le diff GitLab, vérifie le respect de nos conventions Spring Boot, détecte les problèmes de sécurité et de performance, génère un commentaire structuré\n- **Génération de tests E2E** : à partir d'un ticket Jira, génère les scénarios de test adaptés à notre framework, avec les fixtures et les assertions\n- **Briefing MEP** : compile automatiquement les changements de la release, identifie les risques, prépare le message Slack pour l'équipe\n- **Analyse de bugs** : à partir d'un stack trace ou d'un log, remonte la chaîne causale dans notre codebase multi-modules\n\n## Architecture MCP\n\nL'intégration repose sur le **Model Context Protocol** (MCP), qui permet à Claude Code d'interagir avec nos outils :\n\n- **Slack** : lecture des channels, envoi de messages, création de threads\n- **Jira** : lecture des tickets, ajout de commentaires, transitions de statut\n- **Notion** : consultation de la documentation technique\n- **GitLab** : lecture des merge requests, commentaires de review\n\n## Impact mesuré\n\nAprès quelques semaines d'adoption progressive :\n- **Temps de code review** : -67% (45min → 15min en moyenne)\n- **Temps d'écriture de tests** : -63% (2h → 45min par feature)\n- **Couverture de tests** : +40% sur les nouveaux modules\n- **Onboarding** : les juniors montent en compétence plus vite grâce aux reviews IA détaillées\n\n## Philosophie\n\nL'IA ne remplace pas le développeur — elle amplifie son expertise. Les skills sont conçus pour que l'humain reste décisionnaire : l'IA propose, le dev dispose. C'est cette approche \"human-in-the-loop\" qui a permis l'adoption par toute l'équipe.","## The Concept\n\nAt DJUST, I developed an ecosystem of **25+ custom skills** for Claude Code, transforming AI from a simple code assistant into a true team member. Each skill is a specialized prompt that encodes our conventions, architecture and business processes.\n\n## Concrete Use Cases\n\n- **Automated Code Review**: analyzes the GitLab diff, checks compliance with our Spring Boot conventions, detects security and performance issues, generates a structured comment\n- **E2E Test Generation**: from a Jira ticket, generates test scenarios adapted to our framework, with fixtures and assertions\n- **Deployment Briefing**: automatically compiles release changes, identifies risks, prepares the Slack message for the team\n- **Bug Analysis**: from a stack trace or log, traces the causal chain through our multi-module codebase\n\n## MCP Architecture\n\nThe integration relies on the **Model Context Protocol** (MCP), which allows Claude Code to interact with our tools:\n\n- **Slack**: reading channels, sending messages, creating threads\n- **Jira**: reading tickets, adding comments, status transitions\n- **Notion**: consulting technical documentation\n- **GitLab**: reading merge requests, review comments\n\n## Measured Impact\n\nAfter a few weeks of progressive adoption:\n- **Code review time**: -67% (45min → 15min average)\n- **Test writing time**: -63% (2h → 45min per feature)\n- **Test coverage**: +40% on new modules\n- **Onboarding**: juniors ramp up faster thanks to detailed AI reviews\n\n## Philosophy\n\nAI doesn't replace the developer — it amplifies their expertise. Skills are designed so the human remains the decision-maker: AI proposes, the dev decides. This \"human-in-the-loop\" approach is what enabled adoption by the entire team.","25+ skills ផ្ទាល់ខ្លួនសម្រាប់ Claude Code៖ code reviews ស្វ័យប្រវត្តិ ការបង្កើត tests E2E ការប្រជុំ deployment ការវិភាគ bugs។ ការរួមបញ្ចូល Slack/Jira/Notion/GitLab តាមរយៈ MCP។",[139,140,141,142],"Claude Code","MCP","AI","Automation"]