[{"data":1,"prerenderedAt":1563},["ShallowReactive",2],{"api:\u002Fapi\u002Fconfig:ru":3,"api:\u002Fapi\u002Fprofile:":8,"api:\u002Fapi\u002Fprojects:ru":19,"api:\u002Fapi\u002Fcategories:ru":652,"api:\u002Fapi\u002Fengineering:ru":725},{"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,46,63,79,102,120,137,156,174,194,219,242,267,294,320,345,365,389,414,440,456,474,493,513,534,553,573,596,617,634],{"slug":21,"title":22,"type":23,"category":23,"lang":24,"kind":25,"stack":26,"description":29,"highlights":30,"run":35,"hue":36,"og":37,"shots":38},"kanban-tasks","Янтарь: канбан-доска","backend","Go","api",[24,27,28],"SQLite","Docker","Многопользовательская канбан-доска: регистрация, доски, колонки, карточки с перетаскиванием. Go + SQLite без CGO, фронт встроен в бинарник.",[31,32,33,34],"PBKDF2 и сессии в HttpOnly-cookie","Изоляция данных пользователей","Тесты API","Docker multi-stage","cd kanban-tasks && go run .",344,"\u002Fog\u002Fkanban-tasks.jpg",{"desktop":39,"desktop_sm":40,"mobile":41,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Fkanban-tasks-desktop.webp","\u002Fshots\u002Fkanban-tasks-desktop-sm.webp","\u002Fshots\u002Fkanban-tasks-mobile.webp",1280,800,390,844,{"slug":47,"title":48,"type":23,"category":23,"lang":24,"kind":25,"stack":49,"description":50,"highlights":51,"run":56,"hue":57,"og":58,"shots":59},"link-shortener","Короче: сократитель ссылок",[24,27,28],"Сокращение ссылок с алиасами и сроком жизни, статистика кликов по дням, браузерам и устройствам, дашборд под токеном. IP не хранятся.",[52,53,54,55],"Редирект и учёт кликов","Лимит запросов","Приватная статистика (хэш с солью)","Тесты и Docker","cd link-shortener && set SEED_DEMO=1&& go run .\u002Fcmd\u002Fserver",111,"\u002Fog\u002Flink-shortener.jpg",{"desktop":60,"desktop_sm":61,"mobile":62,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Flink-shortener-desktop.webp","\u002Fshots\u002Flink-shortener-desktop-sm.webp","\u002Fshots\u002Flink-shortener-mobile.webp",{"slug":64,"title":65,"type":23,"category":23,"lang":24,"kind":25,"stack":66,"description":67,"highlights":68,"run":72,"hue":73,"og":74,"shots":75},"booking-slots","Запись на приём",[24,27,28],"Онлайн-запись на услуги: свободные слоты с учётом длительности и рабочих часов, защита от двойной записи, админ-панель. Вымышленная студия, демо.",[69,70,71,55],"Расчёт свободных слотов","Без двойных записей","Админка для услуг и расписания","cd booking-slots && go run .",65,"\u002Fog\u002Fbooking-slots.jpg",{"desktop":76,"desktop_sm":77,"mobile":78,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Fbooking-slots-desktop.webp","\u002Fshots\u002Fbooking-slots-desktop-sm.webp","\u002Fshots\u002Fbooking-slots-mobile.webp",{"slug":80,"title":81,"type":82,"category":83,"live_url":84,"stack":85,"description":89,"highlights":90,"run":95,"hue":96,"og":97,"shots":98},"landing-studio","Полутон: студия света","landing","landings","https:\u002F\u002Fewsirdev-studio.pages.dev",[86,87,88],"HTML","CSS","JS","Лендинг вымышленной студии интерьерного света: тёплая палитра, засечный шрифт, SVG-иллюстрация, FAQ и демо-форма. Адаптивный, без внешних картинок.",[91,92,93,94],"Адаптив 390-1280","SVG вместо картинок","Доступные FAQ и форма","Честная демо-форма","cd landing-studio && python -m http.server 8091",212,"\u002Fog\u002Flanding-studio.jpg",{"desktop":99,"desktop_sm":100,"mobile":101,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Flanding-studio-desktop.webp","\u002Fshots\u002Flanding-studio-desktop-sm.webp","\u002Fshots\u002Flanding-studio-mobile.webp",{"slug":103,"title":104,"type":82,"category":83,"live_url":105,"stack":106,"description":107,"highlights":108,"run":113,"hue":114,"og":115,"shots":116},"landing-saas","Рубильник: трекер подписок","https:\u002F\u002Fewsirdev-saas.pages.dev",[86,87,88],"Лендинг вымышленного SaaS-продукта: тёмный градиентный hero, макет интерфейса на CSS, тарифы, FAQ, демо-форма. Все цены и названия придуманы.",[109,110,111,112],"Макет интерфейса на CSS","Тарифная сетка","Адаптив","Демо-форма без отправки данных","cd landing-saas && python -m http.server 8092",182,"\u002Fog\u002Flanding-saas.jpg",{"desktop":117,"desktop_sm":118,"mobile":119,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Flanding-saas-desktop.webp","\u002Fshots\u002Flanding-saas-desktop-sm.webp","\u002Fshots\u002Flanding-saas-mobile.webp",{"slug":121,"title":122,"type":82,"category":83,"live_url":123,"stack":124,"description":125,"highlights":126,"run":130,"hue":131,"og":132,"shots":133},"landing-event","Малый масштаб: конференция","https:\u002F\u002Fewsirdev-event.pages.dev",[86,87,88],"Лендинг вымышленной однодневной конференции: плакатная типографика, расписание, спикеры-заглушки, условия, демо-форма записи.",[127,128,111,129],"Расписание дня","Плакатная типографика","Без выдуманных имён спикеров","cd landing-event && python -m http.server 8093",124,"\u002Fog\u002Flanding-event.jpg",{"desktop":134,"desktop_sm":135,"mobile":136,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Flanding-event-desktop.webp","\u002Fshots\u002Flanding-event-desktop-sm.webp","\u002Fshots\u002Flanding-event-mobile.webp",{"slug":138,"title":139,"type":23,"category":23,"lang":24,"kind":140,"stack":141,"description":144,"highlights":145,"run":149,"hue":150,"og":151,"shots":152},"uptime-monitor","Пульс: монитор доступности","infra",[24,27,142,143,28],"SSE","REST API","Монитор доступности: HTTP и TCP проверки по расписанию, аптайм, p50\u002Fp95, детектор инцидентов, живой дашборд на SSE и публичная статус-страница. Go + SQLite без CGO.",[146,147,148,55],"Горутина-воркер на каждую проверку","Инциденты после серии падений","Дашборд на SSE и публичный \u002Fstatus","cd uptime-monitor && set SEED_DEMO=1&& go run .\u002Fcmd\u002Fserver",101,"\u002Fog\u002Fuptime-monitor.jpg",{"desktop":153,"desktop_sm":154,"mobile":155,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Fuptime-monitor-desktop.webp","\u002Fshots\u002Fuptime-monitor-desktop-sm.webp","\u002Fshots\u002Fuptime-monitor-mobile.webp",{"slug":157,"title":158,"type":23,"category":23,"lang":24,"kind":140,"stack":159,"description":161,"highlights":162,"run":167,"hue":168,"og":169,"shots":170},"job-queue","Конвейер: очередь задач",[24,27,143,142,160,28],"Prometheus","Очередь фоновых задач: приоритеты, отложенные и cron-задачи, ретраи с backoff и jitter, dead-letter, идемпотентные ключи, lease с fencing, метрики Prometheus.",[163,164,165,166],"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":171,"desktop_sm":172,"mobile":173,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Fjob-queue-desktop.webp","\u002Fshots\u002Fjob-queue-desktop-sm.webp","\u002Fshots\u002Fjob-queue-mobile.webp",{"slug":175,"title":176,"type":23,"category":23,"lang":24,"kind":177,"stack":178,"description":181,"highlights":182,"run":187,"hue":188,"og":189,"shots":190},"realtime-rooms","Комнаты: чат и доска в реальном времени","realtime",[24,179,27,180,28],"WebSocket","Canvas","Комнаты в реальном времени: чат с историей в SQLite, список «кто онлайн», индикатор «печатает», общая доска со штрихами и курсорами, анонимные ники. WebSocket (RFC 6455) написан на stdlib.",[183,184,185,186],"Свой слой 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":191,"desktop_sm":192,"mobile":193,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Frealtime-rooms-desktop.webp","\u002Fshots\u002Frealtime-rooms-desktop-sm.webp","\u002Fshots\u002Frealtime-rooms-mobile.webp",{"slug":195,"title":196,"type":23,"category":23,"lang":24,"kind":140,"stack":197,"description":206,"highlights":207,"run":212,"hue":213,"og":214,"shots":215},"api-gateway","Шлюз: API-gateway",[24,198,199,200,201,142,160,202,203,204,28,205],"stdlib only","Reverse proxy","Circuit breaker","Token bucket","slog","Vanilla JS","embed","GitHub Actions","API-шлюз на чистой stdlib: маршруты по хосту\u002Fпрефиксу из JSON-конфига с горячей перезагрузкой без обрыва соединений, балансировка round-robin и least-conn, health-checks, circuit breaker на узел, ретраи идемпотентных запросов, rate limit, хэшированные API-ключи, метрики Prometheus, живой дашборд на SSE.",[208,209,210,211],"Ноль зависимостей: только stdlib","Горячая перезагрузка конфига без обрыва соединений","Circuit breaker на узел и ретраи идемпотентных запросов","Нагрузочный тест: 4000 запросов, 40 клиентов (демо-замер)","cd api-gateway && set SEED_DEMO=1&& go run .\u002Fcmd\u002Fgateway",200,"\u002Fog\u002Fapi-gateway.jpg",{"desktop":216,"desktop_sm":217,"mobile":218,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Fapi-gateway-desktop.webp","\u002Fshots\u002Fapi-gateway-desktop-sm.webp","\u002Fshots\u002Fapi-gateway-mobile.webp",{"slug":220,"title":221,"type":23,"category":23,"lang":24,"kind":222,"stack":223,"description":229,"highlights":230,"run":235,"hue":236,"og":237,"shots":238},"notes-search","Картотека: заметки с поиском","data",[24,27,224,225,226,143,227,228,203,204,28,205],"FTS5","bm25","modernc.org\u002Fsqlite (без CGO)","Markdown (свой безопасный рендер)","SVG force-layout","База Markdown-заметок на Go и SQLite: вики-ссылки [[заметка]] с обратными ссылками и графом связей, полнотекстовый поиск FTS5 (bm25, подсветка, упрощённая русская морфология), история версий с собственным diff, автосохранение с разрешением конфликтов, экспорт и импорт .md.",[231,232,233,234],"Поиск FTS5 с bm25 и подсветкой","Вики-ссылки, backlinks и граф связей","История версий с собственным diff","Свой безопасный Markdown-рендер с тестом на XSS","cd notes-search && set SEED_DEMO=1&& go run .\u002Fcmd\u002Fserver",160,"\u002Fog\u002Fnotes-search.jpg",{"desktop":239,"desktop_sm":240,"mobile":241,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Fnotes-search-desktop.webp","\u002Fshots\u002Fnotes-search-desktop-sm.webp","\u002Fshots\u002Fnotes-search-mobile.webp",{"slug":243,"title":244,"type":82,"category":83,"live_url":245,"stack":246,"description":253,"highlights":254,"run":260,"hue":261,"og":262,"shots":263},"landing-3d","ОКСО Т1: 3D-лендинг","https:\u002F\u002Fewsirdev-3d.pages.dev",[247,248,203,249,250,251,252,205],"HTML5","CSS3","WebGL2","GLSL ES 3.00","Raymarching\u002FSDF","Scroll choreography","Лендинг вымышленного датчика воздуха (демо): объект рисуется одним фрагментным шейдером на чистом WebGL2 без библиотек, четыре SDF-формы перетекают по скроллу, есть SVG-фолбэк, reduced-motion и пауза вне экрана.",[255,256,257,258,259],"Один фрагментный шейдер","WebGL2 без библиотек","Четыре SDF-формы с морфингом по скроллу","SVG-фолбэк и prefers-reduced-motion","Пауза отрисовки вне экрана","cd landing-3d && python -m http.server 8094",90,"\u002Fog\u002Flanding-3d.jpg",{"desktop":264,"desktop_sm":265,"mobile":266,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Flanding-3d-desktop.webp","\u002Fshots\u002Flanding-3d-desktop-sm.webp","\u002Fshots\u002Flanding-3d-mobile.webp",{"slug":268,"title":269,"type":23,"category":23,"lang":24,"kind":222,"stack":270,"description":279,"highlights":280,"run":287,"hue":288,"og":289,"shots":290},"raft-kv","Плот: KV на Raft",[24,271,272,273,198,274,142,275,276,277,278,203,28,205],"Raft","Distributed systems","Consensus","HTTP\u002FJSON","WAL + fsync","Snapshots","Linearizability","Deterministic simulation testing","Учебная реализация Raft с нуля на чистой stdlib: выборы лидера, репликация лога, коммит по кворуму, лог и term\u002Fvote на диске с fsync, снапшоты с компакцией, линеаризуемые чтения (read-index), редирект фолловера на лидера. Тестируется детерминированным симулятором сети и времени с проверкой линеаризуемости; дашборд умеет убивать лидера и рвать сеть (демо).",[281,282,283,284,285,286],"Raft с нуля на stdlib","без raft-библиотек","Детерминированный симулятор сети и времени + проверка линеаризуемости","Лог и term\u002Fvote с fsync","снапшоты и компакция","Дашборд: убить лидера и разорвать сеть (демо)","cd raft-kv && set SEED_DEMO=1&& go run .\u002Fcmd\u002Fraft-kv",290,"\u002Fog\u002Fraft-kv.jpg",{"desktop":291,"desktop_sm":292,"mobile":293,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Fraft-kv-desktop.webp","\u002Fshots\u002Fraft-kv-desktop-sm.webp","\u002Fshots\u002Fraft-kv-mobile.webp",{"slug":295,"title":296,"type":23,"category":23,"lang":24,"kind":297,"stack":298,"description":307,"highlights":308,"run":313,"hue":314,"og":315,"shots":316},"auth-service","Ключница: сервис аутентификации","auth",[24,299,27,226,300,301,302,303,304,305,306,203,204,28,205],"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.",[309,310,311,312],"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":317,"desktop_sm":318,"mobile":319,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Fauth-service-desktop.webp","\u002Fshots\u002Fauth-service-desktop-sm.webp","\u002Fshots\u002Fauth-service-mobile.webp",{"slug":321,"title":322,"type":23,"category":23,"lang":24,"kind":25,"stack":323,"description":332,"highlights":333,"run":338,"hue":339,"og":340,"shots":341},"finance-tracker","Книга учёта: личные финансы",[24,27,226,143,324,325,326,327,328,329,330,28,205,302,331],"Nuxt 4","Vue 3","TypeScript","Pinia","SVG-графики","vitest","ESLint","CSV import\u002Fexport","Учёт личных финансов: счета в разных валютах с ручными курсами, доходы, расходы и переводы, бюджеты, платежи по cron-подобному расписанию, импорт CSV с превью, отчёты. Деньги только в целых минорных единицах, данные пользователей изолированы, графики нарисованы на SVG без библиотек. Go + SQLite и фронтенд на Nuxt 4.",[334,335,336,337],"Деньги только целыми минорными единицами","Мультивалютность с ручными курсами","Импорт CSV с превью и пробным прогоном","Свои SVG-графики без библиотек","cd finance-tracker && set SEED_DEMO=1&& go run .",140,"\u002Fog\u002Ffinance-tracker.jpg",{"desktop":342,"desktop_sm":343,"mobile":344,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Ffinance-tracker-desktop.webp","\u002Fshots\u002Ffinance-tracker-desktop-sm.webp","\u002Fshots\u002Ffinance-tracker-mobile.webp",{"slug":346,"title":347,"type":23,"category":23,"lang":24,"kind":177,"stack":348,"description":352,"highlights":353,"run":358,"hue":359,"og":360,"shots":361},"pixel-wall","Пиксельная стена: общий холст",[24,198,179,349,203,204,350,351,28,205],"Canvas 2D","Binary protocol","Snapshot + journal","Совместный холст 256×256 в духе r\u002Fplace: 16 цветов, кулдаун на сессию и IP, свой WebSocket-слой на stdlib (RFC 6455) с бинарным протоколом (4 бита на пиксель), снимок и дельты, снапшот плюс журнал с восстановлением, таймлапс, зум и пан, мини-карта, экспорт PNG, админ-инструменты. Пет-проект.",[354,355,356,357],"Свой 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":362,"desktop_sm":363,"mobile":364,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Fpixel-wall-desktop.webp","\u002Fshots\u002Fpixel-wall-desktop-sm.webp","\u002Fshots\u002Fpixel-wall-mobile.webp",{"slug":366,"title":367,"type":368,"category":368,"lang":24,"stack":369,"description":376,"highlights":377,"run":382,"hue":383,"og":384,"shots":385},"gitpulse","GitPulse: аналитика git-репозиториев","cli",[24,198,370,371,372,373,374,375,205,28],"CLI","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. Пет-проект.",[378,379,380,381],"Только stdlib Go, внешний бинарник один: git","Поток git log -z --numstat -M с учётом .mailmap и переименований","Самодостаточный HTML-отчёт: тепловая карта, churn, bus factor, светлая и тёмная темы","Синтетическая история из 100 000 коммитов анализируется около 5 секунд","cd gitpulse && go run .\u002Fcmd\u002Fgitpulse stats .",7,"\u002Fog\u002Fgitpulse.jpg",{"desktop":386,"desktop_sm":387,"mobile":388,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Fgitpulse-desktop.webp","\u002Fshots\u002Fgitpulse-desktop-sm.webp","\u002Fshots\u002Fgitpulse-mobile.webp",{"slug":390,"title":391,"type":392,"category":393,"stack":394,"description":399,"highlights":400,"run":407,"hue":408,"og":409,"shots":410},"legal-multipage-site","Многостраничный сайт юридической компании","site","sites",[324,325,326,395,396,397,398,28,205],"Bootstrap 5","Zod","Vitest","Playwright","Реализованный сайт юридической и экспертной компании на Nuxt 4: 65 страниц с серверным рендерингом, JSON-LD разметка и 301-редиректы со старого сайта. Заявки уходят через серверный эндпоинт с проверкой Origin, лимитом размера тела и ограничением частоты запросов.",[401,402,403,404,405,406],"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",250,"\u002Fog\u002Flegal-multipage-site.jpg",{"desktop":411,"desktop_sm":412,"mobile":413,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Flegal-multipage-site-desktop.webp","\u002Fshots\u002Flegal-multipage-site-desktop-sm.webp","\u002Fshots\u002Flegal-multipage-site-mobile.webp",{"slug":415,"title":416,"type":417,"category":418,"stack":419,"description":427,"highlights":428,"run":433,"hue":434,"og":435,"shots":436},"bad-club-booking-app","Bad Club: бронирование мест в игровом коворкинге","app","apps",[325,420,327,421,422,423,424,425,426],"Vite","Vue Router","GSAP","Express","node:sqlite","JWT (httpOnly cookie)","Resend","Демо-приложение на Vue 3 и Express с SQLite: интерактивная карта зала с бронированием мест, личный кабинет и админ-панель с редактором расстановки. Вход без паролей, по одноразовому коду на e-mail.",[429,430,431,432],"Карта зала с зонами (обычные, тихая, VIP, два монитора), фильтром по типу и статусом мест","Админ-панель: обзор, брони, столы, компьютеры, пользователи и перетаскиваемая расстановка зала","Вход по OTP с ограничением попыток и частоты запросов, роль admin задаётся через переменную окружения","Данные для демо создаются сид-скриптом при первом запуске","npm install && npm run dev",150,"\u002Fog\u002Fbad-club-booking-app.jpg",{"desktop":437,"desktop_sm":438,"mobile":439,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Fbad-club-booking-app-desktop.webp","\u002Fshots\u002Fbad-club-booking-app-desktop-sm.webp","\u002Fshots\u002Fbad-club-booking-app-mobile.webp",{"slug":441,"title":442,"type":82,"category":83,"live_url":443,"stack":444,"description":445,"highlights":446,"run":433,"hue":450,"og":451,"shots":452},"dovod-concept","Довод: концепт лендинга экспертной организации","https:\u002F\u002Fewsirdev-dovod.pages.dev",[324,325,326,87,205],"Концепт лендинга на Nuxt 4 для вымышленной экспертной организации: направления услуг, форма консультации с маской телефона, карусель отзывов и страница политики. Компания, отзывы, цифры и контакты придуманы; форма ничего не отправляет.",[447,448,449],"Форма с маской телефона, валидацией и подсказками по ошибкам; в демо показывает «данные не отправляются»","Бесконечная карусель отзывов со свайпом и автопрокруткой","Мобильная панель действий, адаптивная вёрстка, скелетон поверх серверного HTML",215,"\u002Fog\u002Fdovod-concept.jpg",{"desktop":453,"desktop_sm":454,"mobile":455,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Fdovod-concept-desktop.webp","\u002Fshots\u002Fdovod-concept-desktop-sm.webp","\u002Fshots\u002Fdovod-concept-mobile.webp",{"slug":457,"title":458,"type":392,"category":393,"live_url":459,"stack":460,"description":463,"highlights":464,"run":433,"hue":468,"og":469,"shots":470},"niche-landings","Каталог демо-лендингов под 10 ниш","https:\u002F\u002Fewsirdev-niche.pages.dev",[325,461,462,205],"Vue Router 4","Vite 8","Vue-каталог из десяти самостоятельных лендингов под разные ниши малого бизнеса: у каждого своя палитра, шрифты и структура секций. Каждый лендинг загружается отдельным чанком; весь контент демонстрационный.",[465,466,467],"10 ниш: медицина, мебель, ремонт, онлайн-школа, глэмпинг, юристы, автосервис, салон красоты, кафе, фитнес","Код-сплиттинг по маршрутам, общий набор SVG-иконок","Плашка «Демо-контент» на каталоге и каждом лендинге",32,"\u002Fog\u002Fniche-landings.jpg",{"desktop":471,"desktop_sm":472,"mobile":473,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Fniche-landings-desktop.webp","\u002Fshots\u002Fniche-landings-desktop-sm.webp","\u002Fshots\u002Fniche-landings-mobile.webp",{"slug":475,"title":476,"type":82,"category":83,"live_url":477,"stack":478,"description":481,"highlights":482,"run":486,"hue":487,"og":488,"shots":489},"dental-concept","Кварц: концепт лендинга стоматологии","https:\u002F\u002Fewsirdev-dental.pages.dev",[86,87,479,349,480,205],"JavaScript","htmlhint","Концепт одностраничного лендинга вымышленной стоматологии в стиле Liquid Chrome Dark: стеклянные карточки, хромированные заголовки и 3D-блоб в виде зуба на canvas. Один HTML-файл без сборки и зависимостей.",[483,484,485],"Анимированный блоб на canvas с учётом prefers-reduced-motion и видимости на экране","Форма записи с маской телефона и доступными сообщениями об ошибках","CI: htmlhint и проверка локальных ссылок и якорей","python -m http.server 8080",190,"\u002Fog\u002Fdental-concept.jpg",{"desktop":490,"desktop_sm":491,"mobile":492,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Fdental-concept-desktop.webp","\u002Fshots\u002Fdental-concept-desktop-sm.webp","\u002Fshots\u002Fdental-concept-mobile.webp",{"slug":494,"title":495,"type":368,"category":368,"lang":496,"stack":497,"description":501,"highlights":502,"run":507,"hue":236,"og":508,"shots":509},"logslice","logslice: анализ логов без зависимостей","Python",[496,198,370,373,498,499,500,205,28],"pytest","ruff","mypy","CLI на Python 3.11+ только на стандартной библиотеке: потоково разбирает логи nginx\u002Fapache (combined), JSON-lines, logfmt и syslog (gz, bz2, xz тоже), считает статистику и топы, кластеризует похожие ошибки, ищет скачки нагрузки по z-score и EWMA и собирает один HTML-отчёт с SVG-графиками и тепловой картой. Пет-проект.",[503,504,505,506],"Только стандартная библиотека, 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":510,"desktop_sm":511,"mobile":512,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Flogslice-desktop.webp","\u002Fshots\u002Flogslice-desktop-sm.webp","\u002Fshots\u002Flogslice-mobile.webp",{"slug":514,"title":515,"type":368,"category":368,"lang":24,"stack":516,"description":521,"highlights":522,"run":527,"hue":528,"og":529,"shots":530},"envguard","envguard: линтер и сейф .env-файлов",[24,198,370,517,302,518,519,520,205,28],"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. Пет-проект.",[523,524,525,526],"Значения секретов не печатаются нигде: только тип находки, файл, строка и маска из 4 символов","seal и unseal: AES-256-GCM, PBKDF2 600 тыс. итераций, подмена любого бита файла отвергается","Fuzz-тесты: 4 цели по 20 секунд без падений; govulncheck и gitleaks без находок","Сборки под windows, linux и darwin, только стандартная библиотека","go run .\u002Fcmd\u002Fenvguard check",35,"\u002Fog\u002Fenvguard.jpg",{"desktop":531,"desktop_sm":532,"mobile":533,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Fenvguard-desktop.webp","\u002Fshots\u002Fenvguard-desktop-sm.webp","\u002Fshots\u002Fenvguard-mobile.webp",{"slug":535,"title":536,"type":23,"category":23,"lang":496,"stack":537,"description":540,"highlights":541,"run":546,"hue":547,"og":548,"shots":549},"catalog-api","catalog-api: API каталога с поиском",[496,538,27,224,498,499,500,539,28,205],"FastAPI","bandit","API каталога товаров на Python 3.12 и FastAPI: SQLite с полнотекстовым поиском FTS5 (ранжирование bm25), фасеты по категории, цене, наличию, тегам и характеристикам, курсорная пагинация, ETag, импорт и экспорт CSV, история цен, API-ключи с ролями и аудит-лог. Витрина и админка без фреймворка отдаются самим приложением. Пет-проект.",[542,543,544,545],"Поиск 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":550,"desktop_sm":551,"mobile":552,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Fcatalog-api-desktop.webp","\u002Fshots\u002Fcatalog-api-desktop-sm.webp","\u002Fshots\u002Fcatalog-api-mobile.webp",{"slug":554,"title":555,"type":23,"category":23,"lang":326,"live_url":556,"stack":557,"description":562,"highlights":563,"run":433,"hue":450,"og":568,"shots":569},"paste-vault","Конверт: записки с шифрованием в браузере","https:\u002F\u002Fewsirdev-paste.ewsirdev.workers.dev",[326,558,559,560,420,329,561,205],"Cloudflare Workers","D1","Web Crypto","fast-check","Сервис одноразовых записок на TypeScript, Cloudflare Workers и D1: текст шифруется в браузере (AES-256-GCM), ключ лежит только во фрагменте URL и на сервер не попадает. У записки есть срок жизни, пароль и сгорание после первого прочтения. Пет-проект.",[564,565,566,567],"Шифрование в браузере: AES-256-GCM, ключ только во фрагменте URL","Сгорание после прочтения атомарно: ровно один успех из 40 параллельных чтений","52 теста в рантайме workerd, включая property-тесты и фаззинг схемы","Живая версия на Cloudflare Workers + D1","\u002Fog\u002Fpaste-vault.jpg",{"desktop":570,"desktop_sm":571,"mobile":572,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Fpaste-vault-desktop.webp","\u002Fshots\u002Fpaste-vault-desktop-sm.webp","\u002Fshots\u002Fpaste-vault-mobile.webp",{"slug":574,"title":575,"type":392,"category":393,"live_url":576,"stack":577,"description":583,"highlights":584,"run":589,"hue":590,"og":591,"shots":592},"docs-engine","Сверло: генератор документации на Node.js","https:\u002F\u002Fewsirdev-docs.pages.dev",[578,579,580,581,582,205],"Node.js","JavaScript (ESM)","zero dependencies","Markdown","Cloudflare Pages","Генератор статических сайтов документации, написанный с нуля на Node.js без зависимостей: собственный Markdown-парсер, подсветка кода и офлайн-поиск. Демо-сайт про вымышленную библиотеку «Сверло» собран из 43 страниц на двух языках (RU и EN) и двух версиях (v1 и v2). Пет-проект.",[585,586,587,588],"Свой Markdown-парсер, подсветка кода и клиентский поиск без библиотек","43 страницы, RU и EN, версии v1 и v2, hreflang, sitemap и RSS","131 тест; в CI проверка битых ссылок и якорей","Сайт читается без JavaScript, поиск работает офлайн","npm test && npm run build",25,"\u002Fog\u002Fdocs-engine.jpg",{"desktop":593,"desktop_sm":594,"mobile":595,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Fdocs-engine-desktop.webp","\u002Fshots\u002Fdocs-engine-desktop-sm.webp","\u002Fshots\u002Fdocs-engine-mobile.webp",{"slug":597,"title":598,"type":417,"category":418,"live_url":599,"stack":600,"description":605,"highlights":606,"run":433,"hue":611,"og":612,"shots":613},"typing-lab","typing-lab: тренажёр слепой печати","https:\u002F\u002Fewsirdev-typing.pages.dev",[324,325,326,601,602,603,373,329,604,582],"IndexedDB","WebAudio","PWA","axe","Тренажёр слепой печати, который целиком работает в браузере и не обращается к серверу: Nuxt 4, история в IndexedDB, звук клавиш на WebAudio, офлайн через PWA. Режимы времени, слов, цитат, своего текста, кода и «слабых мест», шесть раскладок, метрики WPM, точность и консистентность, графики на чистом SVG. Пет-проект.",[607,608,609,610],"Без сервера и аккаунтов: история в IndexedDB, экспорт и импорт JSON","Шесть раскладок, режимы вплоть до адаптивных упражнений из ваших ошибок","59 unit-тестов и 65 e2e-проверок, axe без нарушений, CLS 0,000","PWA: работает офлайн после первого открытия",45,"\u002Fog\u002Ftyping-lab.jpg",{"desktop":614,"desktop_sm":615,"mobile":616,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Ftyping-lab-desktop.webp","\u002Fshots\u002Ftyping-lab-desktop-sm.webp","\u002Fshots\u002Ftyping-lab-mobile.webp",{"slug":618,"title":619,"type":417,"category":418,"lang":326,"live_url":620,"stack":621,"description":623,"highlights":624,"run":433,"hue":611,"og":629,"shots":630},"budget-split","budget-split: «Поровну», учёт общих расходов","https:\u002F\u002Fewsirdev-budget.ewsirdev.workers.dev",[326,420,558,559,622,329,561,604,205],"WebGL","Веб-приложение учёта общих расходов группы без регистрации: комната по секретной ссылке, деление поровну, по долям, суммам и процентам, расчёт «кто кому» не более n−1 переводов. Пет-проект.",[625,626,627,628],"Без аккаунтов: комната по секретной ссылке в 128 бит, отдельные ссылки «просмотр» и «редактирование»","Четыре способа деления и мультивалюта с ручным курсом; расчёт «кто кому» не более n−1 переводов","Оптимистичная блокировка: из 4 одновременных правок одного расхода проходит одна","60 тестов (vitest в workerd, property-тесты fast-check), CI зелёный, npm audit 0, axe 0 нарушений","\u002Fog\u002Fbudget-split.jpg",{"desktop":631,"desktop_sm":632,"mobile":633,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Fbudget-split-desktop.webp","\u002Fshots\u002Fbudget-split-desktop-sm.webp","\u002Fshots\u002Fbudget-split-mobile.webp",{"slug":635,"title":636,"type":392,"category":393,"lang":326,"live_url":637,"stack":638,"description":641,"highlights":642,"run":433,"hue":611,"og":647,"shots":648},"kladka-site","kladka-site: «Кладка», демо-сайт студии каркасного строительства","https:\u002F\u002Fewsirdev-kladka.pages.dev",[639,326,640,373,622,604,480,205],"Astro","Content collections","Демо-сайт вымышленной студии каркасного строительства: 5 проектов с чертежами, услуги, цены с калькулятором-ориентиром, журнал. Компания, цены и сроки выдуманы, на каждой странице есть пометка.",[643,644,645,646],"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":649,"desktop_sm":650,"mobile":651,"dw":42,"dh":43,"mw":44,"mh":45},"\u002Fshots\u002Fkladka-site-desktop.webp","\u002Fshots\u002Fkladka-site-desktop-sm.webp","\u002Fshots\u002Fkladka-site-mobile.webp",[653,665,680,694,711],{"slug":83,"title":654,"blurb":655,"variant":656,"palette":657,"layout":658,"count":659,"preview":660,"stack":664},"Лендинги","Одна страница, одна задача. Вёрстка без фреймворков, где это возможно, и один WebGL-эксперимент.","ink","cold","shots",6,[661,662,663],{"slug":80,"title":81,"shot":100,"hue":96},{"slug":103,"title":104,"shot":118,"hue":114},{"slug":121,"title":122,"shot":135,"hue":131},[87,86,205,88],{"slug":393,"title":666,"blurb":667,"variant":668,"palette":669,"layout":670,"count":671,"langs":672,"preview":675,"stack":679},"Многостраничные сайты","Сайты со своей структурой, маршрутами и формами: от клиентского проекта до набора нишевых страниц.","honey","mono","frames",4,[673],{"lang":326,"count":674},1,[676,677,678],{"slug":390,"title":391,"shot":412,"hue":408},{"slug":457,"title":458,"shot":472,"hue":468},{"slug":574,"title":575,"shot":594,"hue":590},[205,326,325,639],{"slug":418,"title":681,"blurb":682,"variant":683,"palette":684,"layout":685,"count":686,"langs":687,"preview":689,"stack":693},"Веб-приложения","Интерфейс плюс сервер: авторизация, данные, письма. Приложения, в которые входят.","beads","warm","phone",3,[688],{"lang":326,"count":674},[690,691,692],{"slug":415,"title":416,"shot":438,"hue":434},{"slug":597,"title":598,"shot":615,"hue":611},{"slug":618,"title":619,"shot":632,"hue":611},[326,420,325,604],{"slug":23,"title":695,"blurb":696,"variant":697,"palette":698,"layout":699,"count":700,"langs":701,"preview":706,"stack":710},"Бэкенды","Сервисы с API, базой и тестами: в основном на Go, есть и на Python и TypeScript. Фильтр по языку внутри раздела.","ferro","acid","list",14,[702,704,705],{"lang":24,"count":703},12,{"lang":496,"count":674},{"lang":326,"count":674},[707,708,709],{"slug":21,"title":22,"shot":40,"hue":36},{"slug":47,"title":48,"shot":61,"hue":57},{"slug":64,"title":65,"shot":77,"hue":73},[28,24,27,205],{"slug":368,"title":712,"blurb":713,"variant":714,"palette":657,"layout":715,"count":686,"langs":716,"preview":720,"stack":724},"CLI-утилиты","Программы для терминала: один бинарник, понятные флаги, код выхода, который можно проверить в скрипте.","film","terminal",[717,719],{"lang":24,"count":718},2,{"lang":496,"count":674},[721,722,723],{"slug":366,"title":367,"shot":387,"hue":383},{"slug":494,"title":495,"shot":511,"hue":236},{"slug":514,"title":515,"shot":532,"hue":528},[370,28,205,198],[726,800,874,939,990,1038,1080,1155,1221,1288,1353,1426,1494],{"slug":157,"title":158,"stack":727,"summary":728,"nodes":729,"edges":755,"flows":766,"decisions":774,"tradeoffs":787,"tests":790,"source":791,"stats":792},[24,27,143,142,160,28],"Очередь фоновых задач на SQLite: приоритеты, ретраи с backoff, cron, идемпотентные ключи. Главное — гарантии: at-least-once, lease и fencing.",[730,735,739,743,746,749,752],{"id":731,"label":732,"row":733,"col":733,"span":674,"kind":734},"prod","Клиент API\nPOST \u002Fapi\u002Fjobs",0,"client",{"id":736,"label":737,"row":733,"col":674,"span":674,"kind":738},"cron","Планировщик\ncron-расписания","core",{"id":740,"label":741,"row":674,"col":733,"span":718,"kind":742},"db","SQLite (WAL)\njobs · runs · schedules","store",{"id":744,"label":745,"row":718,"col":733,"span":674,"kind":738},"claim","Захват задачи\nUPDATE … RETURNING",{"id":747,"label":748,"row":718,"col":674,"span":674,"kind":738},"reaper","Reaper\nистёкшие lease",{"id":750,"label":751,"row":686,"col":733,"span":674,"kind":738},"pool","Пул воркеров\nheartbeat аренды",{"id":753,"label":754,"row":686,"col":674,"span":674,"kind":734},"dash","Дашборд и \u002Fmetrics\nSSE · Prometheus",[756,757,758,759,760,763,764],{"n":674,"from":731,"to":740},{"n":718,"from":736,"to":740},{"n":686,"from":740,"to":744},{"n":671,"from":744,"to":750},{"n":761,"from":750,"to":740,"side":762},5,"L",{"n":659,"from":747,"to":740},{"n":383,"from":740,"to":753,"side":765},"R",[767,768,769,770,771,772,773],"Задача ставится через REST под Bearer-токеном; повторная постановка с тем же идемпотентным ключом возвращает уже существующую задачу.","Каждый запуск cron ставится с ключом cron:\u003Cid>:\u003Cвремя>, а next_run сдвигается условным UPDATE, поэтому несколько процессов не создадут дубль.","Самая приоритетная готовая задача выбирается по priority DESC, run_at, id.","Один оператор выставляет статус, счётчик попыток, срок lease и уникальный токен аренды и отдаёт задачу воркеру.","Воркер продлевает аренду heartbeat-ом каждую треть срока и завершает задачу (Complete\u002FFail), предъявляя токен: «зомби»-воркер получает ErrLeaseLost.","Reaper возвращает задачи с истёкшей арендой в очередь с backoff или в dead, если попытки кончились.","Снимок состояния раз в секунду уходит в дашборд по SSE; счётчики и гистограмма доступны на \u002Fmetrics.",[775,778,781,784],{"title":776,"text":777},"Атомарный захват одним UPDATE … RETURNING","Выбор задачи и выдача аренды происходят в одном операторе, поэтому два воркера, даже из разных процессов на одном файле БД, не получат одну задачу.",{"title":779,"text":780},"Lease, heartbeat и fencing-токен","Упавший воркер не теряет задачу: аренда истекает, а Complete\u002FFail\u002FExtend проверяют токен, так что очнувшийся «зомби» не может испортить результат.",{"title":782,"text":783},"At-least-once, а не exactly-once","Обработчик может быть вызван повторно, поэтому обработчики должны быть идемпотентными. Очередь не обещает того, чего не может гарантировать.",{"title":785,"text":786},"Backoff с jitter","Задержка BACKOFF_BASE · 2^(n-1) с потолком, умноженная на случайный множитель 0,5–1; Permanent-ошибка и неизвестный тип сразу уходят в dead.",[788,789],"Один файл SQLite: масштабирование только на одной машине (несколько процессов на одном томе допустимы, по сети нет).","Счётчики метрик живут в памяти и обнуляются при перезапуске; нет цепочек задач, rate limit по очередям и ролей (один токен).","Конкурентный захват (400 задач, два «процесса» на одном файле, 12 воркеров — ни одного двойного исполнения), истечение lease и fencing, идемпотентные ключи в гонке, cron из двух процессов, graceful shutdown.","README проекта job-queue: «Архитектура», «Гарантии доставки», «Ограничения»",{"slug":157,"kind":793,"go_lines":794,"go_test_lines":795,"tests":590,"endpoints":796,"html":797,"css":798,"js":799},"go",2161,833,19,138,188,331,{"slug":138,"title":139,"stack":801,"summary":802,"nodes":803,"edges":826,"flows":838,"decisions":848,"tradeoffs":861,"tests":864,"source":865,"stats":866},[24,27,142,143,28],"Монитор доступности: HTTP и TCP проверки, аптайм, p50\u002Fp95, инциденты, живой дашборд и публичная статус-страница.",[804,807,810,813,816,818,821,824],{"id":805,"label":806,"row":733,"col":733,"span":674,"kind":738},"mgr","Manager\nгорутина на проверку",{"id":808,"label":809,"row":733,"col":674,"span":674,"kind":734},"target","Цели\nHTTP \u002F TCP",{"id":811,"label":812,"row":674,"col":733,"span":674,"kind":738},"run","Run\nзамер проверки",{"id":814,"label":815,"row":674,"col":674,"span":674,"kind":738},"hub","Hub (pub\u002Fsub)\nнеблокирующий",{"id":742,"label":817,"row":718,"col":733,"span":718,"kind":742},"SQLite (WAL)\nпроверки · результаты · инциденты",{"id":819,"label":820,"row":686,"col":733,"span":674,"kind":738},"stats","stats\np50\u002Fp95 · аптайм",{"id":822,"label":823,"row":686,"col":674,"span":674,"kind":738},"det","Detector\nинциденты",{"id":25,"label":825,"row":671,"col":733,"span":718,"kind":734},"REST + SSE\nдашборд · \u002Fstatus",[827,828,829,830,831,832,833,834,836],{"n":674,"from":805,"to":811},{"n":718,"from":811,"to":808},{"n":686,"from":811,"to":742},{"n":671,"from":811,"to":814},{"n":761,"from":814,"to":822,"side":765},{"n":659,"from":822,"to":742},{"n":383,"from":742,"to":819},{"n":835,"from":819,"to":25},8,{"n":837,"from":814,"to":25,"side":765},9,[839,840,841,842,843,844,845,846,847],"Manager держит отдельную горутину на каждую проверку; следующий запуск планируется после завершения предыдущего, поэтому проверки одного адреса не накладываются.","Run делает HTTP GET (до 5 редиректов, ожидаемый код или любой \u003C 400) либо TCP connect с таймаутом.","Результат записывается в SQLite.","После записи результат публикуется в Hub.","Detector получает результат: fail_threshold падений подряд открывают инцидент, первая успешная проверка его закрывает.","Смена состояния пишется как инцидент; открытый инцидент переживает перезапуск.","Аптайм за 24 ч, 7 и 30 дней и p50\u002Fp95 считаются запросами по сырой истории (чистые функции в internal\u002Fstats).","REST отдаёт цифры дашборду и публичной статус-странице (только public-проверки, без адресов и текстов ошибок).","Hub рассылает события SSE-подписчикам неблокирующе: медленный клиент теряет события, а не тормозит проверки.",[849,852,855,858],{"title":850,"text":851},"Перцентили методом ближайшего ранга","p-й процентиль — элемент отсортированного среза с номером ceil(p\u002F100·N). Простая чистая функция без интерполяции, легко проверить тестом.",{"title":853,"text":854},"Не накладывать проверки","Следующий запуск планируется после завершения предыдущего: медленный адрес не копит параллельные проверки сам на себя.",{"title":856,"text":857},"Медленный клиент не тормозит мониторинг","Рассылка в Hub неблокирующая; после переподключения дашборд заново читает состояние.",{"title":859,"text":860},"SSE через fetch, а не EventSource","Так токен передаётся в заголовке Authorization, а не в URL.",[862,863],"Аптайм считается по числу проверок, а не по длительности: у проверок с разным интервалом цифры несопоставимы по точности.","Агрегаты считаются по сырой истории: для десятков проверок мгновенно, для тысяч понадобятся предагрегаты. Одна нода, один токен, уведомлений нет.","Планировщик, перцентили и аптайм, жизненный цикл инцидента (в том числе после перезапуска), HTTP\u002FTCP проверки с таймаутом, API, SSE.","README проекта uptime-monitor: «Возможности», «Архитектура», «Ограничения»",{"slug":138,"kind":793,"go_lines":867,"go_test_lines":868,"tests":869,"endpoints":870,"html":871,"css":872,"js":873},1445,589,18,15,126,172,369,{"slug":175,"title":176,"stack":875,"summary":876,"nodes":877,"edges":898,"flows":906,"decisions":914,"tradeoffs":927,"tests":930,"source":931,"stats":932},[24,179,27,180,28],"Комнаты на WebSocket: чат с историей, присутствие, общая доска. Протокол RFC 6455 написан на stdlib, чтобы показать, что происходит под new WebSocket(…).",[878,881,884,886,889,892,895],{"id":879,"label":880,"row":733,"col":733,"span":718,"kind":734},"browsers","Браузеры\nWebSocket-клиенты",{"id":882,"label":883,"row":674,"col":733,"span":718,"kind":738},"ws","internal\u002Fws\nрукопожатие · фреймы · лимиты",{"id":814,"label":885,"row":718,"col":733,"span":718,"kind":738},"Hub комнат\nмьютекс на комнату",{"id":887,"label":888,"row":686,"col":733,"span":674,"kind":738},"queue","Очередь клиента\n256 сообщений",{"id":890,"label":891,"row":686,"col":674,"span":674,"kind":738},"pump","write pump\nдедлайн записи 10 с",{"id":893,"label":894,"row":671,"col":733,"span":674,"kind":742},"sql","SQLite (WAL)\nистория чата",{"id":896,"label":897,"row":671,"col":674,"span":674,"kind":742},"board","Доска в памяти\nштрихи",[899,900,901,902,903,904,905],{"n":674,"from":879,"to":882},{"n":718,"from":882,"to":814},{"n":686,"from":814,"to":887},{"n":671,"from":887,"to":890},{"n":761,"from":890,"to":879,"side":765},{"n":659,"from":814,"to":893,"side":762},{"n":383,"from":814,"to":896,"side":765},[907,908,909,910,911,912,913],"Браузер открывает WebSocket: GET с Upgrade, версия 13, ключ, проверка Origin; ответ 101 с Sec-WebSocket-Accept.","Фреймы разбираются в internal\u002Fws: маскирование, фрагментация, лимит размера, UTF-8, ping\u002Fpong; JSON-сообщение уходит в хаб (read pump).","Рассылка кладёт сообщение в ограниченную очередь каждого клиента; при переполнении медленный клиент отключается кодом 1008, остальные не ждут.","write pump забирает сообщения из очереди и пишет фреймы с дедлайном 10 с.","Фреймы доходят до браузеров; эфемерные сообщения (курсоры, «печатает») пропускаются при заполнении очереди на 3\u002F4.","Сообщение рассылается только после успешной записи в SQLite; при ошибке отправитель получает error: store.","Штрихи доски хранятся в памяти комнаты и отдаются вошедшим позже.",[915,918,921,924],{"title":916,"text":917},"Свой слой WebSocket","Около 530 строк в internal\u002Fws: рукопожатие, фреймы, маскирование, фрагментация, ping\u002Fpong и close-коды. Цель — понять протокол; для продакшена обычно берут проверенную библиотеку.",{"title":919,"text":920},"Backpressure вместо ожидания","Ограниченная очередь на клиента и отключение медленного клиента: одна медленная вкладка не задерживает комнату.",{"title":922,"text":923},"Одинаковый порядок для всех","Сохранение и рассылка выполняются под одним мьютексом комнаты; история в welcome читается под тем же мьютексом, поэтому нет ни пересечения, ни разрыва с живым потоком.",{"title":925,"text":926},"Лимиты на соединение","Токен-бакет для чата (2 сообщения\u002Fс, пачка 5) и общий лимит (200\u002Fс, пачка 400); флуд закрывает соединение кодом 1008.",[928,929],"Один процесс: хаб в памяти, горизонтального масштабирования нет. Доска не переживает перезапуск сервера, очистить её может любой участник.","Нет permessage-deflate, TLS и аутентификации; Autobahn Test Suite не запускался. Ники анонимные и не защищены.","Пример рукопожатия из RFC, 12 видов нарушений протокола с проверкой close-кода, фрагментация с ping посередине, отключение медленного клиента, а также нагрузочный тест: 360 соединений в 6 комнатах.","README проекта realtime-rooms: «Почему свой слой WebSocket», «Возможности», «Гарантии», «Ограничения»",{"slug":175,"kind":793,"go_lines":933,"go_test_lines":934,"tests":935,"endpoints":383,"html":936,"css":937,"js":938},1822,1313,48,103,184,517,{"slug":64,"title":65,"stack":940,"summary":941,"nodes":942,"edges":958,"flows":965,"decisions":972,"tradeoffs":979,"tests":982,"source":983,"stats":984},[24,27,28],"Онлайн-запись: свободные слоты по длительности услуги и рабочим часам, защита от двойной брони, админка.",[943,945,948,950,953,956],{"id":734,"label":944,"row":733,"col":733,"span":674,"kind":734},"Клиент\nуслуга · день · слот",{"id":946,"label":947,"row":733,"col":674,"span":674,"kind":734},"admin","Админка \u002Fadmin\nBearer-токен",{"id":25,"label":949,"row":674,"col":733,"span":718,"kind":738},"HTTP API\nслоты · брони · админ",{"id":951,"label":952,"row":718,"col":733,"span":674,"kind":738},"slots","Расчёт слотов\nчасы минус занятое",{"id":954,"label":955,"row":718,"col":674,"span":674,"kind":738},"book","Бронирование\nодна транзакция",{"id":740,"label":957,"row":686,"col":733,"span":718,"kind":742},"SQLite\nUNIQUE (date, start_min) WHERE active",[959,960,961,962,963,964],{"n":674,"from":734,"to":25},{"n":718,"from":946,"to":25},{"n":686,"from":25,"to":951},{"n":671,"from":25,"to":954},{"n":761,"from":951,"to":740},{"n":659,"from":954,"to":740},[966,967,968,969,970,971],"Клиент запрашивает 14 дней вперёд и слоты дня, затем отправляет POST \u002Fapi\u002Fbookings.","Владелец управляет записями, услугами, часами работы и закрытыми датами.","Слоты считаются по длительности услуги, часам дня недели и занятым интервалам, шаг сетки 30 минут.","Book проверяет свободность слота и вставляет запись в одной транзакции.","Расчёт читает часы работы, закрытые даты и активные записи.","Вставка защищена частичным UNIQUE-индексом; при конфликте клиент получает 409.",[973,976],{"title":974,"text":975},"Две линии защиты от двойной брони","Проверка свободности и вставка идут в одной транзакции, а частичный UNIQUE-индекс по (date, start_min) для активных записей ловит то, что проверка пропустила. Отменённая запись освобождает время.",{"title":977,"text":978},"Часовой пояс в данных","Пояс берётся из TZ_NAME, база tzdata встроена в бинарник: расписание не зависит от настроек машины.",[980,981],"Один специалист\u002Fресурс: все услуги делят одно расписание.","Закрытие даты не отменяет созданные записи; клиент не может отменить запись сам, уведомлений нет; админка за общим токеном.","Расчёт слотов и границы периода, пересечения, конкурентная бронь: 30 горутин на одно время — успешна ровно одна.","README проекта booking-slots: «Возможности», «Тесты», «Ограничения» и код internal\u002Fbooking",{"slug":64,"kind":793,"go_lines":985,"go_test_lines":986,"tests":703,"endpoints":869,"html":987,"css":988,"js":989},891,338,146,123,309,{"slug":47,"title":48,"stack":991,"summary":992,"nodes":993,"edges":1009,"flows":1016,"decisions":1023,"tradeoffs":1027,"tests":1030,"source":1031,"stats":1032},[24,27,28],"Сократитель ссылок со статистикой переходов: алиасы, срок действия, редирект 302, дашборд под токеном.",[994,997,999,1002,1005,1007],{"id":995,"label":996,"row":733,"col":733,"span":674,"kind":734},"visitor","Посетитель\nGET \u002F{code}",{"id":946,"label":998,"row":733,"col":674,"span":674,"kind":734},"Админ\nдашборд · токен",{"id":1000,"label":1001,"row":674,"col":733,"span":718,"kind":738},"http","net\u002Fhttp\nлимит создания ссылок",{"id":1003,"label":1004,"row":718,"col":733,"span":674,"kind":738},"redirect","Редирект 302\nи учёт клика",{"id":25,"label":1006,"row":718,"col":674,"span":674,"kind":738},"API ссылок\nCRUD · статистика",{"id":740,"label":1008,"row":686,"col":733,"span":718,"kind":742},"SQLite\nссылки · клики",[1010,1011,1012,1013,1014,1015],{"n":674,"from":995,"to":1000},{"n":718,"from":946,"to":1000},{"n":686,"from":1000,"to":1003},{"n":671,"from":1000,"to":25},{"n":761,"from":1003,"to":740},{"n":659,"from":25,"to":740},[1017,1018,1019,1020,1021,1022],"Посетитель открывает короткую ссылку.","Администратор создаёт ссылки и смотрит статистику под Bearer-токеном.","Публичный маршрут отвечает редиректом 302.","Создание ссылок ограничено по частоте с одного адреса.","Клик записывается с хэшем посетителя (суточная соль): по дням, браузерам, ОС и устройствам; IP-адреса не сохраняются.","Статистика строится по сохранённым кликам.",[1024],{"title":1025,"text":1026},"Приватная статистика","Уникальные посетители считаются по хэшу с суточной солью, IP-адреса не сохраняются.",[1028,1029],"Учебный пример: один админ-токен, пользователей нет.","Демо-статистика сгенерирована и не отражает реальный трафик.","go vet и go test в CI; подробности покрытия в README проекта не перечислены.","README проекта link-shortener: «Возможности», «Ограничения»",{"slug":47,"kind":793,"go_lines":985,"go_test_lines":1033,"tests":703,"endpoints":1034,"html":1035,"css":1036,"js":1037},328,11,119,162,317,{"slug":21,"title":22,"stack":1039,"summary":1040,"nodes":1041,"edges":1054,"flows":1060,"decisions":1066,"tradeoffs":1070,"tests":1073,"source":1074,"stats":1075},[24,27,28],"Многопользовательская канбан-доска: регистрация, доски, колонки, карточки с перетаскиванием.",[1042,1045,1047,1049,1052],{"id":1043,"label":1044,"row":733,"col":733,"span":718,"kind":734},"browser","Браузер\nванильный JS · drag and drop",{"id":1000,"label":1046,"row":674,"col":733,"span":718,"kind":738},"HTTP API (JSON)\nContent-Type · same-origin",{"id":297,"label":1048,"row":718,"col":733,"span":674,"kind":738},"Аутентификация\nPBKDF2 · сессии",{"id":1050,"label":1051,"row":718,"col":674,"span":674,"kind":738},"boards","Доски и карточки\nданные по пользователю",{"id":740,"label":1053,"row":686,"col":733,"span":718,"kind":742},"SQLite\nбез CGO",[1055,1056,1057,1058,1059],{"n":674,"from":1043,"to":1000},{"n":718,"from":1000,"to":297},{"n":686,"from":1000,"to":1050},{"n":671,"from":297,"to":740},{"n":761,"from":1050,"to":740},[1061,1062,1063,1064,1065],"Браузер ходит в JSON API; изменяющие запросы проверяются на Content-Type и same-origin.","Регистрация и вход по почте и паролю: PBKDF2-HMAC-SHA256, сессия в HttpOnly-cookie с SameSite=Lax.","Доски, колонки и карточки доступны только владельцу: данные пользователей изолированы.","Пользователи и сессии хранятся в SQLite.","Доски, колонки, карточки и их порядок хранятся в SQLite.",[1067],{"title":1068,"text":1069},"Защита без внешних библиотек","PBKDF2 из стандартной библиотеки, HttpOnly-сессия, проверка Content-Type и same-origin для изменяющих запросов.",[1071,1072],"Учебный пример: нет сброса пароля, подтверждения почты и ограничения частоты входа.","Не выставлять в интернет без HTTPS и обратного прокси.","Тесты API (go vet и go test в CI).","README проекта kanban-tasks: «Возможности», «Ограничения»",{"slug":21,"kind":793,"go_lines":1076,"go_test_lines":1077,"tests":383,"endpoints":869,"html":1078,"css":1079,"js":1033},871,272,102,156,{"slug":195,"title":196,"stack":1081,"summary":1082,"nodes":1083,"edges":1111,"flows":1121,"decisions":1131,"tradeoffs":1144,"tests":1147,"source":1148,"stats":1149},[24,198,199,200,201,142,160,202,203,204,28,205],"API-шлюз на чистой stdlib: маршруты из JSON-конфига с горячей перезагрузкой, балансировка, health-checks, circuit breaker на узел, ретраи, rate limit. Главное — неизменяемое состояние и защита от open-proxy и SSRF.",[1084,1086,1089,1092,1095,1098,1101,1104,1107,1108],{"id":734,"label":1085,"row":733,"col":733,"span":674,"kind":734},"Клиент\nHTTP на :8087",{"id":1087,"label":1088,"row":733,"col":674,"span":674,"kind":734},"ops","Оператор\nSIGHUP · PUT \u002Fapi\u002Fconfig",{"id":1090,"label":1091,"row":674,"col":733,"span":718,"kind":738},"entry","Вход шлюза\nrequest-id · host + prefix",{"id":1093,"label":1094,"row":718,"col":733,"span":674,"kind":738},"guard","Auth и rate limit\nsha256 ключа · token bucket",{"id":1096,"label":1097,"row":718,"col":674,"span":674,"kind":742},"state","Состояние\natomic.Pointer",{"id":1099,"label":1100,"row":686,"col":733,"span":674,"kind":738},"bal","Балансировщик\nround-robin · least-conn",{"id":1102,"label":1103,"row":686,"col":674,"span":674,"kind":738},"health","Health-checks\nGET \u002Fhealth на узел",{"id":1105,"label":1106,"row":671,"col":733,"span":674,"kind":738},"brk","Circuit breaker\nclosed · open · half-open",{"id":753,"label":754,"row":671,"col":674,"span":674,"kind":734},{"id":1109,"label":1110,"row":761,"col":733,"span":718,"kind":738},"up","Апстримы\nSSRF-guard · ретраи",[1112,1113,1114,1115,1116,1117,1118,1119,1120],{"n":674,"from":734,"to":1090},{"n":718,"from":1087,"to":1096,"side":765},{"n":686,"from":1096,"to":1090},{"n":671,"from":1090,"to":1093},{"n":761,"from":1093,"to":1099},{"n":659,"from":1102,"to":1099},{"n":383,"from":1099,"to":1105},{"n":835,"from":1105,"to":1109},{"n":837,"from":1109,"to":753,"side":765},[1122,1123,1124,1125,1126,1127,1128,1129,1130],"Запрос приходит на :8087, получает request-id, маршрут ищется по хосту и самому длинному префиксу; не найден — 404, шлюз не работает как open-proxy.","Оператор меняет конфиг через SIGHUP (только Unix) или PUT\u002FPOST в админке на :8088; конфиг проверяется целиком, ошибочный отклоняется, старый продолжает работать.","Новое состояние (маршруты, апстримы, ключи) подменяется одним atomic.Pointer: слушающий сокет не трогается, запрос в полёте доезжает на старой версии.","Ключ API хэшируется sha256 и сверяется с конфигом; rate limit по token bucket ведётся по IP (IPv6 по \u002F64) или по ключу, лишний запрос не доходит до апстрима.","Балансировщик выбирает узел round-robin или least-conn, но только среди узлов в состоянии healthy.","Отдельный health-loop на каждый узел опрашивает GET \u002Fhealth и по порогам помечает узел healthy или нет.","У каждого узла свой breaker (closed, open, half-open); ошибкой считаются сбой соединения, таймаут и любой 5xx кроме 501, обрыв запроса клиентом не считается.","Соединение идёт через guarded Dialer, который проверяет IP в момент соединения (SSRF, DNS-rebinding); при 502\u002F503\u002F504 или сбое сети GET, HEAD, OPTIONS, PUT и DELETE повторяются на другом узле.","Окно статистики и счётчики уходят в дашборд по SSE и на \u002Fmetrics в формате Prometheus; метки ограничены конфигом, путь и ключи в метки не попадают.",[1132,1135,1138,1141],{"title":1133,"text":1134},"Неизменяемое состояние под atomic.Pointer","Маршруты, апстримы и ключи собираются в новое состояние и подменяются одним указателем. Апстримы с неизменённым описанием переиспользуются целиком, поэтому health, breaker и счётчики не сбрасываются при перезагрузке.",{"title":1136,"text":1137},"Не open-proxy и защита от SSRF","Адрес назначения берётся только из конфига, Host, absolute-URI, CONNECT\u002FTRACE и пути с «..» не дают выйти за маршруты. IP, в который резолвится имя, проверяется ещё раз при соединении, а разрешение приватных сетей задаётся окружением, а не конфигом.",{"title":1139,"text":1140},"Breaker на каждый узел","Состояния closed, open и half-open ведутся отдельно у каждого узла: один больной узел выпадает из ротации, остальные продолжают принимать трафик.",{"title":1142,"text":1143},"Ретраи только идемпотентных запросов","POST не повторяется, чтобы не выполнить операцию дважды; тело буферизуется до 1 МиБ, а более крупные запросы идут без ретраев.",[1145,1146],"Rate limit и состояние breaker живут в памяти одного процесса: при нескольких репликах лимиты не общие, а за балансировщиком все клиенты выглядят одним IP, потому что X-Forwarded-For шлюз не доверяет.","Нет TLS на входе, WebSocket и Upgrade не поддерживаются (501); перцентили по гистограмме с погрешностью до 25%; админка с одним токеном; SIGHUP только на Unix.","Балансировщики, состояния breaker на фейковых часах, token bucket, SSRF-кейсы, ретраи (GET, PUT с телом; POST не ретраится), горячая перезагрузка с запросом «в полёте», защита от open-proxy, нагрузочный тест (4000 запросов, 40 клиентов); go test -race в CI.","README проекта api-gateway: «Архитектура», «Гарантии», «Что считается ошибкой», «Ограничения»",{"slug":195,"kind":793,"go_lines":1150,"go_test_lines":1151,"tests":935,"endpoints":1034,"html":1152,"css":1153,"js":1154},2334,1179,115,139,163,{"slug":220,"title":221,"stack":1156,"summary":1157,"nodes":1158,"edges":1181,"flows":1190,"decisions":1199,"tradeoffs":1212,"tests":1215,"source":1216,"stats":1217},[24,27,224,225,226,143,227,228,203,204,28,205],"База Markdown-заметок на Go и SQLite: вики-ссылки с backlinks и графом, поиск FTS5 с bm25, версии с собственным diff. Главное — безопасный собственный рендер и поиск по стемам.",[1159,1162,1165,1167,1170,1173,1175,1178],{"id":1160,"label":1161,"row":733,"col":733,"span":674,"kind":734},"editor","Редактор\nавтосохранение · rev",{"id":1163,"label":1164,"row":733,"col":674,"span":674,"kind":734},"ui","Поиск и граф\nCtrl+K · SVG-раскладка",{"id":25,"label":1166,"row":674,"col":733,"span":718,"kind":738},"REST API \u002Fapi\nBearer-токен",{"id":1168,"label":1169,"row":718,"col":733,"span":674,"kind":738},"md","Markdown-рендер\nбелый список · [[ссылки]]",{"id":1171,"label":1172,"row":718,"col":674,"span":674,"kind":738},"text","Стеммер и запрос\nстемы · префиксы",{"id":740,"label":1174,"row":686,"col":733,"span":718,"kind":742},"SQLite\nnotes · links · versions · notes_fts",{"id":1176,"label":1177,"row":671,"col":733,"span":674,"kind":738},"ver","Версии и diff\nLCS · окно слияния",{"id":1179,"label":1180,"row":671,"col":674,"span":674,"kind":734},"io","Экспорт\nzip из .md",[1182,1183,1184,1185,1186,1187,1188,1189],{"n":674,"from":1160,"to":25},{"n":718,"from":1163,"to":25},{"n":686,"from":25,"to":1168},{"n":671,"from":25,"to":1171},{"n":761,"from":1168,"to":740},{"n":659,"from":1171,"to":740},{"n":383,"from":740,"to":1176},{"n":835,"from":740,"to":1179},[1191,1192,1193,1194,1195,1196,1197,1198],"Редактор шлёт PUT с rev; список слева обновляется сразу при вводе и откатывается, если сервер вернул ошибку.","Поиск, Ctrl+K и граф запрашивают \u002Fapi\u002Fsearch и \u002Fapi\u002Fgraph под Bearer-токеном; граф раскладывается в браузере (Фрухтерман — Рейнгольд, SVG).","Текст заметки идёт в собственный рендер: исходник экранируется целиком, теги создаёт только рендер из закрытого набора, схемы ссылок из белого списка.","Название, тело и теги проходят токенизацию и стеммер (около 70 окончаний, ё → е); запрос превращается в термы «стем»* через AND, и ввод пользователя не попадает в синтаксис FTS.","Из текста выделяются [[ссылки]] и пишутся в links по title_key; backlinks — это соединение по dst_key, поэтому ссылка на ещё не созданную заметку оживает сама.","В notes_fts пишутся стемы, ранжирование bm25(notes_fts, 8, 1, 4) (название, тело, теги), сниппет строит Go-код; сохранение — одна транзакция: заметка, теги, ссылки, FTS и версия.","Правки автосохранения в пределах 5 минут вливаются в последнюю версию, Ctrl+S создаёт новую; diff построчный на LCS; при устаревшем rev сервер отвечает 409 с актуальной заметкой.","Экспорт отдаёт zip из .md с front matter; импорт папки, файлов или zip идёт через API, пути из архива никуда не пишутся, поэтому zip-slip невозможен.",[1200,1203,1206,1209],{"title":1201,"text":1202},"Свой Markdown-рендер вместо goldmark","Безопасность — свойство конструкции, а не очистка после: исходник экранируется, теги из закрытого набора. Цена — меньше возможностей, чем у CommonMark (нет таблиц и картинок); зато зависимости остаются на уровне stdlib и SQLite.",{"title":1204,"text":1205},"FTS5 над стемами, сниппет в Go","В индекс пишутся стемы, а не исходный текст, поэтому штатный snippet() FTS5 видел бы только стемы. Совпадения подсвечивает Go-код, он же экранирует HTML.",{"title":1207,"text":1208},"title_key вместо NOCASE","Уникальность и вики-ссылки строятся на названии в нижнем регистре с нормализованными пробелами, потому что NOCASE в SQLite для кириллицы не работает.",{"title":1210,"text":1211},"rev для конфликтов, ver для истории","rev считает любые изменения и защищает от затирания чужой правки, ver нумерует версии. «Перезаписать моей» фиксирует версию, так что чужая правка остаётся в истории.",[1213,1214],"Морфология упрощённая: беглые гласные, чередования и неправильные формы не работают, у коротких слов возможны ложные совпадения. Переименование заметки не переписывает [[ссылки]] в других заметках.","Один общий токен, одно соединение с БД, конфликты разрешаются целиком; граф рассчитан на сотни заметок (раскладка O(n²) на шаг). В Markdown нет таблиц, картинок и сносок.","Ранжирование bm25 и вес названия, морфология, мусорные запросы, backlinks и граф, версии и слияние правок, конфликт rev, XSS (около 30 нагрузок), diff, экспорт → импорт, безопасность zip; go test -race в CI.","README проекта notes-search: «Архитектура», «Схема БД», «Поиск», «Решения», «Ограничения»",{"slug":220,"kind":793,"go_lines":1218,"go_test_lines":1076,"tests":468,"endpoints":796,"html":1219,"css":1220,"js":938},2027,174,192,{"slug":268,"title":269,"stack":1222,"summary":1223,"nodes":1224,"edges":1245,"flows":1254,"decisions":1263,"tradeoffs":1276,"tests":1279,"source":1280,"stats":1281},[24,271,272,273,198,274,142,275,276,277,278,203,28,205],"Учебная реализация Raft на чистой stdlib: три узла, выборы, репликация, коммит по кворуму, WAL с fsync, снапшоты, read-index. Главное — ядро без времени и сети, которое один и тот же код гоняет и в рантайме, и в детерминированном симуляторе.",[1225,1227,1229,1232,1234,1237,1239,1242],{"id":734,"label":1226,"row":733,"col":733,"span":674,"kind":734},"Клиент\nPUT · GET · DELETE",{"id":753,"label":1228,"row":733,"col":674,"span":674,"kind":734},"Дашборд :8098\nSSE · аварии (демо)",{"id":1230,"label":1231,"row":674,"col":733,"span":718,"kind":738},"node","Рантайм узла\nHTTP\u002FJSON · тик 25 мс · 307",{"id":738,"label":1233,"row":718,"col":733,"span":674,"kind":738},"raft.Node\nTick() · Step(msg)",{"id":1235,"label":1236,"row":718,"col":674,"span":674,"kind":734},"peers","Два других узла\nvote · append · snapshot",{"id":742,"label":1238,"row":686,"col":733,"span":674,"kind":742},"Хранилище\nWAL + fsync · state.json",{"id":1240,"label":1241,"row":686,"col":674,"span":674,"kind":738},"kv","KV-автомат\nput · del · снапшот",{"id":1243,"label":1244,"row":671,"col":733,"span":718,"kind":738},"sim","Симулятор\nвиртуальное время · seed",[1246,1247,1248,1249,1250,1251,1252,1253],{"n":674,"from":734,"to":1230},{"n":718,"from":753,"to":1230},{"n":686,"from":1230,"to":738},{"n":671,"from":738,"to":1235},{"n":761,"from":738,"to":1240},{"n":659,"from":738,"to":742,"side":762},{"n":383,"from":1240,"to":742},{"n":835,"from":1243,"to":742},[1255,1256,1257,1258,1259,1260,1261,1262],"Клиент шлёт PUT, GET или DELETE на любой из узлов 8095–8097; не лидер отвечает 307 с Location на лидера (и заголовком X-Raft-Leader), а если лидер неизвестен, то 503 с Retry-After.","Дашборд раз в 200 мс получает по SSE роль, term, commit\u002Fapplied и хвост лога; кнопки «остановить узел», «оборвать сеть» и «убить лидера» работают только при SEED_DEMO=1 и идут по настоящему HTTP.","Рантайм тикает ядро каждые 25 мс (Tick) и передаёт ему входящие сообщения (Step); исходящие сообщения возвращаются вызывающему, сам raft.Node не знает ни о времени, ни о сети.","Выборы (vote), репликация (append) и snapshot идут HTTP\u002FJSON-сообщениями, ответы это отдельные сообщения, поэтому сеть может терять и переупорядочивать пакеты; индекс коммитится, когда его хранит большинство и запись относится к текущему term.","Закоммиченные записи применяются к автомату состояний (словарь, команды put и del); ответ PUT уходит после коммита на кворуме, а линеаризуемый GET ждёт applied >= readIndex.","Ответ уходит только после fsync: term и votedFor пишутся в state.json (временный файл, fsync, rename), записи лога в WAL с CRC32; битый хвост после крэша обрезается при загрузке.","После SNAPSHOT_EVERY применённых записей (по умолчанию 200) снапшот словаря пишется новым поколением файлов: новый WAL, затем атомарная публикация snapshot.bin, затем удаление старого WAL.","Тот же raft.Node работает в симуляторе: время виртуальное, сеть управляемая (задержки, потери, обрывы), крэш и рестарт узла идут из его хранилища; один seed воспроизводит запуск побайтно, а инварианты и линеаризуемость проверяются после каждого шага.",[1264,1267,1270,1273],{"title":1265,"text":1266},"Ядро без горутин и часов","raft.Node получает время только через Tick() и сообщения через Step(), а исходящие возвращает вызывающему. Поэтому один и тот же код работает в рантайме (тик 25 мс, HTTP) и в симуляторе, а сбой воспроизводится по seed.",{"title":1268,"text":1269},"Коммит только записей текущего term","Индекс коммитится, когда его хранит большинство и запись относится к текущему term (раздел 5.4.2 статьи), поэтому новый лидер сразу кладёт в лог no-op. Целенаправленного теста на фигуру 8 нет, и это записано в ограничениях.",{"title":1271,"text":1272},"Read-index вместо лизов","Лидер запоминает commitIndex, рассылает heartbeat, ждёт подтверждения большинства и только потом отвечает на GET. Корректность не зависит от часов, а отрезанный лидер ответить не может.",{"title":1274,"text":1275},"Снапшот «поколениями»","Новый WAL, затем атомарная публикация snapshot.bin (точка коммита), затем удаление старого WAL. Крэш на любом шаге оставляет целое старое или целое новое состояние.",[1277,1278],"Состав фиксирован (ровно три узла), нет динамического членства, pre-vote и check-quorum, клиентских сессий и exactly-once: после 504 исход неизвестен. Снапшот идёт одним сообщением и целиком в памяти, каждый PUT это отдельный fsync, TLS и аутентификации нет.","Проверка собственная: симулятор с идеальным диском и проверка линеаризуемости на малых историях, это не Jepsen. Одна ручная мутация, коммит записей прошлых term, тестами не ловится.","Выбор одного лидера на term (40 seed-ов с потерями, крэшами и обрывами), переизбрание после падения лидера, репликация, отсутствие коммита без кворума, откат хвоста отрезанного лидера, рестарт с диска, голос не отдаётся дважды после рестарта, снапшоты и догон, линеаризуемость под хаосом (30 seed-ов), детерминизм трассы; HTTP-тесты узла и сам чекер линеаризуемости; go test -race в CI.","README проекта raft-kv: «Архитектура», «Как устроены части», «Инварианты», «Тесты», «Чего здесь нет»",{"slug":268,"kind":793,"go_lines":1282,"go_test_lines":1283,"tests":1284,"endpoints":1285,"html":1286,"css":1219,"js":1287},2646,895,27,13,91,213,{"slug":295,"title":296,"stack":1289,"summary":1290,"nodes":1291,"edges":1312,"flows":1320,"decisions":1328,"tradeoffs":1341,"tests":1344,"source":1345,"stats":1346},[24,299,27,226,300,301,302,303,304,305,306,203,204,28,205],"Сервис аутентификации на stdlib crypto и SQLite: EdDSA-JWT с ротацией ключей и JWKS, refresh-токены с ротацией и reuse-detection, TOTP, RBAC, lockout, аудит. Главное — потоки токенов и честная модель угроз с тем, что остаётся незакрытым.",[1292,1294,1297,1299,1301,1304,1306,1309],{"id":1043,"label":1293,"row":733,"col":733,"span":674,"kind":734},"Браузер и клиенты\nBearer · cookie rt",{"id":1295,"label":1296,"row":733,"col":674,"span":674,"kind":734},"consumer","Сторонний сервис\nпроверяет по JWKS",{"id":1000,"label":1298,"row":674,"col":733,"span":718,"kind":738},"HTTP-слой\nCSP · Origin · CSRF · лимит IP",{"id":738,"label":1300,"row":718,"col":733,"span":674,"kind":738},"Ядро auth\nPBKDF2 · TOTP · RBAC",{"id":1302,"label":1303,"row":718,"col":674,"span":674,"kind":738},"jwt","JWT EdDSA\nkid · кольцо ключей",{"id":740,"label":1305,"row":686,"col":733,"span":674,"kind":742},"SQLite\nсессии · токены · ключи",{"id":1307,"label":1308,"row":686,"col":674,"span":674,"kind":738},"jwks","JWKS\n\u002F.well-known\u002Fjwks.json",{"id":1310,"label":1311,"row":671,"col":733,"span":718,"kind":742},"audit","Журнал аудита\nтолько дозапись",[1313,1314,1315,1316,1317,1318,1319],{"n":674,"from":1043,"to":1000},{"n":718,"from":1000,"to":738},{"n":686,"from":738,"to":1302},{"n":671,"from":738,"to":740},{"n":761,"from":1302,"to":1307},{"n":659,"from":1307,"to":1295,"side":765},{"n":383,"from":740,"to":1310},[1321,1322,1323,1324,1325,1326,1327],"Вход: логин и пароль, при включённой 2FA второй шаг с кодом TOTP или резервным кодом; в ответе access JWT на 10 минут, а refresh приходит в HttpOnly-cookie SameSite=Strict вместе с CSRF-токеном.","Middleware проверяет Origin, CSRF и лимит запросов с IP, затем подпись и срок access-токена, потом по БД что сессия жива, а права берутся из БД на каждом запросе, поэтому отзыв и смена роли действуют сразу.","Токены подписывает кольцо ключей Ed25519: принимается только EdDSA, kid берётся из белого списка; POST \u002Fapi\u002Fadmin\u002Fkeys\u002Frotate выпускает новый ключ, а старый остаётся для проверки ещё access_ttl + 1 минуту.","В БД лежат хеши пароля (PBKDF2, 600 000 итераций), refresh-токенов (SHA-256) и резервных кодов; refresh гасит старый токен и выдаёт новый, а повторное предъявление использованного отзывает всю семью и пишет token.reuse_detected.","Публичные ключи отдаются на \u002F.well-known\u002Fjwks.json (RFC 7517\u002F8037) вместе с kid.","Сторонний сервис берёт kid из заголовка JWT, достаёт ключ из JWKS (кеш на минуту) и проверяет подпись, iss, aud, typ и exp; об отзыве сессии он узнает не раньше конца жизни access-токена.","Вход, неудачи, блокировки, 2FA, сброс пароля, отзывы сессий, действия админа, ротация ключей и отказы по правам пишутся в таблицу audit; триггеры БД запрещают UPDATE и DELETE, пароли и токены в журнал не попадают.",[1329,1332,1335,1338],{"title":1330,"text":1331},"Проверка сессии по БД на каждом запросе","Access-токен сверяется с БД (сессия не отозвана, пользователь не отключён, права актуальны). Это стоит одного запроса к SQLite и взамен даёт немедленные «выйти на всех устройствах», блокировку и смену роли.",{"title":1333,"text":1334},"Семья refresh-токенов и reuse-detection","Каждое обновление гасит старый токен и выдаёт новый. Если использованный токен предъявлен снова, считается, что его украли: отзывается вся сессия, мёртвыми становятся и украденный, и легитимный токен.",{"title":1336,"text":1337},"Только EdDSA и свой разбор JWT на stdlib","Алгоритмы none и HS256 отклоняются до проверки подписи (защита от alg-confusion), kid из белого списка, длина токена ограничена. Тесты включают HS256 с публичным ключом в роли секрета.",{"title":1339,"text":1340},"Одинаковый ответ и время для неизвестного логина","Для несуществующего пользователя считается фиктивный хеш, ответ и время совпадают с неверным паролем, а forgot всегда отвечает 202. Исключение осталось: регистрация сообщает, что логин или почта заняты.",[1342,1343],"Секрет TOTP и приватные ключи подписи лежат в БД открытым текстом: с дампом БД можно подделать токены и 2FA. Пароли хешируются PBKDF2, а не argon2id, резервные коды быстрым SHA-256.","Не прошёл независимый аудит, нет OAuth\u002FOIDC, WebAuthn и подтверждения почты; письмо сброса это заглушка в логе. Лимитер и блокировки живут в памяти и в SQLite одного процесса, а access-токен не отозвать у сторонних потребителей до exp.","JWT (подделка, exp\u002Fnbf\u002Fiss\u002Faud\u002Ftyp, alg=none, HS256 с публичным ключом, ротация), TOTP по векторам RFC 6238 и RFC 4226, ротация refresh и reuse-detection, блокировка с экспоненциальной задержкой, 2FA и резервные коды, RBAC, сброс пароля, аудит (запрет UPDATE\u002FDELETE), cookie-флаги, CSRF, CORS, Origin, rate limit; JWKS проверяется независимым кодом; go test -race в CI.","README проекта auth-service: «Возможности», «Потоки токенов», «Модель угроз», «Схема БД», «Ограничения»",{"slug":295,"kind":793,"go_lines":1347,"go_test_lines":1348,"tests":1349,"endpoints":528,"html":1350,"css":1351,"js":1352},2826,1393,50,23,166,500,{"slug":321,"title":322,"stack":1354,"summary":1355,"nodes":1356,"edges":1379,"flows":1391,"decisions":1402,"tradeoffs":1414,"tests":1417,"source":1418,"stats":1419},[24,27,226,143,324,325,326,327,328,329,330,28,205,302,331],"Учёт личных финансов: Go-бинарник с SQLite и встроенным фронтендом на Nuxt 4 (SPA). Главное — деньги целыми минорными единицами, мультивалютность с ручными курсами, расписание и импорт CSV, изоляция пользователей.",[1357,1360,1363,1365,1368,1371,1374,1377],{"id":1358,"label":1359,"row":733,"col":733,"span":674,"kind":734},"spa","Nuxt 4 SPA\nPinia · SVG-графики",{"id":1361,"label":1362,"row":733,"col":674,"span":674,"kind":734},"csv","CSV из банка\nимпорт · экспорт",{"id":25,"label":1364,"row":674,"col":733,"span":718,"kind":738},"HTTP API на Go\nсессия в cookie · Origin · user_id",{"id":1366,"label":1367,"row":718,"col":733,"span":674,"kind":738},"imp","Импорт CSV\nпревью · пробный прогон",{"id":1369,"label":1370,"row":718,"col":674,"span":674,"kind":738},"rec","Расписание\ncron-правило · нагон",{"id":1372,"label":1373,"row":686,"col":733,"span":674,"kind":738},"ledger","Счета и операции\nминорные единицы · big.Int",{"id":1375,"label":1376,"row":686,"col":674,"span":674,"kind":738},"rep","Бюджеты и отчёты\nмесяц · тренды",{"id":740,"label":1378,"row":671,"col":733,"span":718,"kind":742},"SQLite\nmodernc · без CGO",[1380,1381,1382,1383,1384,1385,1386,1387,1388,1389],{"n":674,"from":1358,"to":25},{"n":718,"from":1361,"to":25},{"n":686,"from":25,"to":1372,"side":762},{"n":671,"from":25,"to":1366},{"n":761,"from":25,"to":1369},{"n":659,"from":1366,"to":1372},{"n":383,"from":1369,"to":1372},{"n":835,"from":1372,"to":1375},{"n":837,"from":1372,"to":740},{"n":1390,"from":1375,"to":740},10,[1392,1393,1394,1395,1396,1397,1398,1399,1400,1401],"Браузер работает с API через JSON; сессия живёт в httpOnly-cookie (SameSite=Lax), в базе хранится только SHA-256 токена, а у запросов на запись проверяется Origin.","Файл CSV и экспорт (CSV с защитой от formula-injection и полный JSON) проходят через тот же API, отдельных каналов нет.","Каждый запрос привязан к user_id из сессии; обычный CRUD счетов, категорий, курсов и операций идёт прямо в слой счетов и операций, с фильтрами q, kind, account_id, category_id, tag, from, to.","POST \u002Fapi\u002Fimport\u002Fpreview определяет разделитель и кодировку и показывает сопоставление колонок; \u002Fcommit пишет строки, пропускает дубли и возвращает отчёт по ошибочным строкам (не более 20 000 строк за раз).","Cron-подобное правило из пяти полей (с точностью до дня) исполняется при открытии приложения и по кнопке \u002Fapi\u002Frecurring\u002Frun; пропущенные даты нагоняются, фонового планировщика нет.","Импортированные строки становятся обычными транзакциями: дубли пропускаются, недостающие категории создаются, переводы не импортируются.","Регулярные платежи тоже становятся обычными транзакциями и хранят recurring_id; last_date правила двигается вперёд.","Отчёты читают операции целиком в память и считают в базовой валюте по текущим курсам; наклон тренда считается МНК в целых числах.","Суммы пишутся целыми минорными единицами (amount_minor > 0), курс хранится целым ×10⁶, пересчёт идёт через big.Int; баланс счёта не хранится, а считается запросом.","Лимиты бюджета (limit_minor) и пороги предупреждения (warn_pct) лежат в таблице budgets и сравниваются с расходами месяца; отчёты читают те же таблицы и ничего не пишут.",[1403,1405,1408,1411],{"title":334,"text":1404},"В базе, API и браузере суммы это целые числа копеек или центов, без float. Пересчёт валют идёт через big.Int с округлением «от нуля», а округление и разбор сумм покрыты тестами.",{"title":1406,"text":1407},"Свои SVG-графики вместо библиотеки","Нужны три простых типа графиков, а библиотека добавила бы десятки КБ JS. Свои (около 250 строк) дают контроль над доступностью (role=\"img\", текстовая таблица), темами через CSS-переменные и строгим CSP.",{"title":1409,"text":1410},"SPA без SSR","Nuxt собирается как nuxt generate с ssr:false: данные сессии нельзя пререндерить. Go отдаёт встроенную web\u002Fdist и 200.html для неизвестных путей, а CSP получает хэши встроенных скриптов вместо unsafe-inline для скриптов.",{"title":1412,"text":1413},"Баланс считается, а не хранится","Баланс счёта = начальный остаток + доходы − расходы − исходящие переводы + входящие переводы. Хранимого поля нет, поэтому оно не может разойтись с операциями.",[1415,1416],"Курсы ручные и текущие, истории курсов нет: прошлое пересчитывается по сегодняшним курсам. Базовую валюту нельзя сменить, бюджет одинаков каждый месяц, платежи создаются только при открытии приложения.","Нет смены и восстановления пароля, SQLite и один процесс, отчёты читают операции целиком в память. В style-src остался unsafe-inline (стили Vue), SSR отключён.","Деньги (разбор, формат, округление, пересчёт, round-trip), бюджеты, cron-правила и нагон пропущенных дат, импорт CSV, изоляция пользователей, API, отчёты, демо-данные, TestRealDist (встроенная сборка отдаётся, CSP без unsafe-inline для скриптов); во фронтенде vitest, ESLint и typecheck; go test -race в CI.","README проекта finance-tracker: «Возможности», «Архитектура», «Схема БД», «API», «Ограничения»",{"slug":321,"kind":793,"go_lines":1420,"go_test_lines":1421,"tests":1422,"endpoints":611,"html":1423,"css":1424,"js":1425},3741,1371,30,24,1148,72,{"slug":346,"title":347,"stack":1427,"summary":1428,"nodes":1429,"edges":1451,"flows":1460,"decisions":1469,"tradeoffs":1482,"tests":1485,"source":1486,"stats":1487},[24,198,179,349,203,204,350,351,28,205],"Совместный холст 256×256 на WebSocket: свой RFC 6455 на stdlib, бинарный протокол, снимок и дельты, снапшот плюс журнал. Главное — согласованность дельт и восстановление после падения, при том что каждый пиксель стоит 4 бита.",[1430,1432,1434,1437,1440,1443,1446,1449],{"id":1043,"label":1431,"row":733,"col":733,"span":674,"kind":734},"Браузер\nCanvas 2D · зум · таймлапс",{"id":946,"label":1433,"row":733,"col":674,"span":674,"kind":734},"Админ\nBearer ADMIN_TOKEN",{"id":1435,"label":1436,"row":674,"col":733,"span":718,"kind":738},"srv","HTTP + WebSocket\nOrigin · лимиты · хаб 50 мс",{"id":1438,"label":1439,"row":718,"col":733,"span":674,"kind":738},"wall","Ядро wall\nодин мьютекс · seq",{"id":1441,"label":1442,"row":718,"col":674,"span":674,"kind":738},"cool","Кулдаун и баны\nсессия · IP · rate limit",{"id":1444,"label":1445,"row":686,"col":733,"span":674,"kind":738},"canvas","Холст\n4 бита на пиксель",{"id":1447,"label":1448,"row":686,"col":674,"span":674,"kind":738},"hist","История в памяти\nстарый и новый цвет",{"id":742,"label":1450,"row":671,"col":733,"span":718,"kind":742},"Диск\ncanvas.snap + journal.log",[1452,1453,1454,1455,1456,1457,1458,1459],{"n":674,"from":1043,"to":1435},{"n":718,"from":946,"to":1435},{"n":686,"from":1435,"to":1438},{"n":671,"from":1438,"to":1441},{"n":761,"from":1438,"to":1444},{"n":659,"from":1438,"to":1447},{"n":383,"from":1444,"to":742},{"n":835,"from":1447,"to":742},[1461,1462,1463,1464,1465,1466,1467,1468],"Браузер открывает WS \u002Fws?sid=…, получает сжатый снимок (0x01: seq, размер, deflate-raw холста) и дальше только дельты (0x02: firstSeq и до 2000 записей); пиксель уходит в 6 байтах (0x10: x, y, цвет).","Админ-запросы идут с заголовком Authorization: Bearer; токен сравнивается за постоянное время, неверные попытки лимитируются по IP, а без ADMIN_TOKEN админ-API выключено (403).","Сервер проверяет Origin, размеры сообщений (WS до 64 байт, тело до 4 КиБ) и подписанную сессию, затем передаёт установку в wall; хаб рассылает накопленные дельты раз в 50 мс, а медленный клиент отключается и перечитывает снимок.","Перед записью wall спрашивает кулдаун: строго один пиксель за COOLDOWN_SEC на сессию и не больше IP_BURST за то же скользящее окно на IP; бан сессии или IP отвечает 403, лишнее 429.","Единый мьютекс упорядочивает запись в холст, журнал и очередь дельт; пиксель занимает 4 бита (чётный индекс в старшей тетраде байта), 256×256 = 32 КиБ.","Каждая установка добавляется в историю (до MAX_HISTORY записей) со старым и новым цветом; клиент откатывает таймлапс назад от текущего состояния, базовый снимок не нужен.","Снапшот canvas.snap пишется раз в SNAPSHOT_SEC во временный файл с crc32, fsync и rename; восстановление берёт снапшот, затем записи журнала с большим seq и обрезает недописанный хвост.","journal.log (10 байт на запись: время, x, y, старый и новый цвет, флаги очистки и бота) дописывается при каждом пикселе, fsync раз в 30 секунд; при превышении MAX_HISTORY на 25% журнал переписывается с хвостом.",[1470,1473,1476,1479],{"title":1471,"text":1472},"WebSocket вместо SSE и POST","Нужен двунаправленный бинарный канал: пиксель в 6 байтах и сразу ok или err, дельты бинарными. SSE это текст (base64 раздувает трафик на треть) и отдельные POST на каждое действие. Запасной путь без сокета есть: POST \u002Fapi\u002Fpixel.",{"title":1474,"text":1475},"Снимок со seq и дельты с firstSeq","Клиент регистрируется в хабе и под той же блокировкой получает снимок со seq. Дельта несёт firstSeq: клиент пропускает записи с seq не больше своего, а при пропуске переподключается.",{"title":1477,"text":1478},"В журнале хранится старый цвет","Запись «старый\u003C\u003C4 | новый» позволяет клиенту откатывать историю от текущего состояния, поэтому базовый снимок для таймлапса не нужен, а усечение журнала лишь сокращает глубину истории.",{"title":1480,"text":1481},"Снапшот и журнал вместо одного файла","Снапшот через временный файл и rename гарантирует целостность, журнал даёт последние изменения; битый снапшот (crc) восстанавливается из журнала, а не стыкующийся журнал начинается заново.",[1483,1484],"Всё в одном процессе и на одном диске; кулдауны, сессии и rate limit в памяти и сбрасываются при рестарте. Сессия это идентификатор в localStorage, поэтому защита от простых ботов, а не от атаки с множества IP.","Журнал без fsync на каждый пиксель (fsync раз в 30 с), журнал целиком в памяти, расширений WebSocket нет, клиенту нужен DecompressionStream. Браузерных тестов в репозитории нет.","Кулдаун (сессия, IP, скользящее окно, переполнение таблицы), упаковка и граничные координаты, кодеки и битые сообщения, хранилище (crc, оборванный хвост), восстановление без снапшота и из снапшота с журналом, компактация, откат истории, непрерывность дельт, API (лимиты, Origin, 401\u002F403\u002F429), WebSocket с двумя клиентами и флуд, права админа, бан, нагрузка на 300 клиентов; go test -race в CI.","README проекта pixel-wall: «Возможности», «Архитектура», «Бинарный протокол», «Формат хранения», «Ограничения»",{"slug":346,"kind":793,"go_lines":1488,"go_test_lines":1489,"tests":1490,"endpoints":1491,"html":1492,"css":1220,"js":1493},2715,1910,77,16,106,625,{"slug":366,"title":367,"stack":1495,"summary":1496,"nodes":1497,"edges":1524,"flows":1533,"decisions":1542,"tradeoffs":1555,"tests":1558,"source":1559,"stats":1560},[24,198,370,371,372,373,374,375,205,28],"CLI для анализа git-репозиториев: разбор потока git log, агрегаторы метрик и три вывода из одной структуры — терминал, самодостаточный HTML и локальный сервер. Главное — не писать свой разбор .git, а доверить историю и переименования самому git.",[1498,1500,1503,1506,1509,1512,1515,1518,1521],{"id":368,"label":1499,"row":733,"col":733,"span":674,"kind":734},"Командная строка\nчетыре команды и флаги",{"id":1501,"label":1502,"row":733,"col":674,"span":674,"kind":742},"repo","Репозиторий\n.git · .mailmap",{"id":1504,"label":1505,"row":674,"col":733,"span":718,"kind":738},"gitlog","gitlog (os\u002Fexec)\ngit log -z --numstat -M · потоковый разбор",{"id":1507,"label":1508,"row":718,"col":733,"span":718,"kind":738},"agg","analyze.Aggregator\nкоммит за коммитом · переименования · фильтр lock\u002Fvendor",{"id":1510,"label":1511,"row":686,"col":733,"span":718,"kind":738},"res","analyze.Result\nединая структура · JSON",{"id":1513,"label":1514,"row":671,"col":733,"span":674,"kind":738},"term","term\nANSI-таблицы · спарклайны",{"id":1516,"label":1517,"row":671,"col":674,"span":674,"kind":738},"report","report\nhtml\u002Ftemplate · inline SVG",{"id":1519,"label":1520,"row":761,"col":733,"span":674,"kind":734},"file","report.html\nодин файл или stdout",{"id":1522,"label":1523,"row":761,"col":674,"span":674,"kind":734},"serve","serve\n127.0.0.1 · Host-guard",[1525,1526,1527,1528,1529,1530,1531,1532],{"n":674,"from":368,"to":1504},{"n":718,"from":1501,"to":1504},{"n":686,"from":1504,"to":1507},{"n":671,"from":1507,"to":1510},{"n":761,"from":1510,"to":1513},{"n":659,"from":1510,"to":1516},{"n":383,"from":1516,"to":1519},{"n":835,"from":1516,"to":1522},[1534,1535,1536,1537,1538,1539,1540,1541],"Команда разбирает флаги (-rev, -since, -until, -max-commits, -top, -depth, -json, -color) и проверяет репозиторий и ревизию; нет git, не репозиторий или неизвестная ревизия дают код возврата 3, неверные аргументы 2.","git читает историю и .mailmap сам; поэтому имена авторов приходят уже с учётом %aN\u002F%aE, а переименования находит git -M, без собственного разбора packfile.","gitlog запускает git log -z --numstat -M через os\u002Fexec и разбирает поток по одной записи: -z исключает проблемы с кавычками и юникодом в путях, в памяти живёт один коммит; на терминале в stderr рисуется прогресс.","Aggregator принимает коммиты от новых к старым и копит метрики; при встрече old => new запоминает переименование (с разрешением цепочек), поэтому более ранние правки old попадают на актуальное имя; lock-файлы, vendor и min-файлы пропускаются, слияния считаются отдельно.","Результат — одна структура analyze.Result (JSON при -json); term рисует из неё таблицы с цветами, столбцы и спарклайны, NO_COLOR и запись в канал или файл отключают цвет, на Windows включаются VT и UTF-8.","report через html\u002Ftemplate собирает HTML с inline SVG-графиками, CSS и небольшим JS переключателя темы; внешних запросов нет, имена и пути экранируются, тема следует за prefers-color-scheme.","report пишет один файл (-o файл, по умолчанию gitpulse-report.html) или в stdout с -o -.","serve принимает только loopback-адрес в -addr, отвергает запросы с чужим заголовком Host (защита от DNS-rebinding), ставит CSP и no-store, держит результат в кеше (по умолчанию 30 с, \u002F?refresh=1 сбрасывает) и отдаёт \u002F, \u002Fapi\u002Fstats и \u002Fhealthz.",[1543,1546,1549,1552],{"title":1544,"text":1545},"git через os\u002Fexec, а не разбор .git","Собственный разбор объектов и packfile с дельта-цепочками потребовал бы ещё commit-graph, .mailmap, замены ссылок, shallow- и partial-клонов и обнаружения переименований: тысячи строк с риском ошибиться в метриках. git log --numstat -M решает это надёжно, цена — зависимость от установленного git, которая проверяется явно.",{"title":1547,"text":1548},"Поток с -z вместо загрузки всего лога","Лог читается по одной записи, память растёт от числа различных файлов и авторов, а не коммитов; -max-commits помечает отчёт как усечённый.",{"title":1550,"text":1551},"Одна структура Result для всех выводов","Терминал, HTML, JSON и сервер показывают числа из одной analyze.Result, поэтому расходиться им не из чего, а тесты сверяют метрики в одном месте.",{"title":1553,"text":1554},"Самодостаточный HTML и loopback-сервер","В отчёте есть имена авторов, поэтому он одним файлом без внешних запросов и шрифтов, а serve слушает только 127.0.0.1 и проверяет Host.",[1556,1557],"Нужен установленный git; время по UTC автора; слияния не анализируются; копирование файлов не отслеживается, переименования видны лишь там, где их находит git -M.","Рост языков — оценка «добавлено минус удалено», а bus factor и риск — эвристики по числу строк; память растёт с числом файлов и авторов, в -json есть почты авторов, в HTML нет.","Синтетические репозитории через git и метрики на заранее посчитанных данных: слияния, переименования (в том числе правка по новому пути), бинарные файлы, кириллица в путях и именах, lock-файлы, пустой репозиторий, один коммит, лимит коммитов, диапазоны, плохие ревизии; разбор потока (таб в пути, битые записи), golden-тест структуры HTML, самодостаточность и экранирование отчёта, сервер (кеш, Host-guard, методы), коды возврата, JSON CLI, NO_COLOR; go test -race в CI.","README проекта gitpulse: «Команды», «Что считается», «Архитектура», «HTML-отчёт», «Тесты», «Ограничения»; код internal\u002Fgitlog, internal\u002Fanalyze, internal\u002Freport, internal\u002Fterm, internal\u002Fserve",{"slug":366,"kind":793,"go_lines":1561,"go_test_lines":1562,"tests":468,"endpoints":686,"html":733,"css":733,"js":733},2133,735,1793288284524]