1(globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,79300,e=>{e.v({branchCardLink:"changelog-module__MUErDa__branchCardLink",branchDate:"changelog-module__MUErDa__branchDate",branchDot:"changelog-module__MUErDa__branchDot",branchHeader:"changelog-module__MUErDa__branchHeader",branchNameLink:"changelog-module__MUErDa__branchNameLink",branchRow:"changelog-module__MUErDa__branchRow",branchRowLink:"changelog-module__MUErDa__branchRowLink",branchSummary:"changelog-module__MUErDa__branchSummary",branchSummaryLink:"changelog-module__MUErDa__branchSummaryLink",collapseButton:"changelog-module__MUErDa__collapseButton",collapsedIndicator:"changelog-module__MUErDa__collapsedIndicator",emptyState:"changelog-module__MUErDa__emptyState",entryDetail:"changelog-module__MUErDa__entryDetail",entryText:"changelog-module__MUErDa__entryText",entryWhy:"changelog-module__MUErDa__entryWhy",filterActive:"changelog-module__MUErDa__filterActive",filterPill:"changelog-module__MUErDa__filterPill",filterRow:"changelog-module__MUErDa__filterRow",graphContainer:"changelog-module__MUErDa__graphContainer",killedReason:"changelog-module__MUErDa__killedReason",mobileCollapsedBtn:"changelog-module__MUErDa__mobileCollapsedBtn",mobileCollapsedLabel:"changelog-module__MUErDa__mobileCollapsedLabel",mobileEntry:"changelog-module__MUErDa__mobileEntry",mobileEntryDate:"changelog-module__MUErDa__mobileEntryDate",mobileEntryHeader:"changelog-module__MUErDa__mobileEntryHeader",mobileEntryPreview:"changelog-module__MUErDa__mobileEntryPreview",mobileNodeDot:"changelog-module__MUErDa__mobileNodeDot",mobileTimeline:"changelog-module__MUErDa__mobileTimeline",node:"changelog-module__MUErDa__node",nodeActive:"changelog-module__MUErDa__nodeActive",nodeColumn:"changelog-module__MUErDa__nodeColumn",nodeDate:"changelog-module__MUErDa__nodeDate",nodeFaded:"changelog-module__MUErDa__nodeFaded",nodeNow:"changelog-module__MUErDa__nodeNow",nodesRow:"changelog-module__MUErDa__nodesRow",pulse:"changelog-module__MUErDa__pulse",shimmer:"changelog-module__MUErDa__shimmer",statusBadge:"changelog-module__MUErDa__statusBadge",status_killed:"changelog-module__MUErDa__status_killed",status_ongoing:"changelog-module__MUErDa__status_ongoing",status_shipped:"changelog-module__MUErDa__status_shipped",tagline:"changelog-module__MUErDa__tagline",thumb:"changelog-module__MUErDa__thumb",thumbStack:"changelog-module__MUErDa__thumbStack",timeline:"changelog-module__MUErDa__timeline",timelineLine:"changelog-module__MUErDa__timelineLine",timelineLineDashed:"changelog-module__MUErDa__timelineLineDashed",timelineLineKilled:"changelog-module__MUErDa__timelineLineKilled",timelineTrack:"changelog-module__MUErDa__timelineTrack"})},97262,e=>{"use strict";var t=e.i(43476),a=e.i(71645),s=e.i(46932),n=e.i(88653),o=e.i(22016);let i=["all","ongoing","shipped","killed"],r=[{id:"portfolio",name:"portfolio",slug:"portfolio",color:"#9b8ec4",colorDark:"#b0a5d4",status:"shipped",startDate:"nov 2023",endDate:null,lastActive:"2026-03-19",stack:["next.js","tailwind","vercel","framer motion","prisma"],summary:"personal site. next.js, tailwind, vercel. the thing you're looking at right now.",description:"a portfolio site that's been evolving for over two years. started as a basic page, grew into a full content platform with projects, thoughts, reading lists, and now a changelog that tracks everything i build.",decisions:[{what:"next.js app router over pages router",why:"file-based routing under app/ is cleaner. each section is a folder with page.js. no custom routing config needed."},{what:"css modules + tailwind hybrid",why:"tailwind for layout and spacing, css modules for scoped component styles. avoids tailwind's class soup for complex components while keeping utility classes for simple stuff."},{what:"js data files over a CMS",why:"content changes rarely. editing a js array is faster than setting up a CMS, and there's no build step or API call. the data is right next to the component that renders it."}],built:["changelog page with git-style branch timelines, entry overflow, and filter pills","project detail pages with engineering-focused case studies","thoughts and reading list sections","seo component with og/twitter meta tags on every page","dark mode via prefers-color-scheme","blog with prisma + planetscale for view counts"],role:"solo: design, frontend, content",links:{live:"https://hardiksjain.in"},entries:[{date:"nov 2, 2023",text:"pilot. first deploy to vercel."},{date:"nov 3, 2023",text:"project page and home page highlights"},{date:"nov 19, 2023",text:"quotes page and blog api"},{date:"nov 26, 2023",text:"contact page, photography section"},{date:"dec 10, 2023",text:"seo across all pages"},{date:"dec 18, 2023",text:"work experience page"},{date:"feb 2, 2025",text:"redesigned project detail and listing pages"},{date:"jun 8, 2025",text:"thoughts section, grid layout"},{date:"aug 29, 2025",text:"case study components for projects",why:"wanted to showcase technical decisions, not just screenshots"},{date:"mar 19, 2026",text:"rewrote intro, killed case studies, built changelog with branch timelines",why:"case study format made things more ambiguous, not less. the changelog tells a better story."}]},{id:"swiftswipe",name:"swiftswipe",slug:"swiftswipe",color:"#d4845a",colorDark:"#e09b73",status:"killed",startDate:"jan 2025",endDate:"may 2025",lastActive:"2025-05-19",stack:["flutter","nestjs","postgresql","pgvector","openai","redis","docker"],summary:"credit card rewards optimizer. flutter + nestjs + rag. tells which card to use before tapping.",problem:"most people in india hold 2-4 credit cards and have no idea which one gives the best return for a given purchase. the reward structures are buried in PDFs, vary by merchant category, have caps and promo windows, and change quietly. people either pick a card at random or spend 20 minutes researching, and still get it wrong.",description:"swiftswipe takes the guesswork out. pick a merchant, enter an amount, and it tells which card to pull out. behind the scenes, it resolves the merchant to a category, scores every card in the wallet, and surfaces the winner.",howItWorks:["pick a merchant and enter the amount",'the merchant name gets matched to a category code. "Swiggy Instamart" and "Swiggy" both resolve correctly, without maintaining a lookup table. this uses embeddings and vector similarity against known MCCs',"every card in the wallet is scored: cashback percentage, reward point multipliers, category caps, promo windows, exclusions, all factored
1in","the best card is returned with a clear breakdown of why it wins"],decisions:[{what:"rag for merchant categorization",why:'merchants don\'t map cleanly to category codes. "Croma" could be electronics or retail depending on the bank. string matching breaks constantly. embedding the merchant name and running vector similarity against MCC descriptions handles edge cases that rules never would.'},{what:"clean architecture in flutter",why:"the reward logic is genuinely complex: category multipliers, caps, promos, exclusions. separating domain logic from the UI means i can test the scoring engine without touching flutter, and swap the API layer without rewriting business rules."},{what:"postgresql + pgvector over a dedicated vector db",why:"cards, reward rules, users, and wallets are relational. adding pgvector to the same database avoids syncing data between two systems for a few thousand embeddings."},{what:"bullmq for async processing",why:"embedding merchants and computing rewards across a full wallet isn't instant. a job queue keeps the API responsive and lets retries happen cleanly."}],built:["flutter mobile app with bloc state management and offline-first card portfolio (objectbox)","nestjs api with jwt auth, whatsapp otp (msg91), and a modular reward engine","merchant categorization pipeline: openai ada-002 embeddings â pgvector similarity search â mcc resolution","redis caching for hot card and reward data","dockerized with nginx reverse proxy","20+ indian credit cards catalogued with full reward rules"],role:"solo: product, design, full-stack, data modeling",links:{demo:"https://drive.google.com/drive/u/0/folders/1GqUSq6aB2vU59gEoPt_Hk74tEXZYmyzI"},screens:[{src:"/images/changelog/swiftswipe/splash.png",caption:"splash. a card, a keypad, a question."},{src:"/images/changelog/swiftswipe/home.png",caption:"home. where are you shopping next?"},{src:"/images/changelog/swiftswipe/cards.png",caption:"your wallet. every card, every perk, catalogued."},{src:"/images/changelog/swiftswipe/search.png",caption:"merchant search. the rag kicks in as you type."}],killedReason:"worked perfectly on paper, but merchant-to-MCC mapping was too ambiguous. accuracy wasn't reliable enough for fintech. if it's wrong once, nobody trusts it again.",entries:[{date:"jan 20, 2025",text:"pilot"},{date:"jan 23, 2025",text:"card explorer and my cards ui"},{date:"feb 20, 2025",text:"search ui with animations"},{date:"mar 10, 2025",text:"login flow with whatsapp otp, api integration started"},{date:"mar 24, 2025",text:"card management: add, remove, explore"},{date:"mar 29, 2025",text:"cards module, merchant search"},{date:"apr 1, 2025",text:"best card recommendation engine"},{date:"apr 18, 2025",text:"rewards integration, session management, empty states"},{date:"may 18, 2025",text:"card detail page, firebase, final polish"},{date:"may 19, 2025",text:"last commit",why:"merchant categorization accuracy wasn't reliable enough. good problem, wrong approach."}]},{id:"adaptive-palette",name:"adaptive_palette",slug:"adaptive-palette",color:"#9b8ec4",colorDark:"#b0a5d4",status:"shipped",startDate:"nov 2025",endDate:"mar 2026",lastActive:"2026-03-03",stack:["flutter","dart","custom shaders","k-means clustering"],summary:"flutter package. extract colors from images, generate fluid animated backgrounds. published on pub.dev.",description:"a flutter package that creates immersive, music-app-style animated backgrounds from any image. drop in a widget, pass an image, get a fluid background with orbital motion, heavy blur, and corner glows, all extracted from the image's dominant colors.",howItWorks:["pass an image to FluidBackground. it shows a matte fallback instantly (0ms startup)","in the background, the extractor samples every 10th pixel, weights by vibrancy and center proximity, then clusters into 6 colors using weighted k-means","the palette cross-fades in (1400ms): 4 layered image shaders with orbital motion, 80Ï gaussian blur, corner radial glows","results are cached by content hash. repeated calls return instantly"],decisions:[{what:"one widget, not a theme system",why:"v2 tried to do material design theming, WCAG compliance, multiple widget variants, and theme animations. too many problems. v3 does one thing: fluid backgrounds. simpler API, better result."},{what:"weighted k-means over simple averaging",why:"averaging gives muddy colors. k-means with vibrancy weighting (saturation à 0.75 + lightness à 0.25) and center proximity boost produces the colors that actually stand out in the image."},{what:"instant matte fallback",why:"image extraction takes 60-140ms. showing nothing during that time feels broken. the matte gradient appears at 0ms, then the real palette cross-fades in. users never see a blank screen."},{what:"animation off by default",why:"orbital motion looks great but drains battery. defaulting to static with opt-in animation respects the user's device. the toggle is smooth, resumes from frozen phase instead of jumping."}
1],built:["FluidBackground widget: one-line immersive backgrounds with 4 layered shaders","FluidPaletteExtractor: color extraction with smart sampling, vibrancy weighting, and perceptual scoring","LRU cache (30 entries, SHA-1 keyed) with warmup API for pre-loading upcoming images","matte treatment: blends near-whites with 14% gray, enforces dark base ⤠0.22 lightness","v1 â v2 â v3 migration path with full backward compatibility and deprecation warnings","59 unit tests covering all core algorithms"],role:"solo: package design, color science, shader implementation",links:{live:"https://pub.dev/packages/adaptive_palette",github:"https://github.com/HardikSJain/adaptive_palette"},screens:[{src:"/images/changelog/adaptive-palette/in-use.jpg",caption:"the package in the wild. yellow pulled straight from the hum tum cover. this is adaptive_palette running inside the music app."}],entries:[{date:"nov 11, 2025",text:"v1.0: CAM16 color extraction, WCAG compliance, 3 pre-built widgets"},{date:"nov 18, 2025",text:"adaptive scoring for different image types, perceptual color space"},{date:"dec 1, 2025",text:"v2.0: architectural rewrite. 12 modules from one monolithic file, 59 unit tests",why:"original single-file approach didn't scale. modularized everything and added proper test coverage."},{date:"dec 24, 2025",text:"v3.0: complete redesign. dropped material theming, built FluidBackground widget",why:"v2 was trying to solve too many problems. v3 does one thing: beautiful fluid backgrounds. simpler API, better result."},{date:"mar 3, 2026",text:"v3.1: fallback modes, palette caching, new extraction api"}]},{id:"flowy",name:"flowy",slug:"flowy",color:"#9b8ec4",colorDark:"#b0a5d4",status:"shipped",startDate:"nov 2025",endDate:"dec 2025",lastActive:"2025-12-21",stack:["nestjs","supabase","postgresql","openai","assemblyai","vercel ai sdk","typeorm"],summary:"mental health journaling app. built the entire backend: ai chat, voice journaling, emotion analysis. live on the app store.",description:"flowy is an emotional companion app. users journal by typing or talking, get thoughtful ai responses, and track how they feel over time. i built the entire backend as a freelance project: real-time ai streaming, voice transcription, emotion scoring, and daily insights generation.",howItWorks:["user creates a text or audio session. each type gets a different ai personality (detailed for text, quick one-liners for voice notes)","text messages stream back via server-sent events. the ai responds in real-time, token by token","audio messages get uploaded to supabase storage, transcribed by assemblyai, then the ai responds to the transcript","after a conversation, gpt-4o analyzes the full session: extracts emotion score (1-4), feelings with emojis, and a short insight","daily and weekly insights aggregate across sessions to surface patterns like 'journaling more on hard days' or 'mood has been trending up'"],decisions:[{what:"single-table design with jsonb for chat history",why:"one chat_sessions table stores everything: messages as jsonb, metadata as columns, full-text search via tsvector. no joins for the hot path. simple, fast, and the schema grows with the product."},{what:"dual system prompts for text vs audio",why:"text journaling needs depth: empathetic, exploratory responses. voice notes need brevity: quick acknowledgments, not essays. same ai service, different personalities based on session type."},{what:"sse streaming over websocket",why:"server-sent events are simpler than websockets for one-way data flow. the client sends a message via POST, the response streams back token by token. no connection management, works through every proxy."},{what:"assemblyai with polling over real-time transcription",why:"voice notes are pre-recorded, not live. polling a transcription job (10-30 seconds) is simpler and cheaper than maintaining a real-time websocket stream for audio that's already complete."}],built:["nestjs api with supabase auth (jwt verification), auto-creating user profiles on first request","real-time ai chat streaming via sse with vercel ai sdk, model-agnostic, currently using gpt-4o","audio pipeline: upload to supabase storage â transcribe with assemblyai â ai response â save to session","ai-powered session analysis: emotion scoring, insight generation, feelings extraction with structured output (zod schema validation)","daily and weekly insight generation aggregating patterns across sessions","full-text search with tsvector + trigram indexing across titles and transcripts","subscription management with webhook event handling","sentry integration for error tracking, background job for audio cleanup","dual stt provider support (assemblyai + deepgram)"],role:"freelance: backend engineering",links:{live:"https://apps.apple.com/us/app/flowy-emotional-companion/id6755959549"},screens:[{src:"/images/changelog/flowy/hero.webp",caption:"the product. an emotional companion, shipped to the app store."},{src:"/images/changelog/flowy/voice.webp",caption:"voice journaling. audio â assemblyai â ai response. this pipeline is the backend."},{src:"/images/changelog/flowy/insights.webp",caption:"emotional score. gpt-4o analyzes each session with zod-validated structured output."},{src:"/images/changelog/flowy/mood.webp",caption:"mood tracking. daily and weekly insights aggregate patterns across sessions."}],entries:[{date:"nov 9, 2025",text:"text + audio chat flow, initial api setup"},{date:"nov 11, 2025",text:"audio processing with ffmpeg, health check endpoints"},{date:"nov 16, 2025",text:"user management with supabase auth integration"},{date:"nov 28, 2025",text:"enhanced chat sessions with user transcripts and prompt support"},{date:"dec 1, 2025",text:"daily insights generation engine"},{date:"dec 5, 2025",text:"audio cleanup jobs, re-encoding, error logging improvements"},{date:"dec 7, 2025",text:"sentry integration, comprehensive error logging system"}
1,{date:"dec 8, 2025",text:"refactored insights for daily + weekly, schema improvements"},{date:"dec 20, 2025",text:"subscription management with webhook handling"},{date:"dec 21, 2025",text:"dual stt provider support: assemblyai + deepgram"}]},{id:"no-bs-sudoku",name:"no-bs-sudoku",slug:"no-bs-sudoku",color:"#9b8ec4",colorDark:"#b0a5d4",status:"shipped",startDate:"mar 14, 2026",endDate:"jul 2026",lastActive:"2026-07-25",stack:["flutter","flutter_bloc","drift","go_router","fl_chart"],summary:"open source sudoku app. no ads, no paywalls, no bs.",description:"a design-forward sudoku app built with flutter. dark, minimal, fast. zero ads, zero paywalls, zero dark patterns. every feature is free. hints are free. no premium tier. no 'watch an ad for a hint'. the grid is the hero, everything else gets out of the way.",howItWorks:["pure dart puzzle engine generates puzzles with guaranteed unique solutions using constraint propagation and symmetric hole-digging","4 difficulty levels, pencil notes with auto-clear, full undo stack (every action reversible, including hints)","intelligence engine tracks solve velocity, mistake patterns, consistency. surfaces one smart insight per day, not notifications or badges","daily puzzle rotates difficulty by day of week (mon/tue easy â sun expert), seeded generation means no server needed"],decisions:[{what:"pure dart puzzle engine, no external solver",why:"the entire generator, solver, and board model is written from scratch. constraint propagation + symmetric hole-digging gives guaranteed unique solutions without depending on any external library."},{what:"drift over raw sqlite",why:"type-safe sqlite for local persistence. every game state, stat, and insight is stored locally. fully offline, no network calls, no backend dependency."},{what:"intelligence over gamification",why:"no badges, no streaks-as-pressure, no notifications. the quality score (0-100) and velocity analysis give real feedback. daily insights like 'solving best in the evenings' are quiet observations, not engagement tricks."},{what:"terminal aesthetic",why:"near-black OLED background (#0A0A0A), electric lime accent (#C8FF00), DM Mono for numbers, Space Mono for UI. lowercase everything. 'Solved.' not 'Amazing!! You did it! ð¥'. the copy voice matches the design: dry, calm, no exclamation points."}],built:["puzzle engine with unique solution guarantee (constraint propagation + symmetric hole-digging)","game screen with full undo, pencil notes, smart highlighting, 3 free hints per puzzle","daily puzzle with day-of-week difficulty rotation, deterministic seeded generation","intelligence engine: quality score, velocity analysis, daily insights","stats dashboard with 14-day sparkline, 90-day github-style heatmap, per-difficulty breakdown","auto-save with floating resume bar and full state restoration"],role:"solo: product, design, full-stack",links:{live:"https://play.google.com/store/apps/details?id=com.nobssudoku.no_bs_sudoku",github:"https://github.com/HardikSJain/no-bs-sudoku"},screens:[{src:"/images/changelog/no-bs-sudoku/splash.png",caption:"no bs. no ads, no paywalls, ever."},{src:"/images/changelog/no-bs-sudoku/home.png",caption:"home. daily puzzle, 4 difficulties, resume where you left off."},{src:"/images/changelog/no-bs-sudoku/game.png",caption:"the grid. this is the hero. everything else gets out of the way."},{src:"/images/changelog/no-bs-sudoku/solved.png",caption:"solved. clean completion state."},{src:"/images/changelog/no-bs-sudoku/settings.png",caption:"settings. just the essentials."}],entries:[{date:"mar 14, 2026",text:"started",why:"every sudoku app is either ad-infested or paywalled. just wanted a clean one."},{date:"mar 16, 2026",text:"completed. in beta testing"},{date:"jul 25, 2026",text:"shipped. live on google play."}]},{id:"music-app",name:"music app",slug:"music-app",color:"#9b8ec4",colorDark:"#b0a5d4",status:"shipped",startDate:"mar 18, 2026",endDate:"apr 2026",lastActive:"2026-04-19",stack:["flutter","nestjs","prisma","redis","bull","websocket","spotdl","cloudflare tunnel"],summary:"private music streaming for my inner circle. paste spotify/youtube links, auto-download, stream to friends. self-hosted on a raspberry pi.",description:"my own music streaming service, running on a raspberry pi in my room. paste a spotify or youtube link, it auto-downloads the mp3 with full metadata. stream to friends, invite only. curated playlists by me. no ads, no algorithm, no bs. exposed to the internet via cloudflare tunnel on a custom subdomain, so friends can stream from anywhere without me opening a single port.",howItWorks:["paste a spotify or youtube link. spotdl downloads the mp3, pulls album art and id3 tags from spotify's metadata api","bull + redis queue handles downloads asynchronously. websocket gateway pushes live progress to the app, so the download progress shows in real-time","library module scans the filesystem, indexes every track with its metadata, supports search by title/artist and artist grouping","audio streaming uses http range requests. seeking, pause/resume, partial downloads all work natively. no buffering the whole file","cloudflare tunnel exposes the pi to the internet securely. no port forwarding, no dynamic dns, no static ip. friends connect through a cloudflare-proxied subdomain","flutter app connects to the backend, streams audio with just_audio, shows library browser and player ui","analytics module: listen tracking with 30-second threshold, persona detection (night owl, early bird), streaks, weekly/all-time stats, top tracks and artists"],decisions:[{what:"monorepo (flutter + nestjs in one repo)",why:"shared types between backend and app. single git history for atomic changes. simpler ci/cd. easier when i'm the only developer."},{what:"spotdl over yt-dlp",why:"better spotify metadata extraction. auto-fetches album art. handles playlists natively. still uses youtube as the audio source."},{what:"raspberry pi as the server",why:"self-hosted on a pi 5 (8GB). 64GB microSD holds ~9,000 songs. cloudflare tunnel for remote access, no port forwarding, no static IP needed. perfect for 10-50 concurrent users. total cost: the pi i already had."},{what:"websocket for download progress",why:"downloads take 10-30 seconds. polling would hammer the server. websocket pushes real-time progress. the app shows a live progress bar without any API calls."}],built:["nestjs backend with modular architecture: download, library, stream, queue, gateway modules","spotify/youtube â mp3 pipeline with spotdl, id3 tagging, and album art extraction via spotify api","bull + redis job queue with live websocket progress updates","audio streaming with http range request support: seeking, pause/resume, partial downloads","library scanner with metadata indexing, full-text search, and artist grouping","flutter mobile app with spotify-inspired design, provider state management, just_audio playback","raspberry pi deployment with cloudflare tunnel: secure remote access, no port forwarding","comprehensive test suites for stream, library, and e2e flows"],skipped:[{what:"no CI/CD pipeline",why:"manual deploy from laptop. it's a personal project on a pi, not a startup."},{what:"no docker",why:"runs directly on the pi. containerization adds overhead with zero benefit at this scale."},{what:"no monitoring or alerting",why:"no prometheus, no grafana. if it breaks, i'll notice because my music stops playing."},{what:"no real SMS OTP",why:"hardcoded OTP for dev. real SMS costs money and this has two users."},{what:"no database backups",why:"manual sqlite file copy. the music files are the real data, and those live on disk."}
1],role:"solo: product, full-stack, infrastructure",story:{href:"/thoughts/is-that-piracy",title:"the download button that shouldn't exist"},hardware:{src:"/images/changelog/music-app/pi.jpg",caption:"raspberry pi 5. 8gb ram, 64gb microsd, one USB-C cable, and a cloudflare tunnel. this is the server."},screens:[{src:"/images/changelog/music-app/home.jpg",caption:"home. your library, your go-tos."},{src:"/images/changelog/music-app/now-playing.jpg",caption:"now playing. colors pulled from the artwork."},{src:"/images/changelog/music-app/stats.jpg",caption:"stats. how i actually listen."},{src:"/images/changelog/music-app/playlists.jpg",caption:"playlists. hand-curated, by mood."}],entries:[{date:"mar 18, 2026",text:"monorepo setup. flutter + nestjs, database schema, download module with spotdl"},{date:"mar 18, 2026",text:"queue system (bull + redis), spotify metadata + id3 tagging"},{date:"mar 19, 2026",text:"library module, audio streaming with http range requests, websocket gateway"},{date:"mar 19, 2026",text:"flutter app: api integration, spotify-inspired design overhaul",why:"redesigned from scratch after feedback. minimal, dark, intentional."},{date:"mar 20, 2026",text:"auth system: jwt + otp, invite codes, role-based access"},{date:"mar 21, 2026",text:"switched from tailscale to cloudflare tunnel for remote access",why:"cloudflare tunnel on a custom subdomain. no client app needed, works through any browser, and the pi stays invisible to port scanners."},{date:"mar 22, 2026",text:"analytics module: listen tracking, persona detection, streaks, weekly stats",why:"wanted to understand my own listening habits. 30-second threshold before a listen counts, so skips don't inflate the numbers."},{date:"apr 2026",text:"shipped. running on the pi, friends streaming from anywhere."}]},{id:"samya",name:"samya",slug:"samya",color:"#6dba82",colorDark:"#7ccf93",status:"ongoing",startDate:"oct 3, 2026",endDate:null,lastActive:"2026-10-05",stack:["kotlin","jetpack compose","glance","firebase","cloud functions","claude","astronomy engine","health connect"],summary:"a vedic daily app whose background is the real sky over you. the phone computes the facts, claude writes one line from them.",description:"an android app that gives you one line a day. the colour of the screen is the actual sky where you are standing: night, dusk, golden, day. the phone works out the moon, your chart and the day's table itself, and a writer turns those facts into a line, a do, a don't, and a few sentences on home, people, love and work. it says what is computed and what is only the tradition's reading of it.",howItWorks:["the phone computes everything: sun altitude, sidereal positions (lahiri), nakshatra, tithi, whole-sign houses, vimshottari dasha. a vendored astronomy engine, no ephemeris api","the ground colour is eight stops keyed to the sun's altitude, blended in oklab. it is never a mood, only the sky","the facts go up as one signals object: sky, chart, sleep, steps, weather, what you said about today, how yesterday's line landed","a cloud function hands them to claude sonnet with a cached style guide and a strict schema. a validator rejects anything too long or out of voice, retries once, and falls back to a line the phone writes itself","a job runs every 30 minutes and writes the line for anyone whose sunrise is next, then pushes it, so it is on the lock screen before the app is opened","four places: today, days, sky, you. each shows one picture and a few lines, and keeps its working on a sheet of paper one link away","a few nights a year the sky does something you can see with bare eyes. the phone finds them (a planet at its closest, two planets side by side, a lunar eclipse), works out where to look from where you are, and makes a card to send"],decisions:[{what:"the phone computes every fact, the model only writes",why:"a model asked where the moon is will make something up. so it never is. it gets a list of facts and may only say them well. the maths is checked against swiss ephemeris to the arcminute."}
1,{what:"colour is the sky, never the mood",why:"every astrology app is purple. this one is whatever the sky is outside your window, so opening it at 6am and 6pm are two different apps. it also means there is no theme to design, only the sun."},{what:"sonnet over opus for the writer",why:"half the token price and the lines read the same. the thing that keeps quality is the validator and the style guide, not the bigger model."},{what:"say what is known and what is tradition",why:"where saturn is tonight is a fact. what that means for you is a reading. the app labels the second kind as the tradition's, and the writer may not promise outcomes or give medical, legal or money instructions."},{what:"one json file per day over a database",why:"the history is 'load the days'. a few hundred small files list instantly, there is nothing to migrate, and a broken file loses one day, not the lot."},{what:"take the behaviour, not the library",why:"saw hairline (lucasmarkes), line drawings that answer the pointer. the package is web and its figures are laptops and vaults, so none of it fits. the idea does: you can drag the sun across today's sky and the colour, the time and the line follow. the line goes back through the day but never ahead of now."},{what:"a card that is true for whoever gets it",why:"a share card usually only makes sense to the sender. the look-up card says 'it rises as the sun sets, wherever you are', which holds in any city that night. the link goes as text beside the image, because a url on a picture can't be tapped."},{what:"sky events stay out of the reading",why:"saturn being 1.26 billion km away is a fact. what it means for your tuesday is not. so it leads the sky page and the notification, and the writer never hears about it. no solar eclipses either: the app should never send anyone to look at the sun."},{what:"working on paper",why:"it was meant to be calm and it got cluttered. so each place shows a few things, and the whole table, the whole chart and how a line was made live on a sheet one tap away."}],built:["sky maths on device: sun and moon, nine grahas, houses, panchang, rahu kaal, dasha and sade sati, verified against swiss ephemeris","the writer: cloud function, cached style guide, schema, validator with one retry, local fallback","lines written on the server before each person's sunrise and pushed to the lock screen","today: the line by hour of day, do and don't, a strip that replays the sun from sunrise to now","the day by part of life: home, people, love, work, each with landed or missed and the one fact it came from","days as rings, one per day, drawn from what the phone kept","a wheel of twelve houses with the planets at birth and today","a home screen widget, three notifications, an evening check-in, a share card","the sun on today can be dragged through the day: one frame loop, paced along the drawn arc","nights worth going outside for: oppositions, greatest elongations, conjunctions, lunar eclipses, checked against the 2026 almanac","a look-up card with the planet's real path over your city, and a dusk notification that says when and where","navigation as a horizon: a hairline with a small disc on the place you are in","every screen tested at 200% text and on a 360dp phone"],role:"solo: product, design, android, backend, the writing",links:{live:"https://hardiksjain.in/samya"},screens:[{src:"/images/changelog/samya/today.png",caption:"today, at 6:20 as the sun sets. one line, a do, a don't, and the sky it was written under."},{src:"/images/changelog/samya/areas.png",caption:"one scroll down. the day by home, people, love and work."},{src:"/images/changelog/samya/days.png",caption:"days. a ring for each day, a dot for each time you looked. the ground is golden hour."},{src:"/images/changelog/samya/you.png",caption:"you. twelve houses, the planets at birth and today. the ground is midday."},{src:"/images/changelog/samya/sky.png",caption:"sky, on the night saturn was at its closest. the moon and the day's table under it."},{src:"/images/changelog/samya/look-up.png",caption:"the card to send. saturn's real path over mumbai that night, and a sentence that is true wherever it is opened."}],entries:[{date:"oct 3, 2026",text:"pilot. birth date in, 'you're not a leo' out, and a ground that is the colour of the sky",why:"the sidereal sign is usually one behind the one people know. that is the hook, and it is just arithmetic."},{date:"oct 3, 2026",text:"the writer on firebase, haptics, the widget"},{date:"oct 3, 2026",text:"play console, closed testing, 0.1.0"},{date:"oct 4, 2026",text:"birth time and place in the first run, skippable"},{date:"oct 4, 2026",text:"evening check-in, and days drawn as rings",why:"a list of past lines looked like a log file. r
1ings look like something you would want more of."},{date:"oct 4, 2026",text:"warmer grounds: eight stops, so the day is not blue from nine to five"},{date:"oct 4, 2026",text:"you, sky, and lines written on the server before sunrise"},{date:"oct 4, 2026",text:"cut the clutter. each place keeps its working on paper"},{date:"oct 4, 2026",text:"writer moved to sonnet. the day by home, people, love, work. the wheel"},{date:"oct 4, 2026",text:"the sunset in the replay made one continuous motion",why:"the sun popped out at -6 degrees and the colour lagged by a second. now it sinks behind the line and twilight lingers."},{date:"oct 4, 2026",text:"a critique as a real user, then fixes for it one by one. weather, steps and usual sleep to the writer"},{date:"oct 4, 2026",text:"/samya on this site, in the app's own type"},{date:"oct 4, 2026",text:"the sun on today can be dragged. the sky, the time and the line follow",why:"the first version stuttered: every frame rebuilt the whole app state. the altitude now goes to the background colour alone."},{date:"oct 5, 2026",text:"look up tonight: saturn at opposition leads the sky page, the notification, and a card to send",why:"an instagram post about saturn had 26.8k sends. the app already knew where saturn was, it just didn't know tonight was the night."},{date:"oct 5, 2026",text:"the row of places becomes a horizon, with 'you' at the right edge"}]}];var l=e.i(14656),d=e.i(79300);function c(e,t){if(t||e.length<=6)return e;let a=e[0];return{first:a,tail:e.slice(-4),hiddenCount:e.length-1-4,all:e}}function h({branch:e}){let a,s,n,o=(a=e.screens||[],s=e.hardware,n=[],a[0]&&n.push({src:a[0].src,alt:a[0].caption}),s&&n.push({src:s.src,alt:s.caption}),a[1]&&n.push({src:a[1].src,alt:a[1].caption}),n.length<3&&a[2]&&n.push({src:a[2].src,alt:a[2].caption}),n.slice(0,3));return 0===o.length?null:(0,t.jsx)("span",{className:d.default.thumbStack,"aria-hidden":"true",children:o.map((e,a)=>(0,t.jsx)("span",{className:d.default.thumb,style:{zIndex:o.length-a},children:(0,t.jsx)("img",{src:e.src,alt:""})},e.src))})}function u({branch:e,expandedEntry:a,onToggleEntry:s,isMobile:n,overflowExpanded:i,onToggleOverflow:r}){let l="killed"===e.status,p="ongoing"===e.status,m=e.endDate?`${e.startDate} â ${e.endDate}`:`${e.startDate} â now`;if(n){let a=e.entries,s=c(a,i),n=void 0!==s.hiddenCount;return(0,t.jsxs)(o.default,{href:`/changelog/${e.slug}`,className:`${d.default.branchRow} ${d.default.branchRowLink}`,children:[(0,t.jsxs)("div",{className:d.default.branchHeader,children:[(0,t.jsx)("span",{className:d.default.branchDot,style:{background:e.color}}),(0,t.jsx)("span",{className:d.default.branchNameLink,children:e.name}),(0,t.jsx)("span",{className:`${d.default.statusBadge} ${d.default[`status_${e.status}`]}`,children:e.status}),(0,t.jsx)(h,{branch:e}),(0,t.jsx)("span",{className:d.default.branchDate,children:m})]}),(0,t.jsx)("p",{className:d.default.branchSummary,children:e.summary}),(0,t.jsx)("div",{className:d.default.mobileTimeline,style:{borderColor:`${e.color}55`},children:(n?[s.first,...s.tail]:a).map((o,i)=>{let r=n&&i>0?a.length-4+(i-1):n?0:i,c=r===a.length-1;return(0,t.jsxs)("div",{className:d.default.mobileEntry,children:[(0,t.jsxs)("div",{className:d.default.mobileEntryHeader,children:[(0,t.jsx)("span",{className:`${d.default.mobileNodeDot} ${c&&p?d.default.nodeNow:""} ${c&&l?d.default.nodeFaded:""}`,style:{background:e.color}}),(0,t.jsx)("span",{className:d.default.mobileEntryDate,children:o.date})]}),(0,t.jsx)("p",{className:d.default.mobileEntryPreview,children:o.text}),0===i&&n&&(0,t.jsxs)("span",{className:d.default.mobileCollapsedLabel,children:["+",s.hiddenCount," more"]})]},r)})}),l&&e.killedReason&&(0,t.jsx)("p",{className:d.default.killedReason,children:e.killedReason})]})}return(0,t.jsxs)("div",{className:d.default.branchRow,children:[(0,t.jsxs)(o.default,{href:`/changelog/${e.slug}`,className:d.default.branchCardLink,children:[(0,t.jsxs)("div",{className:d.default.branchHeader,children:[(0,t.jsx)("span",{className:d.default.branchDot,style:{background:e.color}}),(0,t.jsx)("span",{className:d.default.branchNameLink,children:e.name}),(0,t.jsx)("span",{className:`${d.default.statusBadge} ${d.default[`status_${e.status}`]}`,children:e.status}),(0,t.jsx)(h,{branch:e}),(0,t.jsx)("span",{className:d.default.branchDate,children:m})]}),(0,t.jsx)("p",{className:d.default.branchSummary,children:e.summary}),l&&e.killedReason&&(0,t.jsx)("p",{className:d.default.killedReason,
1children:e.killedReason})]}),(0,t.jsx)(g,{branch:e,expandedEntry:a,onToggleEntry:s,isKilled:l,isOngoing:p,overflowExpanded:i,onToggleOverflow:r})]})}function g({branch:e,expandedEntry:a,onToggleEntry:s,isKilled:n,isOngoing:o,overflowExpanded:i,onToggleOverflow:r}){let l=e.entries,h=c(l,i),u=void 0!==h.hiddenCount;return(0,t.jsxs)("div",{className:d.default.timeline,children:[(0,t.jsxs)("div",{className:d.default.timelineTrack,children:[(0,t.jsx)("div",{className:`${d.default.timelineLine} ${n?d.default.timelineLineKilled:""}`,style:{background:e.color}}),n&&(0,t.jsx)("div",{className:d.default.timelineLineDashed,style:{borderColor:e.color,opacity:.4}})]}),(0,t.jsx)("div",{className:d.default.nodesRow,children:u?(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(p,{branch:e,entry:h.first,index:0,isLast:!1,isOngoing:!1,isKilled:!1,expandedEntry:a,onToggleEntry:s}),(0,t.jsxs)("button",{className:d.default.collapsedIndicator,onClick:r,"aria-label":`Show ${h.hiddenCount} more entries`,children:["+",h.hiddenCount," more"]}),h.tail.map((i,r)=>{let d=l.length-4+r,c=d===l.length-1;return(0,t.jsx)(p,{branch:e,entry:i,index:d,isLast:c,isOngoing:c&&o,isKilled:n&&c,expandedEntry:a,onToggleEntry:s},d)})]}):(0,t.jsxs)(t.Fragment,{children:[l.map((i,r)=>{let d=r===l.length-1;return(0,t.jsx)(p,{branch:e,entry:i,index:r,isLast:d,isOngoing:d&&o,isKilled:n&&d,expandedEntry:a,onToggleEntry:s},r)}),l.length>6&&(0,t.jsx)("button",{className:d.default.collapseButton,onClick:r,children:"show less"})]})})]})}function p({branch:e,entry:a,index:o,isLast:i,isOngoing:r,isKilled:l,expandedEntry:c,onToggleEntry:h}){let u=`${e.id}-${o}`,g=c===u;return(0,t.jsxs)("div",{className:d.default.nodeColumn,children:[(0,t.jsx)("button",{className:`${d.default.node} ${g?d.default.nodeActive:""} ${r?d.default.nodeNow:""} ${l?d.default.nodeFaded:""}`,style:{background:e.color,borderColor:e.color},onClick:()=>h(u),"aria-expanded":g,"aria-label":`${a.date}: ${a.text}`}),(0,t.jsx)("span",{className:d.default.nodeDate,children:a.date}),(0,t.jsx)(n.AnimatePresence,{children:g&&(0,t.jsxs)(s.motion.div,{className:d.default.entryDetail,initial:{opacity:0,height:0},animate:{opacity:1,height:"auto"},exit:{opacity:0,height:0},transition:{duration:.3,ease:"easeOut"},children:[(0,t.jsx)("p",{className:d.default.entryText,children:a.text}),a.why&&(0,t.jsx)("p",{className:d.default.entryWhy,children:a.why})]})})]})}e.s(["default",0,function(){let[e,s]=(0,a.useState)("all"),[n,o]=(0,a.useState)(null),[c,h]=(0,a.useState)({}),[g,p]=(0,a.useState)(!1);(0,a.useEffect)(()=>{let e=window.matchMedia("(max-width: 768px)");p(e.matches);let t=e=>p(e.matches);return e.addEventListener("change",t),()=>e.removeEventListener("change",t)},[]);let m=r.filter(t=>"all"===e||t.status===e).sort((e,t)=>t.lastActive.localeCompare(e.lastActive)),y=e=>{o(t=>t===e?null:e)};return(0,t.jsx)(t.Fragment,{children:(0,t.jsxs)("section",{children:[(0,t.jsx)("h1",{className:"font-light text-2xl mb-2 tracking-tighter",children:"changelog"}),(0,t.jsx)("p",{className:d.default.tagline,children:"what i'm building, what i killed, and why."}),(0,t.jsx)("div",{className:d.default.filterRow,children:i.map(a=>(0,t.jsx)("button",{className:`${d.default.filterPill} ${e===a?d.default.filterActive:""}`,onClick:()=>s(a),children:a},a))}),0===m.length&&(0,t.jsx)("p",{className:d.default.emptyState,children:"nothing to see here. try clicking a filter above."}),(0,t.jsx)("div",{className:d.default.graphContainer,children:m.map(e=>(0,t.jsx)(u,{branch:e,expandedEntry:n,onToggleEntry:y,isMobile:g,overflowExpanded:!!c[e.id],onToggleOverflow:()=>{var t;return t=e.id,void h(e=>({...e,[t]:!e[t]}))}},e.id))}),(0,t.jsx)(l.default,{})]})})}],97262)}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.