<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Kirill's homepage</title><link>https://kkucherenkov.github.io/en/</link><description>Recent content on Kirill's homepage</description><generator>Hugo</generator><language>en</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/en/index.xml" rel="self" type="application/rss+xml"/><item><title>Course Shelf</title><link>https://kkucherenkov.github.io/en/projects/course_shelf/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://kkucherenkov.github.io/en/projects/course_shelf/</guid><description>&lt;h2 id="what-its-for"&gt;What it&amp;rsquo;s for&lt;/h2&gt;
&lt;p&gt;Courses from Udemy and Coursera, conference and workshop recordings often sit
on disk as plain folders. Course Shelf scans such a folder and builds a
catalogue of courses, sections and lessons. The catalogue opens in the browser
and in the mobile app. The files stay where they are: Course Shelf neither
copies nor transcodes them.&lt;/p&gt;
&lt;h2 id="library"&gt;Library&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Catalogue from the folder tree.&lt;/strong&gt; Folders and file names define courses,
sections and lesson order. The scan attaches subtitles and materials that sit
next to each video.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;course.json&lt;/code&gt; for fine control.&lt;/strong&gt; A file in the course folder sets titles,
lesson order and metadata on top of what the scan inferred.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Metadata from the web.&lt;/strong&gt; Scrapers fetch covers, descriptions, instructors
and tags from Udemy, Coursera and YouTube. Other sites work through
declarative scraper definitions and a generic schema.org and OpenGraph
parser.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Single-course rescan.&lt;/strong&gt; One course updates without a pass over the whole
library.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="watching"&gt;Watching&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Streaming without transcoding.&lt;/strong&gt; Video goes over HTTP with byte ranges, so
seeking responds at once.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Progress.&lt;/strong&gt; The player remembers the position, resumes from it and rolls
into the next lesson. Past 90% a lesson counts as complete.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Home&lt;/strong&gt; shows lessons in progress, recent additions, finished courses and
the week&amp;rsquo;s watch time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Browse with filters&lt;/strong&gt; by status, library, length and instructor. Filters
live in the URL, so a link reopens the same shelf.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Notes and bookmarks.&lt;/strong&gt; Each lesson takes a Markdown note and bookmarks at
any second. Notes are visible to their author only.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="transcripts-and-search"&gt;Transcripts and search&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Local transcription.&lt;/strong&gt; whisper.cpp transcribes lessons without subtitles on
the same server, with no cloud API.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Search&lt;/strong&gt; finds courses and individual lessons in one query and shows the
matching snippet. On desktop it opens as a command palette.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transcript next to the player.&lt;/strong&gt; A &lt;code&gt;?t=&lt;/code&gt; link opens a lesson at the right
second.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quizzes.&lt;/strong&gt; An LLM writes questions for a lesson: local llama.cpp or a model
through OpenRouter.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="mobile-app"&gt;Mobile app&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Flutter client&lt;/strong&gt; for iOS and Android with the same catalogue.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Downloads.&lt;/strong&gt; Lessons download with resume after a dropped connection and
stay encrypted on the device.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Offline.&lt;/strong&gt; Position, notes and bookmarks save on the device and sync when
the connection returns. If the server holds newer progress, the server&amp;rsquo;s
version stands.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="multiple-users"&gt;Multiple users&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Explicit access.&lt;/strong&gt; USER and ADMIN roles, with access granted per library or
per course.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Administration.&lt;/strong&gt; Instance overview, user management, and signing in as a
user to see their catalogue.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backups.&lt;/strong&gt; One button runs &lt;code&gt;pg_dump&lt;/code&gt; and returns a link to the archive.
Media files stay out of it: Course Shelf doesn&amp;rsquo;t own them.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="under-the-hood"&gt;Under the hood&lt;/h2&gt;
&lt;p&gt;A monorepo of three apps: a NestJS backend, a Nuxt web client and a Flutter
client. The API is described once, in OpenAPI 3.1 and AsyncAPI 3.0, and the
TypeScript and Dart clients are generated from it. The backend rejects requests
the spec doesn&amp;rsquo;t describe, and CI fails when code drifts from the contract.
Scan and job events reach the clients through Centrifugo. The interface ships
in English and Russian.&lt;/p&gt;</description></item><item><title>Coriolis Companion</title><link>https://kkucherenkov.github.io/en/projects/coriolis/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://kkucherenkov.github.io/en/projects/coriolis/</guid><description>&lt;h2 id="what-its-for"&gt;What it&amp;rsquo;s for&lt;/h2&gt;
&lt;p&gt;Coriolis: The Third Horizon is a science-fiction RPG from Free League with an
Arabian Nights flavour. The app replaces paper sheets and keeps the table in
one shared state: every member of a session sees the same rolls, Darkness
Points and chat. A fan project, not affiliated with the publisher.&lt;/p&gt;
&lt;h2 id="for-players"&gt;For players&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Character sheet&lt;/strong&gt; true to the Coriolis rules: icons, concept, upbringing,
attributes from 1 to 5, general and advanced skills, talents, gear,
conditions, encumbrance, critical injuries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dice.&lt;/strong&gt; A D6 pool with pushed rolls: sixes are successes, and ones on a
push hand the GM Darkness Points.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Public share link&lt;/strong&gt; to a read-only sheet, with a separate toggle for GM
notes. The link can be revoked.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JSON export&lt;/strong&gt; of sheets, ships and history.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Offline.&lt;/strong&gt; Reading works everywhere. The web client queues edits in a
ServiceWorker, the mobile app in a Hive cache, and both queues sync when the
connection returns.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="for-game-masters"&gt;For game masters&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ship sheets&lt;/strong&gt;: 20 ship classes, modules, crew positions, hull, energy,
signature, armor, problems.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Game sessions&lt;/strong&gt;: a single invite link, member roles, an archive and a
Darkness Point tracker with history.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NPCs and encounters&lt;/strong&gt;: pre-generated and custom NPCs, an encounter builder
with initiative, rounds and turns.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A real-time table.&lt;/strong&gt; Rolls, Darkness Points and chat reach every member
through Centrifugo. Chat covers session-wide messages, private whispers,
blocking and system messages.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="under-the-hood"&gt;Under the hood&lt;/h2&gt;
&lt;p&gt;Three clients on one backend: a Nuxt SPA, a Flutter app for iOS and Android
and a NestJS backend with Postgres, Redis and Centrifugo. The API is described
in OpenAPI and AsyncAPI, the TypeScript and Dart clients are generated from the
spec, and CI fails on drift. The interface ships in English, Russian, Ukrainian
and Greek, with key parity checked in CI. Telemetry flows through
OpenTelemetry into Grafana, errors into Sentry.&lt;/p&gt;</description></item><item><title>burkmak</title><link>https://kkucherenkov.github.io/en/projects/burkmak/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://kkucherenkov.github.io/en/projects/burkmak/</guid><description>&lt;h2 id="what-its-for"&gt;What it&amp;rsquo;s for&lt;/h2&gt;
&lt;p&gt;Articles saved for later get lost across tabs and bookmarks. burkmak gathers
them into one library on your own server, extracts the text without ads and
sends it to a Kobo e-reader with no firmware hacks. Highlights and notes go to
Obsidian.&lt;/p&gt;
&lt;h2 id="saving"&gt;Saving&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;From anywhere.&lt;/strong&gt; Save a link from the web app, the mobile app, the Android
share sheet or a browser bookmarklet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Metadata in the background.&lt;/strong&gt; Title, site, excerpt and lead image appear
without a page reload, over SSE.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Text right away.&lt;/strong&gt; Extraction runs on save, so the reader, OPDS and Kobo
get the article ready to read.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Organising.&lt;/strong&gt; Tags, read states (unread, read, archived) and favourites.
Every user has a separate library.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="reading"&gt;Reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Readability-based reader.&lt;/strong&gt; The article without ads or clutter. The copy
stays even if the original disappears.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Images&lt;/strong&gt; are cached on your own server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full-text search&lt;/strong&gt; across title, URL and article body on SQLite FTS5.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Highlights&lt;/strong&gt; in colour, with notes attached.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Light and dark themes.&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 catalogue&lt;/strong&gt; with covers, pagination and search. Articles download as
EPUB and KEPUB, and a stock Kobo adds the catalogue from its settings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Native sync&lt;/strong&gt; over the Kobo store protocol. The e-reader pulls new articles
when it connects, and reading state flows back: an article finished on the
Kobo turns read in burkmak, and one archived in the app leaves the device.
HTTPS is required. The protocol passes a full simulation; the pass on a
physical device is still ahead.&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 export.&lt;/strong&gt; The API returns an article as a note with its source,
metadata, highlights and notes. A repeat export updates the same note.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Obsidian plugin&lt;/strong&gt; syncs the export into your vault.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="under-the-hood"&gt;Under the hood&lt;/h2&gt;
&lt;p&gt;A NestJS backend on SQLite, a Nuxt web client and a Flutter mobile client. A
worker on top of the database runs background jobs and events reach clients
over SSE, so there are no external services: the local stack is two
containers. The API is described in OpenAPI, and the TypeScript and Dart
clients are generated from the spec.&lt;/p&gt;</description></item><item><title>todoer</title><link>https://kkucherenkov.github.io/en/projects/todoer/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://kkucherenkov.github.io/en/projects/todoer/</guid><description>&lt;h2 id="what-its-for"&gt;What it&amp;rsquo;s for&lt;/h2&gt;
&lt;p&gt;Personal tasks, projects, priorities and tags in the spirit of Todoist and
Things, on your own server. The defining requirement is offline: every client
holds a full replica and answers &amp;ldquo;what&amp;rsquo;s on today&amp;rdquo; without a network.&lt;/p&gt;
&lt;h2 id="what-works"&gt;What works&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sync.&lt;/strong&gt; One endpoint, &lt;code&gt;POST /api/v1/sync&lt;/code&gt;, takes operations and returns
changes. Conflicts resolve per field, last write wins. Tombstones get pruned,
and a client with a stale cursor receives a fresh snapshot.&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; and &lt;code&gt;outbox&lt;/code&gt; work against a local SQLite
replica. Each operation enters a queue first and keeps its id across retries,
so running &lt;code&gt;add&lt;/code&gt; twice doesn&amp;rsquo;t create a duplicate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;For scripts and agents.&lt;/strong&gt; &lt;code&gt;--json&lt;/code&gt; output and exit codes you can branch
on: exit code 5 means the server was unreachable and the operation waits in
the queue.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="planned"&gt;Planned&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Projects and tags from quick-add, recurring tasks.&lt;/li&gt;
&lt;li&gt;Views: list, kanban, calendar.&lt;/li&gt;
&lt;li&gt;The web client and the Flutter app.&lt;/li&gt;
&lt;li&gt;A habit tracker in version two.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="under-the-hood"&gt;Under the hood&lt;/h2&gt;
&lt;p&gt;NestJS 11, Prisma and Postgres 18. There is no REST CRUD: every write goes
through the sync protocol. The contract lives in OpenAPI, and
&lt;code&gt;express-openapi-validator&lt;/code&gt; enforces it in both directions. The repository
started from the &lt;a href="../shipyard/"&gt;project-skeleton&lt;/a&gt; template.&lt;/p&gt;</description></item><item><title>Split Monkey</title><link>https://kkucherenkov.github.io/en/projects/split_monkey/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://kkucherenkov.github.io/en/projects/split_monkey/</guid><description>&lt;h2 id="what-its-for"&gt;What it&amp;rsquo;s for&lt;/h2&gt;
&lt;p&gt;Trainers like monkeytype measure speed and accuracy, but they don&amp;rsquo;t know which
keyboard you type on. After a move to a split board that isn&amp;rsquo;t enough: you
need to see the imbalance between hands, the slow fingers and what layers cost
you. Split Monkey knows the physical layout and the keymap of the keyboard and
breaks the stats down along them. The target hardware is a Lily58 running QMK
configured through Vial.&lt;/p&gt;</description></item><item><title>Shipyard</title><link>https://kkucherenkov.github.io/en/projects/shipyard/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://kkucherenkov.github.io/en/projects/shipyard/</guid><description>&lt;h2 id="what-its-for"&gt;What it&amp;rsquo;s for&lt;/h2&gt;
&lt;p&gt;The rules a project follows when agents write code tend to get copied from
repository to repository and drift apart. Shipyard splits them into two
pieces: a Claude Code plugin with skills and a repository template with the
process files. A fix to a skill reaches every project through
&lt;code&gt;/plugin update&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="the-shipyard-plugin"&gt;The shipyard plugin&lt;/h2&gt;
&lt;p&gt;Six skills, each firing at its own moment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;task-stack&lt;/strong&gt; keeps the task stack: before any task bigger than a one-line
fix, when a task blocks and when one ships.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;lane-discipline&lt;/strong&gt; keeps parallel agents from colliding.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ci-gates&lt;/strong&gt; tells a green pull request from one that only looks green and
finds required checks that never ran.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;issue-bookkeeping&lt;/strong&gt; reconciles the issue tracker with what reached main.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;deploy-verify&lt;/strong&gt; puts a release on a host and checks which version the host
runs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;release-audit&lt;/strong&gt; audits the product before a release.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;/bootstrap&lt;/code&gt; command sets up a fresh repository from the template once:
it fills the placeholders, writes the first ADR and opens the first task.&lt;/p&gt;</description></item></channel></rss>