[{"data":1,"prerenderedAt":1146},["ShallowReactive",2],{"api:\u002Fapi\u002Fconfig:ru":3,"api:\u002Fapi\u002Fprofile:":8,"api:\u002Fapi\u002Fcategories:ru":19,"api:\u002Fapi\u002Fnotes\u002Fqr-from-scratch:ru":177,"api:\u002Fapi\u002Fprojects:ru":186},{"email":4,"lang":5,"name":6,"role":7},"djrciv@gmail.com","ru","Ewsirdev","Веб-разработчик: сайты, лендинги и сервисы на Go",{"name":6,"handle":6,"contacts":9,"note":18},{"email":10,"github":12,"telegram":15},{"url":11,"label":4},"mailto:djrciv@gmail.com",{"url":13,"label":14},"https:\u002F\u002Fgithub.com\u002Fewsir247","github.com\u002Fewsir247",{"url":16,"label":17},"https:\u002F\u002Ft.me\u002FEwsirdev","@Ewsirdev","Contacts: Telegram, GitHub, email. This file is the single source of contacts for the site, the contact block, JSON-LD sameAs, security.txt and llms.txt.",[20,49,80,108,144],{"slug":21,"title":22,"blurb":23,"variant":24,"palette":25,"layout":26,"count":27,"preview":28,"stack":44},"landings","Лендинги","Одна страница, одна задача. Вёрстка без фреймворков, где это возможно, и один WebGL-эксперимент.","drop","ice","shots",6,[29,34,39],{"slug":30,"title":31,"shot":32,"hue":33},"landing-studio","Полутон: студия света","\u002Fshots\u002Flanding-studio-desktop-sm.webp",212,{"slug":35,"title":36,"shot":37,"hue":38},"landing-saas","Рубильник: трекер подписок","\u002Fshots\u002Flanding-saas-desktop-sm.webp",182,{"slug":40,"title":41,"shot":42,"hue":43},"landing-event","Малый масштаб: конференция","\u002Fshots\u002Flanding-event-desktop-sm.webp",124,[45,46,47,48],"CSS","HTML","GitHub Actions","JS",{"slug":50,"title":51,"blurb":52,"variant":53,"palette":54,"layout":55,"count":56,"langs":57,"preview":61,"stack":77},"sites","Многостраничные сайты","Сайты со своей структурой, маршрутами и формами: от клиентского проекта до набора нишевых страниц.","merge","violet","frames",8,[58],{"lang":59,"count":60},"TypeScript",5,[62,67,72],{"slug":63,"title":64,"shot":65,"hue":66},"legal-multipage-site","Многостраничный сайт юридической компании","\u002Fshots\u002Flegal-multipage-site-desktop-sm.webp",250,{"slug":68,"title":69,"shot":70,"hue":71},"niche-landings","Каталог демо-лендингов под 10 ниш","\u002Fshots\u002Fniche-landings-desktop-sm.webp",32,{"slug":73,"title":74,"shot":75,"hue":76},"docs-engine","Сверло: генератор документации на Node.js","\u002Fshots\u002Fdocs-engine-desktop-sm.webp",25,[47,59,78,79],"Cloudflare Pages","axe",{"slug":81,"title":82,"blurb":83,"variant":84,"palette":85,"layout":86,"count":27,"langs":87,"preview":90,"stack":105},"apps","Веб-приложения","Приложения, в которые входят: с сервером, авторизацией и письмами или офлайн-first PWA, где данные остаются в браузере.","torus","mint","phone",[88],{"lang":59,"count":89},4,[91,96,101],{"slug":92,"title":93,"shot":94,"hue":95},"bad-club-booking-app","Bad Club: бронирование мест в игровом коворкинге","\u002Fshots\u002Fbad-club-booking-app-desktop-sm.webp",150,{"slug":97,"title":98,"shot":99,"hue":100},"typing-lab","typing-lab: тренажёр слепой печати","\u002Fshots\u002Ftyping-lab-desktop-sm.webp",45,{"slug":102,"title":103,"shot":104,"hue":100},"budget-split","budget-split: «Поровну», учёт общих расходов","\u002Fshots\u002Fbudget-split-desktop-sm.webp",[59,106,79,107],"Vite","vitest",{"slug":109,"title":110,"blurb":111,"variant":112,"palette":113,"layout":114,"count":115,"langs":116,"preview":125,"stack":141},"backend","Бэкенды","Сервисы с API, базой и тестами: на Go, Rust, Python и TypeScript. Фильтр по языку внутри раздела.","crystal","steel","list",24,[117,120,122,124],{"lang":118,"count":119},"Go",12,{"lang":121,"count":89},"Python",{"lang":123,"count":89},"Rust",{"lang":59,"count":89},[126,131,136],{"slug":127,"title":128,"shot":129,"hue":130},"kanban-tasks","Янтарь: канбан-доска","\u002Fshots\u002Fkanban-tasks-desktop-sm.webp",344,{"slug":132,"title":133,"shot":134,"hue":135},"link-shortener","Короче: сократитель ссылок","\u002Fshots\u002Flink-shortener-desktop-sm.webp",111,{"slug":137,"title":138,"shot":139,"hue":140},"booking-slots","Запись на приём","\u002Fshots\u002Fbooking-slots-desktop-sm.webp",65,[142,47,143,118],"Docker","SQLite",{"slug":145,"title":146,"blurb":147,"variant":148,"palette":149,"layout":150,"count":27,"langs":151,"preview":158,"stack":174},"cli","CLI-утилиты","Программы для терминала: один бинарник, понятные флаги, код выхода, который можно проверить в скрипте.","swarm","champagne","terminal",[152,154,156,157],{"lang":118,"count":153},3,{"lang":121,"count":155},1,{"lang":123,"count":155},{"lang":59,"count":155},[159,164,169],{"slug":160,"title":161,"shot":162,"hue":163},"gitpulse","GitPulse: аналитика git-репозиториев","\u002Fshots\u002Fgitpulse-desktop-sm.webp",7,{"slug":165,"title":166,"shot":167,"hue":168},"logslice","logslice: анализ логов без зависимостей","\u002Fshots\u002Flogslice-desktop-sm.webp",160,{"slug":170,"title":171,"shot":172,"hue":173},"envguard","envguard: линтер и сейф .env-файлов","\u002Fshots\u002Fenvguard-desktop-sm.webp",35,[47,175,142,176],"CLI","stdlib only",{"slug":178,"lang":5,"title":179,"date":180,"summary":181,"project":182,"minutes":183,"body_md":184,"body_html":185},"qr-from-scratch","QR-код с нуля: Рид-Соломон, маски и как доказать верность","2026-10-30","Как устроен QR-кодер на TypeScript без зависимостей и четыре независимых способа проверить, что матрица правильная.","qr-svg",2,"qr-svg написан без единой runtime-зависимости: весь ISO\u002FIEC 18004 для версий 1–40 лежит в `src\u002Fqr\u002F`. Самое интересное здесь не генерация, а доказательство.\n\n## Из чего состоит кодер\n\n- Поле GF(256) и арифметика в нём.\n- Кодирование Рида-Соломона: байты коррекции как остаток от деления многочлена.\n- Чередование блоков для версий с несколькими блоками.\n- Шаблоны (поисковые, выравнивающие, синхронизация), форматная и версионная информация.\n- Восемь масок и штрафы для выбора лучшей.\n\n## Как проверена верность\n\n1. **Эталоны стандарта.** Пример ISO «01234567» в 1-M и разбор «HELLO WORLD» в 1-Q совпадают по байтам коррекции.\n2. **Независимая реализация.** Матрицы сверяются бит в бит с `qrcode-generator` (devDependency): 12 текстов × 4 уровня и 80 случайных строк.\n3. **Сторонний декодер.** jsQR читает все 160 сочетаний «версия × уровень», кроме 23-L, которую не читает и эталон (матрицы идентичны, тест это фиксирует).\n4. **Свой декодер** с проверкой нулевых синдромов Рида-Соломона, независимой от деления в кодировщике.\n\n## Маски\n\nАвтовыбор маски у нас и у эталона часто различается: штрафы считаются чуть по-разному, обе маски корректны. Поэтому теста «выбрали ту же маску» намеренно нет. Сверка идёт с маской, взятой из форматной информации эталона.\n\n## Оговорка\n\nГотовой матрицы из самого стандарта в репозитории нет: её роль играет сверка с независимой реализацией. Нет Kanji, ECI, смешанных сегментов и Micro QR. Тестов 45; на нагрузке с четвертью тяжёлых запросов (версия 37) p50 131 мс, p95 272 мс.\n","\u003Cp>qr-svg написан без единой runtime-зависимости: весь ISO\u002FIEC 18004 для версий 1–40 лежит в \u003Ccode>src\u002Fqr\u002F\u003C\u002Fcode>. Самое интересное здесь не генерация, а доказательство.\u003C\u002Fp>\n\u003Ch2>Из чего состоит кодер\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>Поле GF(256) и арифметика в нём.\u003C\u002Fli>\n\u003Cli>Кодирование Рида-Соломона: байты коррекции как остаток от деления многочлена.\u003C\u002Fli>\n\u003Cli>Чередование блоков для версий с несколькими блоками.\u003C\u002Fli>\n\u003Cli>Шаблоны (поисковые, выравнивающие, синхронизация), форматная и версионная информация.\u003C\u002Fli>\n\u003Cli>Восемь масок и штрафы для выбора лучшей.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>Как проверена верность\u003C\u002Fh2>\n\u003Col>\n\u003Cli>\u003Cstrong>Эталоны стандарта.\u003C\u002Fstrong> Пример ISO «01234567» в 1-M и разбор «HELLO WORLD» в 1-Q совпадают по байтам коррекции.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Независимая реализация.\u003C\u002Fstrong> Матрицы сверяются бит в бит с \u003Ccode>qrcode-generator\u003C\u002Fcode> (devDependency): 12 текстов × 4 уровня и 80 случайных строк.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Сторонний декодер.\u003C\u002Fstrong> jsQR читает все 160 сочетаний «версия × уровень», кроме 23-L, которую не читает и эталон (матрицы идентичны, тест это фиксирует).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Свой декодер\u003C\u002Fstrong> с проверкой нулевых синдромов Рида-Соломона, независимой от деления в кодировщике.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>Маски\u003C\u002Fh2>\n\u003Cp>Автовыбор маски у нас и у эталона часто различается: штрафы считаются чуть по-разному, обе маски корректны. Поэтому теста «выбрали ту же маску» намеренно нет. Сверка идёт с маской, взятой из форматной информации эталона.\u003C\u002Fp>\n\u003Ch2>Оговорка\u003C\u002Fh2>\n\u003Cp>Готовой матрицы из самого стандарта в репозитории нет: её роль играет сверка с независимой реализацией. Нет Kanji, ECI, смешанных сегментов и Micro QR. Тестов 45; на нагрузке с четвертью тяжёлых запросов (версия 37) p50 131 мс, p95 272 мс.\u003C\u002Fp>\n",[187,206,220,233,248,262,275,295,314,335,359,382,407,435,462,486,507,526,546,566,582,596,615,631,648,668,688,706,722,736,754,776,796,814,833,854,872,892,911,931,950,972,991,1010,1028,1048,1067,1087,1106,1126],{"slug":127,"title":128,"type":109,"category":109,"lang":118,"kind":188,"live_url":189,"stack":190,"description":191,"highlights":192,"run":197,"hue":130,"og":198,"shots":199},"api","https:\u002F\u002Fkanban-tasks.ewsir.space",[118,143,142],"Многопользовательская канбан-доска: регистрация, доски, колонки, карточки с перетаскиванием. Go + SQLite без CGO, фронт встроен в бинарник.",[193,194,195,196],"PBKDF2 и сессии в HttpOnly-cookie","Изоляция данных пользователей","Тесты API","Docker multi-stage","cd kanban-tasks && go run .","\u002Fog\u002Fkanban-tasks.jpg",{"desktop":200,"desktop_sm":129,"mobile":201,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fkanban-tasks-desktop.webp","\u002Fshots\u002Fkanban-tasks-mobile.webp",1280,800,390,844,{"slug":132,"title":133,"type":109,"category":109,"lang":118,"kind":188,"live_url":207,"stack":208,"description":209,"highlights":210,"run":215,"hue":135,"og":216,"shots":217},"https:\u002F\u002Flink-shortener.ewsir.space",[118,143,142],"Сокращение ссылок с алиасами и сроком жизни, статистика кликов по дням, браузерам и устройствам, дашборд под токеном. IP не хранятся.",[211,212,213,214],"Редирект и учёт кликов","Лимит запросов","Приватная статистика (хэш с солью)","Тесты и Docker","cd link-shortener && set SEED_DEMO=1&& go run .\u002Fcmd\u002Fserver","\u002Fog\u002Flink-shortener.jpg",{"desktop":218,"desktop_sm":134,"mobile":219,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Flink-shortener-desktop.webp","\u002Fshots\u002Flink-shortener-mobile.webp",{"slug":137,"title":138,"type":109,"category":109,"lang":118,"kind":188,"live_url":221,"stack":222,"description":223,"highlights":224,"run":228,"hue":140,"og":229,"shots":230},"https:\u002F\u002Fbooking-slots.ewsir.space",[118,143,142],"Онлайн-запись на услуги: свободные слоты с учётом длительности и рабочих часов, защита от двойной записи, админ-панель. Вымышленная студия, демо.",[225,226,227,214],"Расчёт свободных слотов","Без двойных записей","Админка для услуг и расписания","cd booking-slots && go run .","\u002Fog\u002Fbooking-slots.jpg",{"desktop":231,"desktop_sm":139,"mobile":232,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fbooking-slots-desktop.webp","\u002Fshots\u002Fbooking-slots-mobile.webp",{"slug":30,"title":31,"type":234,"category":21,"live_url":235,"stack":236,"description":237,"highlights":238,"run":243,"hue":33,"og":244,"shots":245},"landing","https:\u002F\u002Fewsirdev-studio.pages.dev",[46,45,48],"Лендинг вымышленной студии интерьерного света: тёплая палитра, засечный шрифт, SVG-иллюстрация, FAQ и демо-форма. Адаптивный, без внешних картинок.",[239,240,241,242],"Адаптив 390-1280","SVG вместо картинок","Доступные FAQ и форма","Честная демо-форма","cd landing-studio && python -m http.server 8091","\u002Fog\u002Flanding-studio.jpg",{"desktop":246,"desktop_sm":32,"mobile":247,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Flanding-studio-desktop.webp","\u002Fshots\u002Flanding-studio-mobile.webp",{"slug":35,"title":36,"type":234,"category":21,"live_url":249,"stack":250,"description":251,"highlights":252,"run":257,"hue":38,"og":258,"shots":259},"https:\u002F\u002Fewsirdev-saas.pages.dev",[46,45,48],"Лендинг вымышленного SaaS-продукта: тёмный градиентный hero, макет интерфейса на CSS, тарифы, FAQ, демо-форма. Все цены и названия придуманы.",[253,254,255,256],"Макет интерфейса на CSS","Тарифная сетка","Адаптив","Демо-форма без отправки данных","cd landing-saas && python -m http.server 8092","\u002Fog\u002Flanding-saas.jpg",{"desktop":260,"desktop_sm":37,"mobile":261,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Flanding-saas-desktop.webp","\u002Fshots\u002Flanding-saas-mobile.webp",{"slug":40,"title":41,"type":234,"category":21,"live_url":263,"stack":264,"description":265,"highlights":266,"run":270,"hue":43,"og":271,"shots":272},"https:\u002F\u002Fewsirdev-event.pages.dev",[46,45,48],"Лендинг вымышленной однодневной конференции: плакатная типографика, расписание, спикеры-заглушки, условия, демо-форма записи.",[267,268,255,269],"Расписание дня","Плакатная типографика","Без выдуманных имён спикеров","cd landing-event && python -m http.server 8093","\u002Fog\u002Flanding-event.jpg",{"desktop":273,"desktop_sm":42,"mobile":274,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Flanding-event-desktop.webp","\u002Fshots\u002Flanding-event-mobile.webp",{"slug":276,"title":277,"type":109,"category":109,"lang":118,"kind":278,"live_url":279,"stack":280,"description":283,"highlights":284,"run":288,"hue":289,"og":290,"shots":291},"uptime-monitor","Пульс: монитор доступности","infra","https:\u002F\u002Fuptime-monitor.ewsir.space",[118,143,281,282,142],"SSE","REST API","Монитор доступности: HTTP и TCP проверки по расписанию, аптайм, p50\u002Fp95, детектор инцидентов, живой дашборд на SSE и публичная статус-страница. Go + SQLite без CGO.",[285,286,287,214],"Горутина-воркер на каждую проверку","Инциденты после серии падений","Дашборд на SSE и публичный \u002Fstatus","cd uptime-monitor && set SEED_DEMO=1&& go run .\u002Fcmd\u002Fserver",101,"\u002Fog\u002Fuptime-monitor.jpg",{"desktop":292,"desktop_sm":293,"mobile":294,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fuptime-monitor-desktop.webp","\u002Fshots\u002Fuptime-monitor-desktop-sm.webp","\u002Fshots\u002Fuptime-monitor-mobile.webp",{"slug":296,"title":297,"type":109,"category":109,"lang":118,"kind":278,"live_url":298,"stack":299,"description":301,"highlights":302,"run":307,"hue":308,"og":309,"shots":310},"job-queue","Конвейер: очередь задач","https:\u002F\u002Fjob-queue.ewsir.space",[118,143,282,281,300,142],"Prometheus","Очередь фоновых задач: приоритеты, отложенные и cron-задачи, ретраи с backoff и jitter, dead-letter, идемпотентные ключи, lease с fencing, метрики Prometheus.",[303,304,305,306],"At-least-once с lease и fencing","Ретраи с backoff и jitter + dead-letter","Собственный cron-парсер","Метрики Prometheus и graceful shutdown","cd job-queue && set API_TOKEN=demo&& set SEED_DEMO=1&& go run .\u002Fcmd\u002Fserver",249,"\u002Fog\u002Fjob-queue.jpg",{"desktop":311,"desktop_sm":312,"mobile":313,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fjob-queue-desktop.webp","\u002Fshots\u002Fjob-queue-desktop-sm.webp","\u002Fshots\u002Fjob-queue-mobile.webp",{"slug":315,"title":316,"type":109,"category":109,"lang":118,"kind":317,"live_url":318,"stack":319,"description":322,"highlights":323,"run":328,"hue":329,"og":330,"shots":331},"realtime-rooms","Комнаты: чат и доска в реальном времени","realtime","https:\u002F\u002Frealtime-rooms.ewsir.space",[118,320,143,321,142],"WebSocket","Canvas","Комнаты в реальном времени: чат с историей в SQLite, список «кто онлайн», индикатор «печатает», общая доска со штрихами и курсорами, анонимные ники. WebSocket (RFC 6455) написан на stdlib.",[324,325,326,327],"Свой слой WebSocket (RFC 6455) на stdlib","Отключение медленных клиентов и лимиты частоты","Graceful shutdown с кодом 1001","Нагрузочный тест: 360 соединений в 6 комнатах","cd realtime-rooms && set SEED_DEMO=1&& go run .\u002Fcmd\u002Fserver",46,"\u002Fog\u002Frealtime-rooms.jpg",{"desktop":332,"desktop_sm":333,"mobile":334,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Frealtime-rooms-desktop.webp","\u002Fshots\u002Frealtime-rooms-desktop-sm.webp","\u002Fshots\u002Frealtime-rooms-mobile.webp",{"slug":336,"title":337,"type":109,"category":109,"lang":118,"kind":278,"live_url":338,"stack":339,"description":346,"highlights":347,"run":352,"hue":353,"og":354,"shots":355},"api-gateway","Шлюз: API-gateway","https:\u002F\u002Fapi-gateway.ewsir.space",[118,176,340,341,342,281,300,343,344,345,142,47],"Reverse proxy","Circuit breaker","Token bucket","slog","Vanilla JS","embed","API-шлюз на чистой stdlib: маршруты по хосту\u002Fпрефиксу из JSON-конфига с горячей перезагрузкой без обрыва соединений, балансировка round-robin и least-conn, health-checks, circuit breaker на узел, ретраи идемпотентных запросов, rate limit, хэшированные API-ключи, метрики Prometheus, живой дашборд на SSE.",[348,349,350,351],"Ноль зависимостей: только stdlib","Горячая перезагрузка конфига без обрыва соединений","Circuit breaker на узел и ретраи идемпотентных запросов","Нагрузочный тест: 4000 запросов, 40 клиентов (демо-замер)","cd api-gateway && set SEED_DEMO=1&& go run .\u002Fcmd\u002Fgateway",200,"\u002Fog\u002Fapi-gateway.jpg",{"desktop":356,"desktop_sm":357,"mobile":358,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fapi-gateway-desktop.webp","\u002Fshots\u002Fapi-gateway-desktop-sm.webp","\u002Fshots\u002Fapi-gateway-mobile.webp",{"slug":360,"title":361,"type":109,"category":109,"lang":118,"kind":362,"live_url":363,"stack":364,"description":370,"highlights":371,"run":376,"hue":168,"og":377,"shots":378},"notes-search","Картотека: заметки с поиском","data","https:\u002F\u002Fnotes-search.ewsir.space",[118,143,365,366,367,282,368,369,344,345,142,47],"FTS5","bm25","modernc.org\u002Fsqlite (без CGO)","Markdown (свой безопасный рендер)","SVG force-layout","База Markdown-заметок на Go и SQLite: вики-ссылки [[заметка]] с обратными ссылками и графом связей, полнотекстовый поиск FTS5 (bm25, подсветка, упрощённая русская морфология), история версий с собственным diff, автосохранение с разрешением конфликтов, экспорт и импорт .md.",[372,373,374,375],"Поиск FTS5 с bm25 и подсветкой","Вики-ссылки, backlinks и граф связей","История версий с собственным diff","Свой безопасный Markdown-рендер с тестом на XSS","cd notes-search && set SEED_DEMO=1&& go run .\u002Fcmd\u002Fserver","\u002Fog\u002Fnotes-search.jpg",{"desktop":379,"desktop_sm":380,"mobile":381,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fnotes-search-desktop.webp","\u002Fshots\u002Fnotes-search-desktop-sm.webp","\u002Fshots\u002Fnotes-search-mobile.webp",{"slug":383,"title":384,"type":234,"category":21,"live_url":385,"stack":386,"description":393,"highlights":394,"run":400,"hue":401,"og":402,"shots":403},"landing-3d","ОКСО Т1: 3D-лендинг","https:\u002F\u002Fewsirdev-3d.pages.dev",[387,388,344,389,390,391,392,47],"HTML5","CSS3","WebGL2","GLSL ES 3.00","Raymarching\u002FSDF","Scroll choreography","Лендинг вымышленного датчика воздуха (демо): объект рисуется одним фрагментным шейдером на чистом WebGL2 без библиотек, четыре SDF-формы перетекают по скроллу, есть SVG-фолбэк, reduced-motion и пауза вне экрана.",[395,396,397,398,399],"Один фрагментный шейдер","WebGL2 без библиотек","Четыре SDF-формы с морфингом по скроллу","SVG-фолбэк и prefers-reduced-motion","Пауза отрисовки вне экрана","cd landing-3d && python -m http.server 8094",90,"\u002Fog\u002Flanding-3d.jpg",{"desktop":404,"desktop_sm":405,"mobile":406,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Flanding-3d-desktop.webp","\u002Fshots\u002Flanding-3d-desktop-sm.webp","\u002Fshots\u002Flanding-3d-mobile.webp",{"slug":408,"title":409,"type":109,"category":109,"lang":118,"kind":362,"live_url":410,"stack":411,"description":420,"highlights":421,"run":428,"hue":429,"og":430,"shots":431},"raft-kv","Плот: KV на Raft","https:\u002F\u002Fraft-kv.ewsir.space",[118,412,413,414,176,415,281,416,417,418,419,344,142,47],"Raft","Distributed systems","Consensus","HTTP\u002FJSON","WAL + fsync","Snapshots","Linearizability","Deterministic simulation testing","Учебная реализация Raft с нуля на чистой stdlib: выборы лидера, репликация лога, коммит по кворуму, лог и term\u002Fvote на диске с fsync, снапшоты с компакцией, линеаризуемые чтения (read-index), редирект фолловера на лидера. Тестируется детерминированным симулятором сети и времени с проверкой линеаризуемости; дашборд умеет убивать лидера и рвать сеть (демо).",[422,423,424,425,426,427],"Raft с нуля на stdlib","без raft-библиотек","Детерминированный симулятор сети и времени + проверка линеаризуемости","Лог и term\u002Fvote с fsync","снапшоты и компакция","Дашборд: убить лидера и разорвать сеть (демо)","cd raft-kv && set SEED_DEMO=1&& go run .\u002Fcmd\u002Fraft-kv",290,"\u002Fog\u002Fraft-kv.jpg",{"desktop":432,"desktop_sm":433,"mobile":434,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fraft-kv-desktop.webp","\u002Fshots\u002Fraft-kv-desktop-sm.webp","\u002Fshots\u002Fraft-kv-mobile.webp",{"slug":436,"title":437,"type":109,"category":109,"lang":118,"kind":438,"live_url":439,"stack":440,"description":449,"highlights":450,"run":455,"hue":456,"og":457,"shots":458},"auth-service","Ключница: сервис аутентификации","auth","https:\u002F\u002Fauth-service.ewsir.space",[118,441,143,367,442,443,444,445,446,447,448,344,345,142,47],"stdlib crypto","JWT EdDSA\u002FEd25519","JWKS","PBKDF2","TOTP","RBAC","REST","OpenAPI 3","Сервис аутентификации и авторизации на Go: EdDSA-JWT с ротацией ключей и JWKS, refresh-токены с ротацией и обнаружением кражи, TOTP-2FA (RFC 6238) с резервными кодами, RBAC, блокировка при переборе, сброс пароля по одноразовому токену, журнал аудита, список устройств, openapi.yaml. Не прошёл независимый аудит, не production-IdP.",[451,452,453,454],"JWT EdDSA и ротация ключей с JWKS","Refresh-токены с ротацией и reuse-detection","TOTP по RFC 6238 с проверкой на векторах RFC","Журнал аудита только на дозапись (триггеры БД)","cd auth-service && set SEED_DEMO=1&& go run .\u002Fcmd\u002Fserver",20,"\u002Fog\u002Fauth-service.jpg",{"desktop":459,"desktop_sm":460,"mobile":461,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fauth-service-desktop.webp","\u002Fshots\u002Fauth-service-desktop-sm.webp","\u002Fshots\u002Fauth-service-mobile.webp",{"slug":463,"title":464,"type":109,"category":109,"lang":118,"kind":188,"live_url":465,"stack":466,"description":473,"highlights":474,"run":479,"hue":480,"og":481,"shots":482},"finance-tracker","Книга учёта: личные финансы","https:\u002F\u002Ffinance-tracker.ewsir.space",[118,143,367,282,467,468,59,469,470,107,471,142,47,444,472],"Nuxt 4","Vue 3","Pinia","SVG-графики","ESLint","CSV import\u002Fexport","Учёт личных финансов: счета в разных валютах с ручными курсами, доходы, расходы и переводы, бюджеты, платежи по cron-подобному расписанию, импорт CSV с превью, отчёты. Деньги только в целых минорных единицах, данные пользователей изолированы, графики нарисованы на SVG без библиотек. Go + SQLite и фронтенд на Nuxt 4.",[475,476,477,478],"Деньги только целыми минорными единицами","Мультивалютность с ручными курсами","Импорт CSV с превью и пробным прогоном","Свои SVG-графики без библиотек","cd finance-tracker && set SEED_DEMO=1&& go run .",140,"\u002Fog\u002Ffinance-tracker.jpg",{"desktop":483,"desktop_sm":484,"mobile":485,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Ffinance-tracker-desktop.webp","\u002Fshots\u002Ffinance-tracker-desktop-sm.webp","\u002Fshots\u002Ffinance-tracker-mobile.webp",{"slug":487,"title":488,"type":109,"category":109,"lang":118,"kind":317,"live_url":489,"stack":490,"description":494,"highlights":495,"run":500,"hue":501,"og":502,"shots":503},"pixel-wall","Пиксельная стена: общий холст","https:\u002F\u002Fpixel-wall.ewsir.space",[118,176,320,491,344,345,492,493,142,47],"Canvas 2D","Binary protocol","Snapshot + journal","Совместный холст 256×256 в духе r\u002Fplace: 16 цветов, кулдаун на сессию и IP, свой WebSocket-слой на stdlib (RFC 6455) с бинарным протоколом (4 бита на пиксель), снимок и дельты, снапшот плюс журнал с восстановлением, таймлапс, зум и пан, мини-карта, экспорт PNG, админ-инструменты. Пет-проект.",[496,497,498,499],"Свой WebSocket (RFC 6455) на stdlib","Бинарный протокол: 4 бита на пиксель","Снапшот + журнал с восстановлением после падения","Таймлапс по журналу изменений","cd pixel-wall && set SEED_DEMO=1&& set ADMIN_TOKEN=secret&& go run .\u002Fcmd\u002Fserver",320,"\u002Fog\u002Fpixel-wall.jpg",{"desktop":504,"desktop_sm":505,"mobile":506,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fpixel-wall-desktop.webp","\u002Fshots\u002Fpixel-wall-desktop-sm.webp","\u002Fshots\u002Fpixel-wall-mobile.webp",{"slug":160,"title":161,"type":145,"category":145,"lang":118,"live_url":508,"stack":509,"description":515,"highlights":516,"run":521,"hue":163,"og":522,"shots":523},"https:\u002F\u002Fgitpulse.ewsir.space",[118,176,175,510,511,512,513,514,47,142],"git","html\u002Ftemplate","SVG","ANSI","os\u002Fexec","CLI на Go для анализа git-репозиториев без внешних зависимостей (нужен только git): stats с цветами и спарклайнами в терминале, report — самодостаточный HTML-отчёт с тепловой картой активности, churn и горячими файлами, вкладом авторов, bus factor по каталогам, ростом кода по языкам и периодами тишины (светлая и тёмная темы), diff-hotspots между ревизиями и локальный serve на 127.0.0.1. История читается потоком через git log -z --numstat -M. Пет-проект.",[517,518,519,520],"Только stdlib Go, внешний бинарник один: git","Поток git log -z --numstat -M с учётом .mailmap и переименований","Самодостаточный HTML-отчёт: тепловая карта, churn, bus factor, светлая и тёмная темы","Синтетическая история из 100 000 коммитов анализируется около 5 секунд","cd gitpulse && go run .\u002Fcmd\u002Fgitpulse stats .","\u002Fog\u002Fgitpulse.jpg",{"desktop":524,"desktop_sm":162,"mobile":525,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fgitpulse-desktop.webp","\u002Fshots\u002Fgitpulse-mobile.webp",{"slug":63,"title":64,"type":527,"category":50,"stack":528,"description":533,"highlights":534,"run":541,"hue":66,"og":542,"shots":543},"site",[467,468,59,529,530,531,532,142,47],"Bootstrap 5","Zod","Vitest","Playwright","Реализованный сайт юридической и экспертной компании на Nuxt 4: 65 страниц с серверным рендерингом, JSON-LD разметка и 301-редиректы со старого сайта. Заявки уходят через серверный эндпоинт с проверкой Origin, лимитом размера тела и ограничением частоты запросов.",[535,536,537,538,539,540],"65 маршрутов с SSR, sitemap.xml и robots.txt; индексация включается явным флагом окружения","Форма заявок: серверный эндпоинт, проверка Origin, лимит тела 10 КБ, 3 запроса за 10 минут с одного IP, honeypot, опциональная капча","JSON-LD: LegalService, WebSite, BreadcrumbList","Одношаговые 301-редиректы со старых URL и со слешей на конце, заголовки безопасности (CSP, HSTS)","CI\u002FCD: lint, typecheck, Vitest, сборка, Playwright; затем Docker-образ и выкладка на сервер","210 модульных тестов в 13 файлах и 4 набора e2e-тестов","npm install && npm run build && node .output\u002Fserver\u002Findex.mjs","\u002Fog\u002Flegal-multipage-site.jpg",{"desktop":544,"desktop_sm":65,"mobile":545,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Flegal-multipage-site-desktop.webp","\u002Fshots\u002Flegal-multipage-site-mobile.webp",{"slug":92,"title":93,"type":547,"category":81,"stack":548,"description":555,"highlights":556,"run":561,"hue":95,"og":562,"shots":563},"app",[468,106,469,549,550,551,552,553,554],"Vue Router","GSAP","Express","node:sqlite","JWT (httpOnly cookie)","Resend","Демо-приложение на Vue 3 и Express с SQLite: интерактивная карта зала с бронированием мест, личный кабинет и админ-панель с редактором расстановки. Вход без паролей, по одноразовому коду на e-mail.",[557,558,559,560],"Карта зала с зонами (обычные, тихая, VIP, два монитора), фильтром по типу и статусом мест","Админ-панель: обзор, брони, столы, компьютеры, пользователи и перетаскиваемая расстановка зала","Вход по OTP с ограничением попыток и частоты запросов, роль admin задаётся через переменную окружения","Данные для демо создаются сид-скриптом при первом запуске","npm install && npm run dev","\u002Fog\u002Fbad-club-booking-app.jpg",{"desktop":564,"desktop_sm":94,"mobile":565,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fbad-club-booking-app-desktop.webp","\u002Fshots\u002Fbad-club-booking-app-mobile.webp",{"slug":567,"title":568,"type":234,"category":21,"live_url":569,"stack":570,"description":571,"highlights":572,"run":561,"hue":576,"og":577,"shots":578},"dovod-concept","Довод: концепт лендинга экспертной организации","https:\u002F\u002Fewsirdev-dovod.pages.dev",[467,468,59,45,47],"Концепт лендинга на Nuxt 4 для вымышленной экспертной организации: направления услуг, форма консультации с маской телефона, карусель отзывов и страница политики. Компания, отзывы, цифры и контакты придуманы; форма ничего не отправляет.",[573,574,575],"Форма с маской телефона, валидацией и подсказками по ошибкам; в демо показывает «данные не отправляются»","Бесконечная карусель отзывов со свайпом и автопрокруткой","Мобильная панель действий, адаптивная вёрстка, скелетон поверх серверного HTML",215,"\u002Fog\u002Fdovod-concept.jpg",{"desktop":579,"desktop_sm":580,"mobile":581,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fdovod-concept-desktop.webp","\u002Fshots\u002Fdovod-concept-desktop-sm.webp","\u002Fshots\u002Fdovod-concept-mobile.webp",{"slug":68,"title":69,"type":527,"category":50,"live_url":583,"stack":584,"description":587,"highlights":588,"run":561,"hue":71,"og":592,"shots":593},"https:\u002F\u002Fewsirdev-niche.pages.dev",[468,585,586,47],"Vue Router 4","Vite 8","Vue-каталог из десяти самостоятельных лендингов под разные ниши малого бизнеса: у каждого своя палитра, шрифты и структура секций. Каждый лендинг загружается отдельным чанком; весь контент демонстрационный.",[589,590,591],"10 ниш: медицина, мебель, ремонт, онлайн-школа, глэмпинг, юристы, автосервис, салон красоты, кафе, фитнес","Код-сплиттинг по маршрутам, общий набор SVG-иконок","Плашка «Демо-контент» на каталоге и каждом лендинге","\u002Fog\u002Fniche-landings.jpg",{"desktop":594,"desktop_sm":70,"mobile":595,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fniche-landings-desktop.webp","\u002Fshots\u002Fniche-landings-mobile.webp",{"slug":597,"title":598,"type":234,"category":21,"live_url":599,"stack":600,"description":603,"highlights":604,"run":608,"hue":609,"og":610,"shots":611},"dental-concept","Кварц: концепт лендинга стоматологии","https:\u002F\u002Fewsirdev-dental.pages.dev",[46,45,601,491,602,47],"JavaScript","htmlhint","Концепт одностраничного лендинга вымышленной стоматологии в стиле Liquid Chrome Dark: стеклянные карточки, хромированные заголовки и 3D-блоб в виде зуба на canvas. Один HTML-файл без сборки и зависимостей.",[605,606,607],"Анимированный блоб на canvas с учётом prefers-reduced-motion и видимости на экране","Форма записи с маской телефона и доступными сообщениями об ошибках","CI: htmlhint и проверка локальных ссылок и якорей","python -m http.server 8080",190,"\u002Fog\u002Fdental-concept.jpg",{"desktop":612,"desktop_sm":613,"mobile":614,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fdental-concept-desktop.webp","\u002Fshots\u002Fdental-concept-desktop-sm.webp","\u002Fshots\u002Fdental-concept-mobile.webp",{"slug":165,"title":166,"type":145,"category":145,"lang":121,"stack":616,"description":620,"highlights":621,"run":626,"hue":168,"og":627,"shots":628},[121,176,175,512,617,618,619,47,142],"pytest","ruff","mypy","CLI на Python 3.11+ только на стандартной библиотеке: потоково разбирает логи nginx\u002Fapache (combined), JSON-lines, logfmt и syslog (gz, bz2, xz тоже), считает статистику и топы, кластеризует похожие ошибки, ищет скачки нагрузки по z-score и EWMA и собирает один HTML-отчёт с SVG-графиками и тепловой картой. Пет-проект.",[622,623,624,625],"Только стандартная библиотека, pip install ничего не тянет","Кластеры ошибок по шаблону и аномалии двумя методами (z-score и EWMA)","Один самодостаточный HTML-отчёт: SVG, тепловая карта, светлая и тёмная темы","53 теста, ruff и mypy --strict; 1 млн строк combined за 8,2 с (один прогон, Windows, CPython 3.12.10)","logslice report access.log -o report.html","\u002Fog\u002Flogslice.jpg",{"desktop":629,"desktop_sm":167,"mobile":630,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Flogslice-desktop.webp","\u002Fshots\u002Flogslice-mobile.webp",{"slug":170,"title":171,"type":145,"category":145,"lang":118,"stack":632,"description":637,"highlights":638,"run":643,"hue":173,"og":644,"shots":645},[118,176,175,633,444,634,635,636,47,142],"AES-256-GCM","fuzzing","govulncheck","gitleaks","CLI на Go без внешних модулей: check сверяет .env со схемой, scan ищет секреты в дереве и git-истории, diff и doctor проверяют окружения и git-гигиену, seal и unseal шифруют .env в .env.sealed (AES-256-GCM, PBKDF2 600 тыс. итераций), run запускает команду с чистым окружением, report и serve строят отчёт. Значение секрета не попадает ни в вывод, ни в лог, ни в HTML. Пет-проект.",[639,640,641,642],"Значения секретов не печатаются нигде: только тип находки, файл, строка и маска из 4 символов","seal и unseal: AES-256-GCM, PBKDF2 600 тыс. итераций, подмена любого бита файла отвергается","Fuzz-тесты: 4 цели по 20 секунд без падений; govulncheck и gitleaks без находок","Сборки под windows, linux и darwin, только стандартная библиотека","go run .\u002Fcmd\u002Fenvguard check","\u002Fog\u002Fenvguard.jpg",{"desktop":646,"desktop_sm":172,"mobile":647,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fenvguard-desktop.webp","\u002Fshots\u002Fenvguard-mobile.webp",{"slug":649,"title":650,"type":109,"category":109,"lang":121,"live_url":651,"stack":652,"description":655,"highlights":656,"run":661,"hue":662,"og":663,"shots":664},"catalog-api","catalog-api: API каталога с поиском","https:\u002F\u002Fcatalog-api.ewsir.space",[121,653,143,365,617,618,619,654,142,47],"FastAPI","bandit","API каталога товаров на Python 3.12 и FastAPI: SQLite с полнотекстовым поиском FTS5 (ранжирование bm25), фасеты по категории, цене, наличию, тегам и характеристикам, курсорная пагинация, ETag, импорт и экспорт CSV, история цен, API-ключи с ролями и аудит-лог. Витрина и админка без фреймворка отдаются самим приложением. Пет-проект.",[657,658,659,660],"Поиск FTS5 с bm25, фасеты и курсорная пагинация","API-ключи с ролями admin, editor и reader; в базе только хэши ключей","511 тестов, покрытие 96%; ruff, mypy --strict, bandit, pip-audit и gitleaks без замечаний","Нагрузка: 339 запросов\u002Fс, p95 50 мс при 8 соединениях (Windows, один процесс)","SEED_DEMO=1 python -m app   # http:\u002F\u002F127.0.0.1:8110",265,"\u002Fog\u002Fcatalog-api.jpg",{"desktop":665,"desktop_sm":666,"mobile":667,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fcatalog-api-desktop.webp","\u002Fshots\u002Fcatalog-api-desktop-sm.webp","\u002Fshots\u002Fcatalog-api-mobile.webp",{"slug":669,"title":670,"type":109,"category":109,"lang":59,"live_url":671,"stack":672,"description":677,"highlights":678,"run":561,"hue":576,"og":683,"shots":684},"paste-vault","Конверт: записки с шифрованием в браузере","https:\u002F\u002Fewsirdev-paste.ewsirdev.workers.dev",[59,673,674,675,106,107,676,47],"Cloudflare Workers","D1","Web Crypto","fast-check","Сервис одноразовых записок на TypeScript, Cloudflare Workers и D1: текст шифруется в браузере (AES-256-GCM), ключ лежит только во фрагменте URL и на сервер не попадает. У записки есть срок жизни, пароль и сгорание после первого прочтения. Пет-проект.",[679,680,681,682],"Шифрование в браузере: AES-256-GCM, ключ только во фрагменте URL","Сгорание после прочтения атомарно: ровно один успех из 40 параллельных чтений","52 теста в рантайме workerd, включая property-тесты и фаззинг схемы","Живая версия на Cloudflare Workers + D1","\u002Fog\u002Fpaste-vault.jpg",{"desktop":685,"desktop_sm":686,"mobile":687,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fpaste-vault-desktop.webp","\u002Fshots\u002Fpaste-vault-desktop-sm.webp","\u002Fshots\u002Fpaste-vault-mobile.webp",{"slug":73,"title":74,"type":527,"category":50,"live_url":689,"stack":690,"description":695,"highlights":696,"run":701,"hue":76,"og":702,"shots":703},"https:\u002F\u002Fewsirdev-docs.pages.dev",[691,692,693,694,78,47],"Node.js","JavaScript (ESM)","zero dependencies","Markdown","Генератор статических сайтов документации, написанный с нуля на Node.js без зависимостей: собственный Markdown-парсер, подсветка кода и офлайн-поиск. Демо-сайт про вымышленную библиотеку «Сверло» собран из 43 страниц на двух языках (RU и EN) и двух версиях (v1 и v2). Пет-проект.",[697,698,699,700],"Свой Markdown-парсер, подсветка кода и клиентский поиск без библиотек","43 страницы, RU и EN, версии v1 и v2, hreflang, sitemap и RSS","131 тест; в CI проверка битых ссылок и якорей","Сайт читается без JavaScript, поиск работает офлайн","npm test && npm run build","\u002Fog\u002Fdocs-engine.jpg",{"desktop":704,"desktop_sm":75,"mobile":705,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fdocs-engine-desktop.webp","\u002Fshots\u002Fdocs-engine-mobile.webp",{"slug":97,"title":98,"type":547,"category":81,"live_url":707,"stack":708,"description":712,"highlights":713,"run":561,"hue":100,"og":718,"shots":719},"https:\u002F\u002Fewsirdev-typing.pages.dev",[467,468,59,709,710,711,512,107,79,78],"IndexedDB","WebAudio","PWA","Тренажёр слепой печати, который целиком работает в браузере и не обращается к серверу: Nuxt 4, история в IndexedDB, звук клавиш на WebAudio, офлайн через PWA. Режимы времени, слов, цитат, своего текста, кода и «слабых мест», шесть раскладок, метрики WPM, точность и консистентность, графики на чистом SVG. Пет-проект.",[714,715,716,717],"Без сервера и аккаунтов: история в IndexedDB, экспорт и импорт JSON","Шесть раскладок, режимы вплоть до адаптивных упражнений из ваших ошибок","59 unit-тестов и 65 e2e-проверок, axe без нарушений, CLS 0,000","PWA: работает офлайн после первого открытия","\u002Fog\u002Ftyping-lab.jpg",{"desktop":720,"desktop_sm":99,"mobile":721,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Ftyping-lab-desktop.webp","\u002Fshots\u002Ftyping-lab-mobile.webp",{"slug":102,"title":103,"type":547,"category":81,"lang":59,"live_url":723,"stack":724,"description":726,"highlights":727,"run":561,"hue":100,"og":732,"shots":733},"https:\u002F\u002Fewsirdev-budget.ewsirdev.workers.dev",[59,106,673,674,725,107,676,79,47],"WebGL","Веб-приложение учёта общих расходов группы без регистрации: комната по секретной ссылке, деление поровну, по долям, суммам и процентам, расчёт «кто кому» не более n−1 переводов. Пет-проект.",[728,729,730,731],"Без аккаунтов: комната по секретной ссылке в 128 бит, отдельные ссылки «просмотр» и «редактирование»","Четыре способа деления и мультивалюта с ручным курсом; расчёт «кто кому» не более n−1 переводов","Оптимистичная блокировка: из 4 одновременных правок одного расхода проходит одна","60 тестов (vitest в workerd, property-тесты fast-check), CI зелёный, npm audit 0, axe 0 нарушений","\u002Fog\u002Fbudget-split.jpg",{"desktop":734,"desktop_sm":104,"mobile":735,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fbudget-split-desktop.webp","\u002Fshots\u002Fbudget-split-mobile.webp",{"slug":737,"title":738,"type":527,"category":50,"lang":59,"live_url":739,"stack":740,"description":743,"highlights":744,"run":561,"hue":100,"og":749,"shots":750},"kladka-site","kladka-site: «Кладка», демо-сайт студии каркасного строительства","https:\u002F\u002Fewsirdev-kladka.pages.dev",[741,59,742,512,725,79,602,47],"Astro","Content collections","Демо-сайт вымышленной студии каркасного строительства: 5 проектов с чертежами, услуги, цены с калькулятором-ориентиром, журнал. Компания, цены и сроки выдуманы, на каждой странице есть пометка.",[745,746,747,748],"24 страницы (19 RU, 4 EN, 404) на Astro 7 со статической сборкой и content collections","Все 20 чертежей рисует код: SVG по данным проекта; 24 картинки OG рендерятся скриптом","Строгий CSP без inline, robots, sitemap с hreflang, llms.txt, security.txt, JSON-LD Organization\u002FService\u002FArticle\u002FFAQPage","756 ссылок и якорей без ошибок, htmlhint 0, axe 0 по всем страницам на 390 и 1280, npm audit 0","\u002Fog\u002Fkladka-site.jpg",{"desktop":751,"desktop_sm":752,"mobile":753,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fkladka-site-desktop.webp","\u002Fshots\u002Fkladka-site-desktop-sm.webp","\u002Fshots\u002Fkladka-site-mobile.webp",{"slug":755,"title":756,"type":109,"category":109,"lang":123,"live_url":757,"stack":758,"description":764,"highlights":765,"run":770,"hue":119,"og":771,"shots":772},"rate-gate","rate-gate: ограничитель частоты запросов","https:\u002F\u002Frate-gate.ewsir.space",[123,759,760,761,762,763,300,142,47],"axum","tokio","proptest","musl","distroless","HTTP-сервис на Rust: один вызов POST \u002Fv1\u002Fcheck отвечает, можно ли пропустить действие. Два алгоритма: токен-бакет на целочисленных милли-токенах (всплески) и скользящее окно по двум счётчикам (строгие лимиты). Правила в JSON, выбор по префиксу ключа, метрики Prometheus, снапшот состояния, страница-демо. Состояние в памяти одного экземпляра: реплики его не делят.",[766,767,768,769],"Токен-бакет без float: милли-токены, cost больше 1, расчёт retry_after_ms и reset_ms","Property-тесты (proptest): за любой период бакет выдаёт не больше capacity + refill * elapsed","20 тестов; образ 8,2 МБ, 3,8 МиБ RAM в покое и 3,9 МиБ после 200 запросов","Контейнер uid 10001, read-only, cap-drop ALL, CSP script-src 'self'","cd rate-gate && cargo run --release","\u002Fog\u002Frate-gate.jpg",{"desktop":773,"desktop_sm":774,"mobile":775,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Frate-gate-desktop.webp","\u002Fshots\u002Frate-gate-desktop-sm.webp","\u002Fshots\u002Frate-gate-mobile.webp",{"slug":777,"title":778,"type":109,"category":109,"lang":123,"live_url":779,"stack":780,"description":783,"highlights":784,"run":789,"hue":790,"og":791,"shots":792},"hook-relay","hook-relay: доставка вебхуков","https:\u002F\u002Fhook-relay.ewsir.space",[123,759,760,143,781,782,761,762,763,142,47],"HMAC-SHA256","SSRF","Сервис доставки вебхуков на Rust: принимает событие, подписывает его HMAC-SHA256, отправляет получателю и не теряет. При сбое повторяет с экспоненциальной задержкой и джиттером, после лимита попыток откладывает в dead-letter, где доставку можно вернуть в очередь вручную. Адрес получателя проверяется на SSRF после DNS: все полученные адреса должны быть публичными. Идемпотентность по Idempotency-Key, состояние в SQLite.",[785,786,787,788],"SSRF-проверка после разрешения DNS: loopback, приватные, link-local, CGNAT, ULA, IPv4-mapped, NAT64 и 6to4 блокируются","HMAC-подпись «t.тело» у каждого получателя свой секрет; ретраи с джиттером, dead-letter, ручной повтор","20 тестов, в том числе proptest: подпись не проходит после изменения любого байта, любой приватный IPv4 блокируется","Образ 14,4 МБ; 3,5 МиБ в покое, 5,5 МиБ после 200 событий с реальной доставкой","cd hook-relay && cargo run --release",28,"\u002Fog\u002Fhook-relay.jpg",{"desktop":793,"desktop_sm":794,"mobile":795,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fhook-relay-desktop.webp","\u002Fshots\u002Fhook-relay-desktop-sm.webp","\u002Fshots\u002Fhook-relay-mobile.webp",{"slug":797,"title":798,"type":109,"category":109,"lang":123,"live_url":799,"stack":800,"description":802,"highlights":803,"run":808,"hue":168,"og":809,"shots":810},"flag-store","flag-store: фича-флаги с процентным rollout","https:\u002F\u002Fflag-store.ewsir.space",[123,759,760,143,281,801,761,762,763,142,47],"SHA-256","Сервис фича-флагов на Rust: флаги по окружениям, правила по атрибутам пользователя, процентный rollout без случайности, журнал аудита и SSE-поток изменений. Корзина пользователя это первые 8 байт SHA-256 от флага и id по модулю 10000, поэтому ответ стабилен, а рост процента никого не выключает. Доли в базисных пунктах, float нигде нет.",[804,805,806,807],"Детерминированный rollout: SHA-256(флаг, id) по модулю 10000; один пользователь всегда получает один ответ","3 property-теста: стабильность, монотонность по проценту, равномерность на 20 000 пользователей с допуском 2 п.п.","15 тестов; образ 11,6 МБ, 2,7 МиБ RAM в покое и 3,5 МиБ после 200 evaluate","SSE-поток изменений с hello, change и resync; журнал аудита с автором и состоянием до и после","cd flag-store && cargo run --release","\u002Fog\u002Fflag-store.jpg",{"desktop":811,"desktop_sm":812,"mobile":813,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fflag-store-desktop.webp","\u002Fshots\u002Fflag-store-desktop-sm.webp","\u002Fshots\u002Fflag-store-mobile.webp",{"slug":815,"title":816,"type":109,"category":109,"lang":123,"live_url":817,"stack":818,"description":820,"highlights":821,"run":826,"hue":827,"og":828,"shots":829},"ledger-core","ledger-core: двойная запись и хэш-цепочка журнала","https:\u002F\u002Fledger-core.ewsir.space",[123,759,143,801,761,762,763,142,819,47],"cargo-audit","Бухгалтерское ядро с двойной записью на Rust: счета, проводки, неизменяемый журнал с хэш-цепочкой, проверка целостности и оборотно-сальдовая ведомость. Деньги только целыми числами (i64, копейки), сумма проводки строго 0, арифметика checked. Каждая запись хранит prev_hash и SHA-256 от своих полей, GET \u002Fv1\u002Fverify пересчитывает цепочку и указывает подменённую запись. Идемпотентность по Idempotency-Key.",[822,823,824,825],"Инвариант проводки: сумма строк 0, от 2 до 100 строк, одна валюта; переполнение даёт 422, а не панику","Хэш-цепочка: любая подмена суммы, комментария или счёта ловится, и указывается именно подменённая запись","19 тестов, property-тесты: сумма проводки и сальдо всех счетов всегда 0","Образ 11,4 МБ; 3,5 МиБ в покое и 4,5 МиБ после 200 запросов, цепочка осталась целой","cd ledger-core && cargo run --release",52,"\u002Fog\u002Fledger-core.jpg",{"desktop":830,"desktop_sm":831,"mobile":832,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fledger-core-desktop.webp","\u002Fshots\u002Fledger-core-desktop-sm.webp","\u002Fshots\u002Fledger-core-mobile.webp",{"slug":834,"title":835,"type":109,"category":109,"lang":121,"live_url":836,"stack":837,"description":842,"highlights":843,"run":848,"hue":480,"og":849,"shots":850},"csv-doctor","csv-doctor: профиль и очистка CSV","https:\u002F\u002Fcsv-doctor.ewsir.space",[121,653,838,839,840,618,841,142,47],"uvicorn","hypothesis","mypy --strict","pip-audit","Сервис проверки и очистки CSV на Python (FastAPI, без pandas). Загружаете файл до 5 МБ: определяются кодировка (utf-8, cp1251, koi8-r, UTF-16 с BOM) и разделитель, строится профиль колонок и список проблем с номерами строк и подсказками, затем можно скачать очищенный CSV. Опасные ячейки (=, +, -, @) ловятся как CSV-injection. Файл разбирается в памяти и нигде не сохраняется.",[844,845,846,847],"Автоопределение кодировки и разделителя, профиль колонок, выбросы по 1.5·IQR, неоднозначные даты","Защита от CSV-injection на входе (ошибка) и на выходе очистки (апостроф), телефоны +7 не ломаются","43 теста; hypothesis: круговой проход кодировок, идемпотентность очистки clean(clean(x)) == clean(x)","CI: ruff, mypy --strict, pytest --cov, pip-audit по хэш-закреплённому requirements.lock; файл 3,8 МБ (95 000 строк) разбирается за 1,5 с","cd csv-doctor && python -m app","\u002Fog\u002Fcsv-doctor.jpg",{"desktop":851,"desktop_sm":852,"mobile":853,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fcsv-doctor-desktop.webp","\u002Fshots\u002Fcsv-doctor-desktop-sm.webp","\u002Fshots\u002Fcsv-doctor-mobile.webp",{"slug":855,"title":856,"type":109,"category":109,"lang":121,"live_url":857,"stack":858,"description":860,"highlights":861,"run":866,"hue":353,"og":867,"shots":868},"cron-clock","cron-clock: объяснение cron и ближайшие запуски","https:\u002F\u002Fcron-clock.ewsir.space",[121,653,838,859,839,840,618,841,142,47],"zoneinfo","Сервис разбора cron-выражений на Python (FastAPI, только стандартная библиотека и zoneinfo). Объясняет выражение по-русски и по-английски, считает ближайшие запуски в заданном часовом поясе с учётом перехода на летнее время, предупреждает об «опасных» расписаниях и сравнивает два выражения. Поддержаны L, W, #, шесть полей с секундами.",[862,863,864,865],"Ближайшие запуски с DST: границы Europe\u002FBerlin 2024-03-31 и 2024-10-27, пояс Lord Howe с получасовым переходом","64 теста; hypothesis по 150 примеров на свойство: запуск удовлетворяет выражению, нет пропущенной минуты (проверка перебором)","Сравнение `* * * * * *` против `* * * * *` на 400 дней за 0,6 с","\u002Fapi\u002Fparse: около 800 запросов в секунду, p95 35 мс; 37 МиБ RAM в покое","cd cron-clock && python -m app","\u002Fog\u002Fcron-clock.jpg",{"desktop":869,"desktop_sm":870,"mobile":871,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fcron-clock-desktop.webp","\u002Fshots\u002Fcron-clock-desktop-sm.webp","\u002Fshots\u002Fcron-clock-mobile.webp",{"slug":873,"title":874,"type":109,"category":109,"lang":121,"live_url":875,"stack":876,"description":879,"highlights":880,"run":885,"hue":886,"og":887,"shots":888},"ical-forge","ical-forge: iCalendar (RFC 5545), RRULE и подписка","https:\u002F\u002Fical-forge.ewsir.space",[121,653,838,143,877,878,839,840,618,142,47],"RFC 5545","ETag","Сервис iCalendar на Python (FastAPI + SQLite, без сторонних библиотек для календаря). Собирает корректный .ics (складывание строк, экранирование, VTIMEZONE из zoneinfo), проверяет чужой файл с номерами строк, разворачивает повторяющиеся события по RRULE и публикует ленты для подписки по ссылке с ETag и ответом 304.",[881,882,883,884],"28 примеров повторов из RFC 5545 §3.8.5.3 сверяются по спискам дат (BYSETPOS, «предпоследний понедельник», пятница 13-е с EXDATE)","119 тестов; hypothesis: круговой проход событие → .ics → событие на произвольном Unicode, unfold(fold(x)) == x, строки не длиннее 75 октетов","POST \u002Fapi\u002Fexpand на 500 вхождений за 0,2 с; лента около 640 запросов в секунду, p95 45 мс","Ключ редактирования хранится только как SHA-256, сравнение за постоянное время","cd ical-forge && python -m app",280,"\u002Fog\u002Fical-forge.jpg",{"desktop":889,"desktop_sm":890,"mobile":891,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fical-forge-desktop.webp","\u002Fshots\u002Fical-forge-desktop-sm.webp","\u002Fshots\u002Fical-forge-mobile.webp",{"slug":893,"title":894,"type":109,"category":109,"lang":59,"live_url":895,"stack":896,"description":898,"highlights":899,"run":904,"hue":905,"og":906,"shots":907},"poll-board","poll-board: опросы в реальном времени","https:\u002F\u002Fpoll-board.ewsir.space",[59,897,281,320,676,107,79,142,47],"Node.js 24","Сервис опросов и голосований на TypeScript без runtime-зависимостей. Опрос на 2–10 вариантов со сроком жизни, ссылка, результаты растут у всех в реальном времени по SSE или WebSocket (кадры WebSocket разобраны вручную). Повторные голоса и накрутка режутся подписанным токеном и хэшем IP; экспорт CSV защищён от формульных ячеек.",[900,901,902,903],"SSE и WebSocket на одном потоке событий; порядок монотонен: seq растёт на 1, closed последним","43 теста; fast-check: сумма голосов равна числу принятых бюллетеней, кадры WebSocket при любой нарезке потока","200 запросов (20 параллельно): p50 6,4 мс, p95 21,7 мс; около 25 МиБ RAM","Запуск read-only, cap-drop ALL, uid 10001; axe 0 в Firefox на 390 и 1280","cd poll-board && npm ci && npm run build && npm start",330,"\u002Fog\u002Fpoll-board.jpg",{"desktop":908,"desktop_sm":909,"mobile":910,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fpoll-board-desktop.webp","\u002Fshots\u002Fpoll-board-desktop-sm.webp","\u002Fshots\u002Fpoll-board-mobile.webp",{"slug":912,"title":913,"type":109,"category":109,"lang":59,"live_url":914,"stack":915,"description":918,"highlights":919,"run":924,"hue":925,"og":926,"shots":927},"md-safe","md-safe: Markdown-парсер и санитайзер","https:\u002F\u002Fmd-safe.ewsir.space",[59,897,694,916,917,676,107,79,142,47],"XSS","allowlist","API и веб-демо безопасного рендера Markdown в HTML на TypeScript. Парсер подмножества CommonMark написан с нуля, результат всегда проходит санитайзер по allowlist (тоже свой): готовых markdown-it, marked и dompurify в проекте нет. Проверка: корпус из 242 XSS-векторов и независимый проверяющий вывода, который не использует код санитайзера.",[920,921,922,923],"242 XSS-вектора (OWASP, PortSwigger, HTML5 Security Cheatsheet) прогоняются как HTML и как Markdown в нескольких режимах","Независимый проверяющий вывода: строгий шаблон, любой лишний тег, атрибут или схема считаются нарушением","829 тестов, в том числе fast-check: вывод проходит проверяющего, санитайзер идемпотентен","Тяжёлые входы (60 тысяч «[», 100 тысяч «\u003Cb>») обрабатываются быстрее 3 секунд: разбор линейный","cd md-safe && npm ci && npm run build && npm start",240,"\u002Fog\u002Fmd-safe.jpg",{"desktop":928,"desktop_sm":929,"mobile":930,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fmd-safe-desktop.webp","\u002Fshots\u002Fmd-safe-desktop-sm.webp","\u002Fshots\u002Fmd-safe-mobile.webp",{"slug":182,"title":932,"type":109,"category":109,"lang":59,"live_url":933,"stack":934,"description":937,"highlights":938,"run":943,"hue":944,"og":945,"shots":946},"qr-svg: QR-коды с нуля","https:\u002F\u002Fqr-svg.ewsir.space",[59,897,935,936,512,676,107,79,142,47],"Reed-Solomon","GF(256)","Генератор QR-кодов в SVG на TypeScript без единой runtime-зависимости: весь стандарт ISO\u002FIEC 18004 для версий 1–40 написан в src\u002Fqr\u002F (поле GF(256), Рид-Соломон, чередование блоков, 8 масок со штрафами). API отдаёт SVG, веб-демо показывает предпросмотр, скачивает SVG и PNG и проверяет контраст цветов по WCAG.",[939,940,941,942],"Версии 1–40, уровни L, M, Q, H; эталонные примеры ISO «01234567» и «HELLO WORLD» совпадают по байтам коррекции","Матрицы сверены бит в бит с независимой реализацией qrcode-generator на 12 текстах × 4 уровня и 80 случайных строках","Все 160 сочетаний «версия × уровень» читаются сторонним декодером jsQR, кроме 23-L (его не читает и эталон); есть и собственный декодер","Образ 244 МБ; запуск read-only, cap-drop ALL, uid 10001","cd qr-svg && npm ci && npm run build && npm start",100,"\u002Fog\u002Fqr-svg.jpg",{"desktop":947,"desktop_sm":948,"mobile":949,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fqr-svg-desktop.webp","\u002Fshots\u002Fqr-svg-desktop-sm.webp","\u002Fshots\u002Fqr-svg-mobile.webp",{"slug":951,"title":952,"type":527,"category":50,"lang":59,"live_url":953,"stack":954,"description":959,"highlights":960,"run":965,"hue":966,"og":967,"shots":968},"bike-workshop-react","bike-workshop-react: «Ось», концепт сайта веломастерской","https:\u002F\u002Fewsirdev-bike.pages.dev",[955,956,59,106,957,958,79,78,47],"React 19","React Router","Prerender","CSP","Концепт многостраничного сайта вымышленной веломастерской на React 19 с пререндером каждой страницы в статический HTML: услуги, прайс-демо с калькулятором, запись с расписанием, журнал из трёх статей. Мастерской, клиентов и реальных цен не существует, форма записи работает в браузере и ничего не отправляет.",[961,962,963,964],"React 19 + React Router + Vite, каждая страница пререндерится в плоский .html (адреса без слэша без редиректа на Pages)","CSP script-src 'self' и style-src 'self' без unsafe-inline: check:security проваливает сборку при inline-коде","Фирменные приёмы: линейка как разделитель, бирка с отрывным корешком для смет и записи","Firefox 390, 768, 1280, 1920: axe 0, CLS 0, горизонтального скролла и ошибок консоли нет","cd bike-workshop-react && npm ci && npm run dev",18,"\u002Fog\u002Fbike-workshop-react.jpg",{"desktop":969,"desktop_sm":970,"mobile":971,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fbike-workshop-react-desktop.webp","\u002Fshots\u002Fbike-workshop-react-desktop-sm.webp","\u002Fshots\u002Fbike-workshop-react-mobile.webp",{"slug":973,"title":974,"type":527,"category":50,"lang":59,"live_url":975,"stack":976,"description":978,"highlights":979,"run":984,"hue":985,"og":986,"shots":987},"landscape-bureau-nuxt","landscape-bureau-nuxt: «Изолиния», концепт сайта ландшафтного бюро","https:\u002F\u002Fewsirdev-landscape.pages.dev",[467,468,59,977,512,958,79,78,47],"Static generation","Концепт многостраничного сайта вымышленного ландшафтного бюро на Nuxt 4 со статической генерацией: шесть страниц проектов по динамическому маршруту, фильтр растений по свету, влажности и форме, бриф-форма. Данные лежат в JSON, планы участков рисует код. Бюро, заказчиков и площади выдуманы.",[980,981,982,983],"Nuxt 4 generate: все динамические маршруты пререндерены","CSP без unsafe-inline на Nuxt: выключены entryImportMap и инлайн-стили, window.__NUXT__ вынесен в файл _nuxt\u002Finline-\u003Chash>.js, check-security проваливает сборку при inline-скрипте","Планы участков генерирует код (SVG по данным проекта)","Firefox 390, 768, 1280, 1920: axe, CLS, горизонтальный скролл, консоль проверены","cd landscape-bureau-nuxt && npm ci && npm run dev",130,"\u002Fog\u002Flandscape-bureau-nuxt.jpg",{"desktop":988,"desktop_sm":989,"mobile":990,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Flandscape-bureau-nuxt-desktop.webp","\u002Fshots\u002Flandscape-bureau-nuxt-desktop-sm.webp","\u002Fshots\u002Flandscape-bureau-nuxt-mobile.webp",{"slug":992,"title":993,"type":527,"category":50,"lang":59,"live_url":994,"stack":995,"description":998,"highlights":999,"run":1004,"hue":173,"og":1005,"shots":1006},"letterpress-ts","letterpress-ts: «Реглет», концепт сайта печатной мастерской","https:\u002F\u002Fewsirdev-letterpress.pages.dev",[59,106,996,997,45,79,78,47],"Multi-page","DOM","Концепт многостраничного сайта вымышленной мастерской высокой печати на чистом TypeScript без фреймворка: Vite multi-page с 8 HTML-входами, компоненты как типизированные функции, возвращающие DOM. Конструктор набора превращает текст в литеры и оттиск, калькулятор считает тираж, состояние хранится в ссылке. Мастерской и ценам верить не нужно: суммы по демо-формулам.",[1000,1001,1002,1003],"Без фреймворка: 8 HTML-входов Vite, компоненты как функции, возвращающие DOM","Конструктор набора: гарнитура, краска, бумага, кегль, шпации; состояние в ссылке и проверка параметров","Таблица цен на главной считается при сборке теми же формулами, что калькулятор","Пользовательский текст попадает в DOM через textContent, innerHTML запрещён линтером","cd letterpress-ts && npm ci && npm run dev","\u002Fog\u002Fletterpress-ts.jpg",{"desktop":1007,"desktop_sm":1008,"mobile":1009,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fletterpress-ts-desktop.webp","\u002Fshots\u002Fletterpress-ts-desktop-sm.webp","\u002Fshots\u002Fletterpress-ts-mobile.webp",{"slug":1011,"title":1012,"type":527,"category":50,"lang":59,"live_url":1013,"stack":1014,"description":1016,"highlights":1017,"run":1022,"hue":456,"og":1023,"shots":1024},"ceramics-vue","ceramics-vue: «Муфель», концепт сайта керамической студии","https:\u002F\u002Fewsirdev-ceramics.pages.dev",[468,549,59,106,1015,512,958,79,78,47],"vite-ssg","Концепт сайта вымышленной керамической студии на Vue 3 с пререндером каждой страницы в статический HTML (vite-ssg): 7 страниц, 5 динамических страниц курсов и 404. Расписание с фильтрами, запись на пробное через mailto, калькулятор обжига с кривой в SVG. Студии, отзывов и цен не существует, формулы помечены «демо».",[1018,1019,1020,1021],"Vue 3 + vite-ssg: 7 страниц и 5 страниц курсов пререндерены в HTML","Калькулятор обжига: объём, вес, сушка, режим, длительность и кривая SVG по демо-формулам","CSP script-src 'self' без inline-скриптов; check-security проваливает сборку при inline-коде или стиле","Firefox 390, 768, 1280, 1920: axe 0, CLS 0, консоль чистая; CI: lint, typecheck, test, build, check:links, npm audit, gitleaks","cd ceramics-vue && npm ci && npm run dev","\u002Fog\u002Fceramics-vue.jpg",{"desktop":1025,"desktop_sm":1026,"mobile":1027,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fceramics-vue-desktop.webp","\u002Fshots\u002Fceramics-vue-desktop-sm.webp","\u002Fshots\u002Fceramics-vue-mobile.webp",{"slug":1029,"title":1030,"type":547,"category":81,"lang":59,"live_url":1031,"stack":1032,"description":1035,"highlights":1036,"run":1041,"hue":1042,"og":1043,"shots":1044},"habit-tracker","habit-tracker: «Привычка», офлайн-трекер привычек (демо)","https:\u002F\u002Fewsirdev-habit.pages.dev",[1033,59,106,711,1034,676,107,79,78,47],"Svelte 5","Service Worker","Демо-приложение: трекер привычек на Svelte 5, работает офлайн (PWA), без аккаунта и сервера, данные остаются в браузере. Серии с льготным днём, частота «каждый день», по дням недели или N раз в неделю, тепловая карта года на SVG, напоминания через Notification API, экспорт и импорт JSON со строгой проверкой схемы. День хранится строкой в поясе пользователя, поэтому перелёты и летнее время историю не меняют.",[1037,1038,1039,1040],"Серии с льготным днём: один изолированный пропуск не рвёт серию, не чаще раза в 7 дней","Дни как строки ГГГГ-ММ-ДД в поясе пользователя; арифметика через UTC-полночь; границу суток можно сдвинуть","Офлайн: service worker с версионированным кэшем; CSP connect-src 'self', внешних запросов нет","29 тестов vitest, в том числе fast-check; кнопки не меньше 44 px, axe 0","cd habit-tracker && npm ci && npm run dev",75,"\u002Fog\u002Fhabit-tracker.jpg",{"desktop":1045,"desktop_sm":1046,"mobile":1047,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fhabit-tracker-desktop.webp","\u002Fshots\u002Fhabit-tracker-desktop-sm.webp","\u002Fshots\u002Fhabit-tracker-mobile.webp",{"slug":1049,"title":1050,"type":547,"category":81,"lang":59,"live_url":1051,"stack":1052,"description":1054,"highlights":1055,"run":1060,"hue":1061,"og":1062,"shots":1063},"flashcards-sm2","flashcards-sm2: «Карточки», картотека с интервальным повторением SM-2","https:\u002F\u002Fewsirdev-cards.pages.dev",[468,59,106,709,711,1053,676,107,79,78,47],"SM-2","Концепт-приложение на Vue 3: колоды карточек и повторение по алгоритму SM-2, офлайн и без бэкенда, данные в IndexedDB. Импорт CSV\u002FTSV сам определяет разделитель и кодировку (UTF-8, UTF-16 по BOM, Windows-1251), экспорт защищён от CSV-injection, есть полный JSON-бэкап со строгой валидацией. Статистика на SVG: прогноз на 30 дней, зрелость карточек, ретеншен.",[1056,1057,1058,1059],"SM-2 по описанию SuperMemo-2: EF от 1.3, интервалы 1, 6 и далее round(prev·EF), отличия от оригинала описаны","Расписание хранится датами, а не метками времени: пояса и летнее время ничего не сдвигают; границу суток можно перенести","Импорт CSV с автоопределением кодировки и разделителя, лимиты 1 МБ и 5000 строк","30 тестов vitest + fast-check; переворот карточки на чистом CSS, клавиатура (пробел, 1–4), axe 0","cd flashcards-sm2 && npm ci && npm run dev",300,"\u002Fog\u002Fflashcards-sm2.jpg",{"desktop":1064,"desktop_sm":1065,"mobile":1066,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fflashcards-sm2-desktop.webp","\u002Fshots\u002Fflashcards-sm2-desktop-sm.webp","\u002Fshots\u002Fflashcards-sm2-mobile.webp",{"slug":1068,"title":1069,"type":547,"category":81,"lang":59,"live_url":1070,"stack":1071,"description":1074,"highlights":1075,"run":1080,"hue":1081,"og":1082,"shots":1083},"scratch-notes","scratch-notes: «Черновик», офлайн-редактор заметок в Markdown","https:\u002F\u002Fewsirdev-scratch.pages.dev",[1072,59,106,709,1073,711,676,107,79,78,47],"SolidJS","WebCrypto","Демо-концепт офлайн-редактора заметок в Markdown на SolidJS: данные в IndexedDB, сервера и аккаунтов нет. Живой предпросмотр, собственный полнотекстовый поиск (BM25, стемминг RU\u002FEN, исправление раскладки), история правок с построчным diff, шифрование отдельной заметки паролем (PBKDF2 600 000 итераций + AES-256-GCM, WebCrypto), импорт и экспорт .md и .zip со своим ZIP-писателем.",[1076,1077,1078,1079],"Парсер Markdown и санитайзер скопированы из md-safe вместе с корпусом XSS-векторов и независимым проверяющим","Шифрование: PBKDF2-HMAC-SHA-256 600 000 итераций, AES-256-GCM, новый IV на каждое сохранение, шифруются и снимки истории; модель угроз описана","Поиск: свой индекс в памяти, BM25 с весами полей, префикс, «фразы», #теги","777 тестов (в том числе корпус XSS); горячие клавиши по e.code на любой раскладке; axe 0","cd scratch-notes && npm ci && npm run dev",55,"\u002Fog\u002Fscratch-notes.jpg",{"desktop":1084,"desktop_sm":1085,"mobile":1086,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fscratch-notes-desktop.webp","\u002Fshots\u002Fscratch-notes-desktop-sm.webp","\u002Fshots\u002Fscratch-notes-mobile.webp",{"slug":1088,"title":1089,"type":145,"category":145,"lang":123,"stack":1090,"description":1093,"highlights":1094,"run":1099,"hue":1100,"og":1101,"shots":1102},"dupfind","dupfind: быстрый поиск дубликатов файлов",[123,1091,1092,761,175,47,142],"BLAKE3","rayon","CLI на Rust для поиска дубликатов файлов. Файлы группируются по размеру, затем по частичному хэшу (первые и последние 4 КиБ) и только потом по полному BLAKE3; обход и хэширование параллельные. По умолчанию программа только показывает результат; удаление, жёсткие ссылки и перенос включаются --apply и перепроверяют хэш перед действием. Жёсткие ссылки одного файла дубликатами не считаются, симлинки не обходятся.",[1095,1096,1097,1098],"Демо-каталог 10 000 файлов (307 МБ, треть копии): около 120 мс на 20 потоках, около 200 мс на одном; конвейер find + sha256sum за около 1130 мс","Размер, частичный хэш, полный BLAKE3: уникальные размеры отбрасываются без чтения","Режимы --delete-newer, --hardlink, --move-to по умолчанию dry-run; код выхода 3 для CI","18 интеграционных тестов и proptest: группы в точности равны классам одинакового содержимого","cd dupfind && cargo build --release && .\u002Ftarget\u002Frelease\u002Fdupfind --help",22,"\u002Fog\u002Fdupfind.jpg",{"desktop":1103,"desktop_sm":1104,"mobile":1105,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fdupfind-desktop.webp","\u002Fshots\u002Fdupfind-desktop-sm.webp","\u002Fshots\u002Fdupfind-mobile.webp",{"slug":1107,"title":1108,"type":145,"category":145,"lang":118,"stack":1109,"description":1113,"highlights":1114,"run":1119,"hue":1120,"og":1121,"shots":1122},"sqlcsv","sqlcsv: SQL-запросы к CSV из командной строки",[118,176,1110,1111,634,1112,47],"SQL","CSV","SQLite (oracle)","CLI на Go: SQL-запросы к CSV и TSV. Один статический бинарник, только стандартная библиотека: собственный лексер, парсер и интерпретатор. Потоковая обработка: SELECT, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, JOIN двух файлов (hash join), подсказки «did you mean» в ошибках, --explain и защита вывода от CSV-injection (--csv-safe).",[1115,1116,1117,1118],"Демо-CSV на 1 000 000 строк (39 МБ): агрегат с GROUP BY за 0,23 с и 13 МБ памяти; ранняя остановка по LIMIT за 0,03 с","Oracle-тест: 600 случайных запросов сверяются с SQLite (отдельный модуль), числа с точностью 1e-9","Fuzz парсера и движка: любая строка как запрос не вызывает панику","Ошибки с позицией и подсказкой имени колонки","cd sqlcsv && go run . \"SELECT * FROM testdata\u002Fdemo-sales.csv\"",195,"\u002Fog\u002Fsqlcsv.jpg",{"desktop":1123,"desktop_sm":1124,"mobile":1125,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Fsqlcsv-desktop.webp","\u002Fshots\u002Fsqlcsv-desktop-sm.webp","\u002Fshots\u002Fsqlcsv-mobile.webp",{"slug":1127,"title":1128,"type":145,"category":145,"lang":59,"stack":1129,"description":1133,"highlights":1134,"run":1139,"hue":1140,"og":1141,"shots":1142},"lockweigh","lockweigh: «взвешивает» lock-файл",[59,691,1130,1131,1132,107,47],"esbuild","Node SEA","YAML parser","CLI на TypeScript без runtime-зависимостей: читает package-lock.json (v2, v3), pnpm-lock.yaml (v9) и yarn.lock (classic v1) и показывает, сколько в проекте пакетов, кто тянет дубликаты версий, какие поддеревья самые тяжёлые, где install-скрипты и какие лицензии, а также почему пакет попал в проект (why) и что изменилось между двумя lock-файлами (diff). Парсеры, включая подмножество YAML, написаны с нуля. Работает офлайн и ничего не выполняет.",[1135,1136,1137,1138],"npm, pnpm и yarn в одном отчёте; на фикстуре express: 73 пакета, глубина графа 7, 2 дубликата версий","Команды why (все пути от корня) и diff двух lock-файлов; коды возврата для CI с порогами","Один бандл esbuild и одиночный исполняемый файл через Node SEA","77 тестов vitest; в runtime-зависимостях пусто, поэтому цепочка поставки это dev-инструменты","cd lockweigh && npm ci && npm run build && node dist\u002Flockweigh.cjs --help",340,"\u002Fog\u002Flockweigh.jpg",{"desktop":1143,"desktop_sm":1144,"mobile":1145,"dw":202,"dh":203,"mw":204,"mh":205},"\u002Fshots\u002Flockweigh-desktop.webp","\u002Fshots\u002Flockweigh-desktop-sm.webp","\u002Fshots\u002Flockweigh-mobile.webp",1793371809443]