[{"data":1,"prerenderedAt":239},["ShallowReactive",2],{"api:\u002Fapi\u002Fconfig:en":3,"api:\u002Fapi\u002Fprofile:":8,"api:\u002Fapi\u002Fcategories:en":20,"api:\u002Fapi\u002Fprojects\u002Fcatalog-api:en":164,"api:\u002Fapi\u002Fnotes:en":218},{"email":4,"lang":5,"name":6,"role":7},"hello@ewsirdev.example","en","Ewsirdev","Web developer: websites, landing pages and Go services",{"name":6,"handle":6,"contacts":9,"note":19},{"email":10,"github":13,"telegram":16},{"url":11,"label":4,"placeholder":12},"mailto:hello@ewsirdev.example",true,{"url":14,"label":15},"https:\u002F\u002Fgithub.com\u002Fewsir247","github.com\u002Fewsir247",{"url":17,"label":18},"https:\u002F\u002Ft.me\u002FEwsirdev","@Ewsirdev","email is a placeholder on a reserved domain: mail is not delivered. Telegram is the real channel. This file is the single source of contacts for the site, the contact block, JSON-LD sameAs, security.txt and llms.txt.",[21,50,78,100,136],{"slug":22,"title":23,"blurb":24,"variant":25,"palette":26,"layout":27,"count":28,"preview":29,"stack":45},"landings","Landing pages","One page, one job. Hand-written markup where possible, plus one WebGL experiment.","drop","cold","shots",6,[30,35,40],{"slug":31,"title":32,"shot":33,"hue":34},"landing-studio","Halftone: lighting studio","\u002Fshots\u002Flanding-studio-desktop-sm.webp",212,{"slug":36,"title":37,"shot":38,"hue":39},"landing-saas","Kill Switch: subscription tracker","\u002Fshots\u002Flanding-saas-desktop-sm.webp",182,{"slug":41,"title":42,"shot":43,"hue":44},"landing-event","Small Scale: conference","\u002Fshots\u002Flanding-event-desktop-sm.webp",124,[46,47,48,49],"CSS","HTML","GitHub Actions","JS",{"slug":51,"title":52,"blurb":53,"variant":54,"palette":55,"layout":56,"count":57,"preview":58,"stack":74},"sites","Multi-page sites","Sites with their own structure, routing and forms: from a client project to a set of niche pages.","lens","mono","frames",3,[59,64,69],{"slug":60,"title":61,"shot":62,"hue":63},"legal-multipage-site","Multi-page legal services website","\u002Fshots\u002Flegal-multipage-site-desktop-sm.webp",250,{"slug":65,"title":66,"shot":67,"hue":68},"niche-landings","Catalog of 10 niche demo landing pages","\u002Fshots\u002Fniche-landings-desktop-sm.webp",32,{"slug":70,"title":71,"shot":72,"hue":73},"docs-engine","Sverlo: a documentation generator in Node.js","\u002Fshots\u002Fdocs-engine-desktop-sm.webp",25,[48,75,76,77],"Vue 3","Bootstrap 5","Cloudflare Pages",{"slug":79,"title":80,"blurb":81,"variant":82,"palette":83,"layout":84,"count":85,"preview":86,"stack":97},"apps","Web apps","Interface plus server: auth, data, e-mail. Apps you sign in to.","merge","warm","phone",2,[87,92],{"slug":88,"title":89,"shot":90,"hue":91},"bad-club-booking-app","Bad Club: seat booking for a gaming coworking space","\u002Fshots\u002Fbad-club-booking-app-desktop-sm.webp",150,{"slug":93,"title":94,"shot":95,"hue":96},"typing-lab","typing-lab: a touch-typing trainer","\u002Fshots\u002Ftyping-lab-desktop-sm.webp",45,[75,77,98,99],"Express","GSAP",{"slug":101,"title":102,"blurb":103,"variant":104,"palette":105,"layout":106,"count":107,"langs":108,"preview":117,"stack":133},"backend","Backends","Services with an API, a database and tests: mostly Go, with some in Python and TypeScript. A language filter sits inside the section.","torus","acid","list",14,[109,112,115],{"lang":110,"count":111},"Go",12,{"lang":113,"count":114},"Python",1,{"lang":116,"count":114},"TypeScript",[118,123,128],{"slug":119,"title":120,"shot":121,"hue":122},"kanban-tasks","Amber: kanban board","\u002Fshots\u002Fkanban-tasks-desktop-sm.webp",344,{"slug":124,"title":125,"shot":126,"hue":127},"link-shortener","Short: link shortener","\u002Fshots\u002Flink-shortener-desktop-sm.webp",111,{"slug":129,"title":130,"shot":131,"hue":132},"booking-slots","Appointment booking","\u002Fshots\u002Fbooking-slots-desktop-sm.webp",65,[134,110,135,48],"Docker","SQLite",{"slug":137,"title":138,"blurb":139,"variant":140,"palette":55,"layout":141,"count":57,"langs":142,"preview":145,"stack":161},"cli","CLI tools","Terminal programs: one binary, clear flags, an exit code you can check in a script.","film","terminal",[143,144],{"lang":110,"count":85},{"lang":113,"count":114},[146,151,156],{"slug":147,"title":148,"shot":149,"hue":150},"gitpulse","GitPulse: git repository analytics","\u002Fshots\u002Fgitpulse-desktop-sm.webp",7,{"slug":152,"title":153,"shot":154,"hue":155},"logslice","logslice: log analysis with no dependencies","\u002Fshots\u002Flogslice-desktop-sm.webp",160,{"slug":157,"title":158,"shot":159,"hue":160},"envguard","envguard: a linter and a safe for .env files","\u002Fshots\u002Fenvguard-desktop-sm.webp",35,[162,134,48,163],"CLI","stdlib only",{"slug":165,"title":166,"type":101,"category":101,"lang":113,"stack":167,"description":174,"highlights":175,"run":180,"hue":181,"og":182,"shots":183,"case":191,"case_lang":5,"prev":212,"next":215},"catalog-api","catalog-api: a catalog API with search",[113,168,135,169,170,171,172,173,134,48],"FastAPI","FTS5","pytest","ruff","mypy","bandit","A product catalog API on Python 3.12 and FastAPI: SQLite with FTS5 full-text search (bm25 ranking), facets by category, price, availability, tags and attributes, cursor pagination, ETag, CSV import and export, price history, API keys with roles and an audit log. The storefront and the admin panel are served by the app itself, with no framework. A side project.",[176,177,178,179],"FTS5 search with bm25, facets and cursor pagination","API keys with the roles admin, editor and reader; only key hashes are stored","511 tests, 96% coverage; ruff, mypy --strict, bandit, pip-audit and gitleaks are clean","Load: 339 requests\u002Fs, p95 50 ms at 8 connections (Windows, a single process)","SEED_DEMO=1 python -m app   # http:\u002F\u002F127.0.0.1:8110",265,"\u002Fog\u002Fen\u002Fcatalog-api.jpg",{"desktop":184,"desktop_sm":185,"mobile":186,"dw":187,"dh":188,"mw":189,"mh":190},"\u002Fshots\u002Fcatalog-api-desktop.webp","\u002Fshots\u002Fcatalog-api-desktop-sm.webp","\u002Fshots\u002Fcatalog-api-mobile.webp",1280,800,390,844,{"about":192,"features":195,"run":202,"notes":207},[193,194],"catalog-api is a product catalog API on Python 3.12 and FastAPI with a storefront and an admin panel served by the app itself. A side project.","The storage is SQLite: full-text search through FTS5 with bm25 ranking, facets by category, price, availability, tags and attributes, signed cursor pagination. Access is by API keys with the roles admin, editor and reader: a key is shown once and only its hash is stored. The demo mode SEED_DEMO=1 brings up 300 synthetic products.",[196,197,198,199,178,200,201],"FTS5 search with bm25, facets, sorting and cursor pagination; ETag for conditional requests","CSV import and export, price history, image upload with signature and limit checks, an audit log","API keys with roles, a request limiter, strict security headers; OpenAPI in dev mode and off in production","A storefront and an admin panel without a framework, sitemap, robots, llms.txt and OG tags","Load measurement: 339 requests\u002Fs, p95 50 ms at 8 connections (Windows, a single process)","A Docker image on python:3.12-slim, user uid 10001, a HEALTHCHECK, graceful shutdown on SIGTERM",[203,204,205,206],"git clone \u003Crepository on request> catalog-api","cd catalog-api","python -m venv .venv && .venv\u002Fbin\u002Fpip install --require-hashes -r requirements.lock","SEED_DEMO=1 python -m app   # http:\u002F\u002F127.0.0.1:8110, admin at \u002Fadmin",[208,209,210,211],"There is no live version: Cloudflare does not run a Python server with SQLite. Run it locally, in Docker or on your own VPS.","The request limiter lives in process memory: with several workers the limit multiplies. SQLite is built for a single writer process. There is no Russian morphology and no typo correction.","Images are not re-encoded and metadata is kept as is. Facets are not disjunctive; the total is counted exactly on every request, which has to be replaced at millions of products.","Demo keys are public and work only with SEED_DEMO=1. Not checked: running behind a real reverse proxy with TLS, browsers other than Chromium, and screen readers. The load measurement is a single one, on one machine.",{"slug":213,"title":214},"pixel-wall","Pixel Wall: a shared canvas",{"slug":216,"title":217},"paste-vault","Konvert: notes encrypted in the browser",[219,224,229,234],{"slug":220,"lang":5,"title":221,"date":222,"summary":223,"project":129,"minutes":114},"double-booking","Double booking: a transaction plus a partial UNIQUE index","2026-10-20","How booking-slots protects against two clients booking the same time at once, and how that protection is tested.",{"slug":225,"lang":5,"title":226,"date":222,"summary":227,"project":228,"minutes":85},"lease-and-fencing","Lease and fencing: how a queue neither loses a job nor lets a zombie finish it twice","A worker can crash, stall, or wake up after its job was taken by someone else. How the lease and the lease token in job-queue deal with that on SQLite.","job-queue",{"slug":230,"lang":5,"title":231,"date":222,"summary":232,"project":233,"minutes":85},"p95-nearest-rank","p95 in an uptime monitor: the nearest-rank method and what it does not hide","How uptime-monitor computes p50\u002Fp95 latency and uptime, why the nearest-rank method was chosen, and where those numbers can mislead.","uptime-monitor",{"slug":235,"lang":5,"title":236,"date":222,"summary":237,"project":238,"minutes":85},"websocket-from-scratch","WebSocket on the standard library: what really happens under new WebSocket()","Why realtime-rooms has its own RFC 6455 layer, which protocol violations it checks, and why a library is still the better choice for production.","realtime-rooms",1792819120146]