<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Проекты on Kirill's homepage</title><link>https://kkucherenkov.github.io/projects/</link><description>Recent content in Проекты on Kirill's homepage</description><generator>Hugo</generator><language>ru</language><copyright>&lt;a href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank" rel="noopener"&gt;CC BY-NC 4.0&lt;/a&gt;</copyright><atom:link href="https://kkucherenkov.github.io/projects/index.xml" rel="self" type="application/rss+xml"/><item><title>Course Shelf</title><link>https://kkucherenkov.github.io/projects/course_shelf/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://kkucherenkov.github.io/projects/course_shelf/</guid><description>&lt;h2 id="для-чего"&gt;Для чего&lt;/h2&gt;
&lt;p&gt;Курсы с Udemy и Coursera, записи конференций и воркшопов часто лежат на диске
обычными папками. Course Shelf сканирует такую папку и собирает из неё каталог
курсов, секций и уроков. Каталог открывается в браузере и в мобильном
приложении. Файлы остаются на месте: Course Shelf их не копирует и не
перекодирует.&lt;/p&gt;
&lt;h2 id="библиотека"&gt;Библиотека&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Каталог из структуры папок.&lt;/strong&gt; Папки и имена файлов задают курсы, секции и
порядок уроков. Скан привязывает к урокам субтитры и материалы, которые лежат
рядом с видео.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;course.json&lt;/code&gt; для точной настройки.&lt;/strong&gt; Файл в папке курса задаёт названия,
порядок уроков и метаданные поверх того, что вывел скан.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Метаданные из сети.&lt;/strong&gt; Скраперы берут обложки, описания, авторов и теги с
Udemy, Coursera и YouTube. Для других сайтов подходят декларативные
определения скраперов и общий разбор schema.org и OpenGraph.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Пересканирование курса.&lt;/strong&gt; Один курс обновляется без прогона всей
библиотеки.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="просмотр"&gt;Просмотр&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Стриминг без перекодирования.&lt;/strong&gt; Видео идёт по HTTP с byte-range, перемотка
срабатывает сразу.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Прогресс.&lt;/strong&gt; Плеер запоминает позицию, продолжает с неё и переходит к
следующему уроку. После 90% урок отмечается пройденным.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Главная&lt;/strong&gt; показывает начатые уроки, свежие поступления, завершённые курсы и
статистику просмотра за неделю.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Каталог с фильтрами&lt;/strong&gt; по статусу, библиотеке, длительности и автору.
Фильтры хранятся в адресной строке, и ссылка открывает ту же выборку.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Заметки и закладки.&lt;/strong&gt; К каждому уроку можно вести заметку в Markdown и
ставить закладки на нужную секунду. Заметки видит только их автор.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="транскрипты-и-поиск"&gt;Транскрипты и поиск&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Локальная расшифровка.&lt;/strong&gt; whisper.cpp расшифровывает уроки без субтитров на
том же сервере, без облачных API.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Поиск&lt;/strong&gt; находит курсы и отдельные уроки одним запросом и показывает
фрагмент с найденным местом. На десктопе он открывается командной палитрой.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Транскрипт рядом с плеером.&lt;/strong&gt; Ссылка с &lt;code&gt;?t=&lt;/code&gt; открывает урок на нужной
секунде.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Квизы.&lt;/strong&gt; Вопросы по уроку генерирует LLM: локальный llama.cpp или модель
через OpenRouter.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="мобильное-приложение"&gt;Мобильное приложение&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Flutter-клиент&lt;/strong&gt; для iOS и Android с тем же каталогом.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Загрузки.&lt;/strong&gt; Уроки скачиваются с докачкой после обрыва и хранятся на
устройстве в зашифрованном виде.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Офлайн.&lt;/strong&gt; Позиция, заметки и закладки сохраняются на устройстве и уходят на
сервер, когда появляется связь. Если на сервере прогресс новее, остаётся
серверная версия.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="несколько-пользователей"&gt;Несколько пользователей&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Доступ по выдаче.&lt;/strong&gt; Роли USER и ADMIN, доступ выдаётся на библиотеку
целиком или на отдельный курс.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Администрирование.&lt;/strong&gt; Сводка по инстансу, управление пользователями и вход от
имени пользователя, чтобы увидеть его каталог.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Бэкап.&lt;/strong&gt; Одна кнопка запускает &lt;code&gt;pg_dump&lt;/code&gt; и отдаёт ссылку на архив.
Медиафайлы в бэкап не входят: Course Shelf ими не владеет.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="под-капотом"&gt;Под капотом&lt;/h2&gt;
&lt;p&gt;Монорепо из трёх приложений: бэкенд на NestJS, веб на Nuxt и клиент на Flutter.
Контракт API описан один раз, в OpenAPI 3.1 и AsyncAPI 3.0, и клиенты для
TypeScript и Dart генерируются из него. Бэкенд отклоняет запросы, которых нет в
спецификации, а CI падает, если код разошёлся с контрактом. События скана и
фоновых задач приходят в клиенты через Centrifugo. Интерфейс переведён на
русский и английский.&lt;/p&gt;</description></item><item><title>Coriolis Companion</title><link>https://kkucherenkov.github.io/projects/coriolis/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://kkucherenkov.github.io/projects/coriolis/</guid><description>&lt;h2 id="для-чего"&gt;Для чего&lt;/h2&gt;
&lt;p&gt;Coriolis: The Third Horizon — научно-фантастическая ролевая игра от Free
League в духе «Тысячи и одной ночи». Приложение заменяет бумажные листы и
держит партию в общем состоянии: броски, очки Тьмы и чат видят все участники
сессии. Фанатский проект, с издателем не связан.&lt;/p&gt;
&lt;h2 id="для-игроков"&gt;Для игроков&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Лист персонажа&lt;/strong&gt; по правилам Coriolis: иконы, концепция, происхождение,
характеристики от 1 до 5, общие и продвинутые навыки, таланты, снаряжение,
состояния, нагрузка, критические травмы.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Броски.&lt;/strong&gt; Пул D6 с перебросом: шестёрки дают успехи, единицы при
перебросе приносят мастеру очки Тьмы.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Публичная ссылка на лист&lt;/strong&gt; только для чтения, с отдельной настройкой для
заметок мастера. Ссылку можно отозвать.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Экспорт в JSON&lt;/strong&gt; листов, кораблей и истории.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Офлайн.&lt;/strong&gt; Чтение работает везде. Правки веб сохраняет в очередь
ServiceWorker, мобильное приложение — в кэш Hive, и обе очереди уходят на
сервер при появлении сети.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="для-мастера"&gt;Для мастера&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Листы кораблей&lt;/strong&gt;: 20 классов, модули, посты экипажа, корпус, энергия,
сигнатура, броня, неисправности.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Игровые сессии&lt;/strong&gt;: приглашение одной ссылкой, роли участников, архив и
трекер очков Тьмы с историей.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NPC и энкаунтеры&lt;/strong&gt;: готовые и свои NPC, конструктор энкаунтеров с
инициативой, раундами и ходами.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Общий стол в реальном времени.&lt;/strong&gt; Броски, очки Тьмы и чат расходятся по
участникам через Centrifugo. В чате есть сообщения на всю сессию, личный
шёпот, блокировка и системные сообщения.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="под-капотом"&gt;Под капотом&lt;/h2&gt;
&lt;p&gt;Три клиента на одном бэкенде: Nuxt-SPA, приложение на Flutter для iOS и
Android и бэкенд на NestJS с Postgres, Redis и Centrifugo. API описан в
OpenAPI и AsyncAPI, клиенты для TypeScript и Dart генерируются из
спецификации, CI падает при расхождении. Интерфейс переведён на английский,
русский, украинский и греческий, паритет ключей проверяет CI. Телеметрия идёт
через OpenTelemetry в Grafana, ошибки — в Sentry.&lt;/p&gt;</description></item><item><title>burkmak</title><link>https://kkucherenkov.github.io/projects/burkmak/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://kkucherenkov.github.io/projects/burkmak/</guid><description>&lt;h2 id="для-чего"&gt;Для чего&lt;/h2&gt;
&lt;p&gt;Статьи, отложенные на потом, теряются по вкладкам и закладкам. burkmak
собирает их в одну библиотеку на своём сервере, извлекает текст без рекламы и
отправляет на читалку Kobo без правки прошивки. Выделения и заметки уходят в
Obsidian.&lt;/p&gt;
&lt;h2 id="сохранение"&gt;Сохранение&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Откуда угодно.&lt;/strong&gt; Ссылка сохраняется из веба, мобильного приложения,
share-меню Android или букмарклета в браузере.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Метаданные в фоне.&lt;/strong&gt; Заголовок, сайт, описание и картинка появляются без
перезагрузки страницы, по SSE.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Текст сразу.&lt;/strong&gt; Статья извлекается при сохранении, и ридер, OPDS и Kobo
получают её готовой.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Порядок.&lt;/strong&gt; Теги, статусы (непрочитано, прочитано, в архиве) и избранное.
У каждого пользователя своя библиотека.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="чтение"&gt;Чтение&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ридер на Readability.&lt;/strong&gt; Статья без рекламы и обвеса. Копия остаётся, даже
если оригинал удалят.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Картинки&lt;/strong&gt; кэшируются на своём сервере.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Полнотекстовый поиск&lt;/strong&gt; по заголовку, адресу и тексту статьи на SQLite
FTS5.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Выделения&lt;/strong&gt; цветом и заметки к ним.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Светлая и тёмная тема.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="kobo"&gt;Kobo&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OPDS-каталог&lt;/strong&gt; с обложками, пагинацией и поиском. Статьи скачиваются в
EPUB и KEPUB, каталог добавляется на обычную Kobo через настройки.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Нативная синхронизация&lt;/strong&gt; по протоколу магазина Kobo. Читалка забирает
новые статьи при подключении, а статус прочтения возвращается обратно:
дочитанная на Kobo статья становится прочитанной в burkmak, а
архивированная в приложении исчезает с устройства. Нужен HTTPS. Протокол
проверен на полной симуляции, проверка на живом устройстве впереди.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="obsidian"&gt;Obsidian&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Экспорт в Markdown.&lt;/strong&gt; API отдаёт статью заметкой с источником,
метаданными, выделениями и заметками. Повторный экспорт обновляет ту же
заметку.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Плагин для Obsidian&lt;/strong&gt; синхронизирует экспорт в хранилище.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="под-капотом"&gt;Под капотом&lt;/h2&gt;
&lt;p&gt;Бэкенд на NestJS с SQLite, веб на Nuxt, мобильный клиент на Flutter. Фоновые
задачи выполняет воркер поверх базы, события в клиенты идут по SSE, внешних
сервисов нет: локальный стек — два контейнера. API описан в OpenAPI, клиенты
для TypeScript и Dart генерируются из спецификации.&lt;/p&gt;</description></item><item><title>todoer</title><link>https://kkucherenkov.github.io/projects/todoer/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://kkucherenkov.github.io/projects/todoer/</guid><description>&lt;h2 id="для-чего"&gt;Для чего&lt;/h2&gt;
&lt;p&gt;Личные задачи, проекты, приоритеты и теги в духе Todoist и Things, но на
своём сервере. Главное требование — офлайн: каждый клиент держит полную
реплику и отвечает на вопрос «что сегодня» без сети.&lt;/p&gt;
&lt;h2 id="что-работает"&gt;Что работает&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Синхронизация.&lt;/strong&gt; Один эндпоинт &lt;code&gt;POST /api/v1/sync&lt;/code&gt; принимает операции и
отдаёт изменения. Конфликты решаются по полям, побеждает последняя запись.
Удалённые записи чистятся, а клиент с устаревшим курсором получает свежий
снапшот.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CLI.&lt;/strong&gt; Команды &lt;code&gt;todoer add&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt; и &lt;code&gt;outbox&lt;/code&gt; работают с локальной
репликой в SQLite. Операция сначала встаёт в очередь и сохраняет свой id при
повторах, поэтому повторный &lt;code&gt;add&lt;/code&gt; не создаёт дубль.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Для скриптов и агентов.&lt;/strong&gt; Вывод в &lt;code&gt;--json&lt;/code&gt; и коды возврата, по которым
можно ветвиться: код 5 означает, что сервер недоступен и операция ждёт в
очереди.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="в-планах"&gt;В планах&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Проекты и теги из быстрого ввода, повторяющиеся задачи.&lt;/li&gt;
&lt;li&gt;Представления: список, канбан, календарь.&lt;/li&gt;
&lt;li&gt;Веб-клиент и приложение на Flutter.&lt;/li&gt;
&lt;li&gt;Трекер привычек во второй версии.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="под-капотом"&gt;Под капотом&lt;/h2&gt;
&lt;p&gt;NestJS 11, Prisma и Postgres 18. REST-CRUD нет: любая запись идёт через
протокол синхронизации. Контракт описан в OpenAPI, и
&lt;code&gt;express-openapi-validator&lt;/code&gt; проверяет его в обе стороны. Репозиторий создан из
шаблона &lt;a href="../shipyard/"&gt;project-skeleton&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Split Monkey</title><link>https://kkucherenkov.github.io/projects/split_monkey/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://kkucherenkov.github.io/projects/split_monkey/</guid><description>&lt;h2 id="для-чего"&gt;Для чего&lt;/h2&gt;
&lt;p&gt;Тренажёры вроде monkeytype меряют скорость и точность, но не знают, на какой
клавиатуре идёт набор. После переезда на split этого мало: нужно видеть
перекос между руками, медленные пальцы и цену работы со слоями. Split Monkey
знает физическую раскладку и keymap клавиатуры и считает статистику в этом
разрезе. Целевое железо — Lily58 с QMK, настроенной через Vial.&lt;/p&gt;
&lt;h2 id="тренажёр"&gt;Тренажёр&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Режимы как у monkeytype:&lt;/strong&gt; на время (15, 30, 60 секунд) и на число слов
(10, 25, 50), с пунктуацией или без.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Скорость, точность и ровность темпа&lt;/strong&gt; считаются так же, как в monkeytype,
и цифры можно сравнивать.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Клавиатура под текстом.&lt;/strong&gt; Под строкой нарисована Lily58 целиком: клавиша
следующего символа обведена, нажатая подсвечивается. Если символ живёт на
слое, подсвечиваются и активатор слоя, и сама клавиша.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Раскладка из Vial.&lt;/strong&gt; Keymap загружается файлом &lt;code&gt;.vil&lt;/code&gt;. Символы, которых
на клавиатуре нет, в текст не попадают.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="аналитика"&gt;Аналитика&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Баланс рук&lt;/strong&gt; и доля чередования.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SFB:&lt;/strong&gt; биграммы одним пальцем и их цена в миллисекундах.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Пальцы и клавиши:&lt;/strong&gt; нажатия, ошибки, медианная задержка.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Цена слоя:&lt;/strong&gt; насколько символы на слое медленнее базового.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;История забегов&lt;/strong&gt; с графиками динамики и профилем слабых мест по
последним забегам.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="как-считает"&gt;Как считает&lt;/h2&gt;
&lt;p&gt;Браузер видит только результат работы прошивки: слой и mod-tap разрешаются
внутри клавиатуры. Тренажёр восстанавливает физический путь каждого символа по
загруженному keymap, выбирает путь с меньшим числом нажатий и учитывает, что
заглавную букву левой руки набирают правым Shift. Разрез по слоям выводится из
этого пути, остальные метрики измеряются по нажатым клавишам. Задержки
агрегируются медианой, чтобы одна пауза на раздумье не искажала картину.&lt;/p&gt;</description></item><item><title>Shipyard</title><link>https://kkucherenkov.github.io/projects/shipyard/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://kkucherenkov.github.io/projects/shipyard/</guid><description>&lt;h2 id="для-чего"&gt;Для чего&lt;/h2&gt;
&lt;p&gt;Правила, по которым проект живёт с агентами, обычно копируются из репозитория
в репозиторий и расходятся. Shipyard выносит их в две части: плагин Claude
Code с навыками и шаблон репозитория с файлами процесса. Исправление в навыке
приходит во все проекты через &lt;code&gt;/plugin update&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="плагин-shipyard"&gt;Плагин shipyard&lt;/h2&gt;
&lt;p&gt;Шесть навыков, каждый срабатывает в свой момент:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;task-stack&lt;/strong&gt; ведёт стек задач: перед задачей крупнее однострочного фикса,
при блокировке и при выпуске.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;lane-discipline&lt;/strong&gt; разводит параллельных агентов, чтобы их работа не
пересекалась.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ci-gates&lt;/strong&gt; отличает зелёный PR от того, что только выглядит зелёным, и
находит обязательные проверки, которые не запустились.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;issue-bookkeeping&lt;/strong&gt; сверяет задачи в трекере с тем, что ушло в main.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;deploy-verify&lt;/strong&gt; выкатывает релиз на хост и проверяет, какая версия там
работает.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;release-audit&lt;/strong&gt; проводит аудит продукта перед релизом.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Команда &lt;code&gt;/bootstrap&lt;/code&gt; один раз настраивает свежий репозиторий из шаблона:
заполняет плейсхолдеры, пишет первый ADR и открывает первую задачу.&lt;/p&gt;</description></item></channel></rss>