<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Denis Evsyukov</title><link>https://www.juev.org/</link><description>Recent content on Denis Evsyukov</description><generator>Hugo -- gohugo.io</generator><language>ru-RU</language><managingEditor>denis@evsyukov.org (Denis Evsyukov)</managingEditor><webMaster>denis@evsyukov.org (Denis Evsyukov)</webMaster><copyright>This work is licensed under a MIT License.</copyright><lastBuildDate>Sun, 11 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://www.juev.org/atom.xml" rel="self" type="application/rss+xml"/><atom:link href="https://pubsubhubbub.appspot.com/" rel="hub"/><item><title>Lists: список дел, который хранится у вас</title><link>https://www.juev.org/2026/10/11/lists/</link><pubDate>Sun, 11 Oct 2026 16:05:20 +0300</pubDate><author>denis@evsyukov.org (Denis Evsyukov)</author><guid>https://www.juev.org/2026/10/11/lists/</guid><description>&lt;p&gt;Пока все мои устройства были от Apple, список дел я вёл в &lt;a href="https://culturedcode.com/things/"&gt;Things&lt;/a&gt;
. Это красивое и удобное приложение, и менять его я не собирался.&lt;/p&gt;
&lt;p&gt;Потом телефон сменился на Android, а приложения для Android у Things нет. Я перепробовал замены: TickTick, 2Do и многие другие. Ни одна не прижилась.&lt;/p&gt;
&lt;p&gt;В итоге я сделал свой список дел: с помощью Claude написал &lt;a href="https://github.com/juev/lists"&gt;Lists&lt;/a&gt;
. Возможности взяты у 2Do, внешний вид у Things. Данные лежат на устройстве, а синхронизируются через хранилище, которое у вас уже есть: каталог WebDAV или сервер CalDAV.&lt;/p&gt;
&lt;p&gt;&lt;figure class="image-figure"&gt;&lt;img src="https://static.juev.org/2026/10/lists-macos-today.png"
alt="Представление «Сегодня» в Lists на macOS"
loading="lazy"
decoding="async"&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id="что-получилось"&gt;&lt;a href="#%d1%87%d1%82%d0%be-%d0%bf%d0%be%d0%bb%d1%83%d1%87%d0%b8%d0%bb%d0%be%d1%81%d1%8c" class="post_hash"&gt;Что получилось&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Lists состоит из трёх частей: приложение для macOS, приложение для Android и веб-интерфейс, который ставится на свой сервер. Все три работают с одними данными и умеют одно и то же; где платформа отличается, это описано в руководстве&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Учётной записи нет, и подписываться не на что. Приложение полностью работает без сети и без настроенной синхронизации: правка сразу записывается на устройстве, а до остальных доходит, когда появится связь.&lt;/p&gt;
&lt;p&gt;Интерфейс английский и русский, язык берётся из системы. Оформление светлое и тёмное, по умолчанию как в системе.&lt;/p&gt;
&lt;h2 id="задачи"&gt;&lt;a href="#%d0%b7%d0%b0%d0%b4%d0%b0%d1%87%d0%b8" class="post_hash"&gt;Задачи&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Обязательно у задачи только название. Остальное добавляется по мере надобности:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;заметка в Markdown;&lt;/li&gt;
&lt;li&gt;дата начала и срок, день или день со временем;&lt;/li&gt;
&lt;li&gt;приоритет и теги;&lt;/li&gt;
&lt;li&gt;повтор и напоминание;&lt;/li&gt;
&lt;li&gt;вложения и подзадачи.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Даты хранятся без часового пояса: «завтра в 9:00» останется девятью утра и после перелёта. Задача с датой начала в будущем не показывается в «Сегодня», пока этот день не наступит.&lt;/p&gt;
&lt;p&gt;&lt;figure class="image-figure"&gt;&lt;img src="https://static.juev.org/2026/10/lists-macos-task.png"
alt="Список с открытой карточкой задачи: заметка, срок, приоритет, тег, файл и подзадачи"
loading="lazy"
decoding="async"&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Заметки.&lt;/strong&gt; Markdown оформляется прямо при наборе, отдельного режима просмотра нет. Знаки разметки скрыты везде, кроме абзаца, в котором стоит курсор. Чекбоксы &lt;code&gt;- [ ]&lt;/code&gt; переключаются щелчком, таблицы рисуются сеткой, ссылки открываются. Хранится заметка так, как набрана, поэтому поиск и CalDAV видят обычный текст.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Вложения.&lt;/strong&gt; К задаче прикладывается любое число файлов. Скачанный файл открывается без сети. Изображения и PDF показываются внутри приложения, остальное открывается в программе по умолчанию. Файл можно сохранить наружу или передать в другое приложение.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Подзадачи и проекты.&lt;/strong&gt; Подзадача — это строка с отметкой и названием, своих дат и заметок у неё нет. Когда нужно больше, задачу можно сделать проектом. Проект появляется в боковой панели со счётчиком «сделано/всего», а его задачи полноценные, со всеми полями и своими подзадачами.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Повторы.&lt;/strong&gt; Готовые варианты: каждый день, по будням, раз в неделю, раз в две недели, раз в месяц, раз в квартал, раз в год. Своё правило повторяет задачу каждые N дней, недель, месяцев или лет, по выбранным дням недели или, например, в последнюю пятницу месяца. Отсчёт идёт от срока или от дня, когда задача выполнена. Правило заканчивается после N повторов, в назначенный день или не заканчивается вовсе.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;«Не буду делать».&lt;/strong&gt; Задачу, которую делать уже не нужно, а удалять жалко, можно закрыть вторым исходом. Она уходит из работы, остаётся в истории и не попадает в счётчики проекта.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Отмена.&lt;/strong&gt; Выполненная задача не исчезает сразу: несколько секунд она стоит на месте зачёркнутой, и её можно вернуть. Удаление отменяется, удалённое лежит в корзине, пока её не очистят вручную.&lt;/p&gt;
&lt;p&gt;Кроме списков, есть постоянные представления: «Входящие», «Сегодня», «Предстоящие», «Все», «Выполненные», «Не буду делать» и «Корзина». Сохранённые фильтры собирают задачи из разных списков по сроку, спискам, тегам, приоритету, состоянию и словам в тексте. Поиск идёт по названиям и заметкам во всех списках.&lt;/p&gt;
&lt;h2 id="быстрый-ввод"&gt;&lt;a href="#%d0%b1%d1%8b%d1%81%d1%82%d1%80%d1%8b%d0%b9-%d0%b2%d0%b2%d0%be%d0%b4" class="post_hash"&gt;Быстрый ввод&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Задача должна записываться быстрее, чем забывается. Для этого хватает одной строки:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;купить молоко завтра 18:30 !! #дом @Покупки
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Получится задача «купить молоко» со сроком на завтра в 18:30, средним приоритетом и тегом «дом» в списке «Покупки». Даты понимаются на русском и английском. То, что приложение распознало, оно показывает плашками до сохранения, а распознавание можно выключить.&lt;/p&gt;
&lt;p&gt;Мест, откуда задачу можно добавить, много:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;macOS&lt;/strong&gt;: &lt;code&gt;⌘N&lt;/code&gt; в основном окне, окно поверх любой программы по &lt;code&gt;⌃⌥Пробел&lt;/code&gt;, значок в строке меню со списком задач на сегодня, «Поделиться», меню «Службы» и адрес &lt;code&gt;lists://add?text=…&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Android&lt;/strong&gt;: кнопка «+», «Поделиться», меню выделенного текста, ярлык на значке приложения, плитка быстрых настроек и виджеты;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;веб&lt;/strong&gt;: поле «Новая задача» в списке.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class="image-figure"&gt;&lt;img src="https://static.juev.org/2026/10/lists-macos-quick-entry.png"
alt="Окно быстрого ввода на macOS: заметка начата с текста из буфера обмена"
loading="lazy"
decoding="async"&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Окно быстрого ввода начинает заметку с содержимого буфера обмена. Скопировали ссылку, нажали клавишу, набрали название — ссылка уже в заметке. Каждая копия используется один раз, а то, что менеджер паролей пометил скрытым, приложение не читает.&lt;/p&gt;
&lt;h2 id="android"&gt;&lt;a href="#android" class="post_hash"&gt;Android&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;На телефоне Lists выглядит так же, как на Mac, насколько это позволяет экран: крупное название представления, задача в одну строку, карточка задачи выезжает снизу.&lt;/p&gt;
&lt;div class="shareding-gallery"&gt;
&lt;a href="https://static.juev.org/2026/10/lists-android-today.png"&gt;&lt;img src="https://static.juev.org/2026/10/lists-android-today.png" alt="Представление «Сегодня» в Lists на Android" width="720" height="1600" loading="lazy"&gt;&lt;span&gt;Сегодня&lt;/span&gt;&lt;/a&gt;
&lt;a href="https://static.juev.org/2026/10/lists-android-task.png"&gt;&lt;img src="https://static.juev.org/2026/10/lists-android-task.png" alt="Карточка задачи в Lists на Android" width="720" height="1600" loading="lazy"&gt;&lt;span&gt;Задача&lt;/span&gt;&lt;/a&gt;
&lt;a href="https://static.juev.org/2026/10/lists-android-quick-entry.png"&gt;&lt;img src="https://static.juev.org/2026/10/lists-android-quick-entry.png" alt="Быстрый ввод в Lists на Android: срок и приоритет прочитаны из строки" width="720" height="1600" loading="lazy"&gt;&lt;span&gt;Быстрый ввод&lt;/span&gt;&lt;/a&gt;
&lt;a href="https://static.juev.org/2026/10/lists-android-list.png"&gt;&lt;img src="https://static.juev.org/2026/10/lists-android-list.png" alt="Список в Lists на Android" width="720" height="1600" loading="lazy"&gt;&lt;span&gt;Список&lt;/span&gt;&lt;/a&gt;
&lt;a href="https://static.juev.org/2026/10/lists-android-drawer.png"&gt;&lt;img src="https://static.juev.org/2026/10/lists-android-drawer.png" alt="Панель с представлениями, списками, проектами и фильтрами в Lists на Android" width="720" height="1600" loading="lazy"&gt;&lt;span&gt;Панель списков&lt;/span&gt;&lt;/a&gt;
&lt;a href="https://static.juev.org/2026/10/lists-android-settings.png"&gt;&lt;img src="https://static.juev.org/2026/10/lists-android-settings.png" alt="Настройки Lists на Android" width="720" height="1600" loading="lazy"&gt;&lt;span&gt;Настройки&lt;/span&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;p&gt;Смахивание вправо выполняет задачу, влево открывает выбор срока. Долгое нажатие открывает меню, а из него включается выбор нескольких задач: их можно разом выполнить, перенести или удалить, и всё действие отменяется одной кнопкой.&lt;/p&gt;
&lt;p&gt;На главный экран ставятся два виджета, 2×2 и 4×4. Каждый показывает «Сегодня», «Входящие» или выбранный список, и задачу можно отметить прямо в нём. В шторке можно держать постоянное уведомление с задачами на сегодня и кнопкой «Новая задача».&lt;/p&gt;
&lt;p&gt;Напоминания настраиваются на каждом устройстве отдельно: за сколько предупреждать о сроке, в котором часу напоминать о задачах на день, присылать ли сводку дня. И на Android, и на macOS в «Сегодня» можно показать события дня из календарей устройства. Они только читаются и никуда не отправляются.&lt;/p&gt;
&lt;h2 id="синхронизация"&gt;&lt;a href="#%d1%81%d0%b8%d0%bd%d1%85%d1%80%d0%be%d0%bd%d0%b8%d0%b7%d0%b0%d1%86%d0%b8%d1%8f" class="post_hash"&gt;Синхронизация&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Это главная часть приложения. Своего сервера у Lists нет. Устройства обмениваются изменениями через хранилище, от которого нужно только читать, писать, перечислять и удалять файлы. Хранилищ три вида:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Вид&lt;/th&gt;
&lt;th&gt;Что лежит в хранилище&lt;/th&gt;
&lt;th&gt;Когда выбирать&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WebDAV&lt;/td&gt;
&lt;td&gt;журнал изменений приложения, передаётся всё&lt;/td&gt;
&lt;td&gt;любой сервер WebDAV, в том числе Nextcloud&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Папка&lt;/td&gt;
&lt;td&gt;то же самое&lt;/td&gt;
&lt;td&gt;каталог, который синхронизирует другая программа: Syncthing, iCloud Drive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CalDAV&lt;/td&gt;
&lt;td&gt;по одному VTODO на задачу, календарь на список&lt;/td&gt;
&lt;td&gt;задачи должны быть видны и в других клиентах CalDAV&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Папку умеют приложение для macOS и веб-сервер, на Android этого вида нет.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Как сливаются правки.&lt;/strong&gt; Каждое поле каждой задачи хранится отдельно и несёт метку времени. Если на телефоне изменили срок, а на компьютере в это время дописали заметку, сохранятся обе правки. Если на двух устройствах изменили одно и то же поле, побеждает более поздняя правка. Итог не зависит от того, в каком порядке изменения дошли: два устройства с одинаковым набором изменений показывают одно и то же&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Что лежит в хранилище.&lt;/strong&gt; Каждое устройство пишет туда файлы только под своим именем и чужие не трогает. Поэтому двум устройствам нечего делить на сервере, и блокировки не нужны. Вложения лежат отдельно, под именем хеша содержимого. Задача доходит до других устройств раньше своих файлов: синхронизация сначала передаёт поля, показывает результат и только потом берётся за вложения.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CalDAV.&lt;/strong&gt; Здесь списки становятся календарями, а задачи обычными VTODO, так что их видят Tasks.org, DAVx5 и другие клиенты. Всё, чего в стандарте нет, Lists кладёт в собственное свойство рядом со стандартными. Правки, сделанные в других клиентах, подхватываются, а их свойства сохраняются. Ограничения такие: общие настройки через CalDAV не передаются, вложения ограничены 5 МБ на файл и 20 МБ на задачу. Сервер, который вырезает неизвестные свойства, оставит от задачи только стандартные поля&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Когда запускается.&lt;/strong&gt; После правки на устройстве синхронизация идёт через две секунды тишины. Пока приложение открыто, она повторяется раз в минуту и ещё раз при возврате в приложение. На Android запуск после правки отдан системе, поэтому изменение уйдёт, даже если приложение сразу закрыли, а без сети дождётся её появления. Там же синхронизация идёт в фоне раз в 15 минут. Сбой сети ничего не теряет, ошибка синхронизации работе не мешает.&lt;/p&gt;
&lt;p&gt;Если подключить к непустому хранилищу устройство, на котором уже есть задачи, приложение спросит, что оставить: слить обе стороны, взять данные хранилища или заменить хранилище данными устройства.&lt;/p&gt;
&lt;h2 id="быстрая-доставка"&gt;&lt;a href="#%d0%b1%d1%8b%d1%81%d1%82%d1%80%d0%b0%d1%8f-%d0%b4%d0%be%d1%81%d1%82%d0%b0%d0%b2%d0%ba%d0%b0" class="post_hash"&gt;Быстрая доставка&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;У файлового хранилища есть слабое место: оно не умеет сообщать, что в нём что-то изменилось. Задача, добавленная на компьютере, появится в открытом приложении на телефоне в течение минуты, а в закрытом — когда система запустит фоновую синхронизацию.&lt;/p&gt;
&lt;p&gt;Lists сокращает это ожидание уведомлениями. Устройство, которое выгрузило изменения, просит остальные синхронизироваться, и они делают это за несколько секунд. В проекте такой запрос называется толчком.&lt;/p&gt;
&lt;p&gt;Данных толчок не несёт. В нём одно слово, &lt;code&gt;sync&lt;/code&gt;, а задачи по-прежнему идут через ваше хранилище.&lt;/p&gt;
&lt;p&gt;Настраивается это так:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;macOS и веб-сервер&lt;/strong&gt; получают толчки через сервер &lt;a href="https://ntfy.sh"&gt;ntfy&lt;/a&gt;
, общий &lt;code&gt;https://ntfy.sh&lt;/code&gt; или свой. Адрес задаётся в настройках синхронизации, у веб-сервера — в переменной &lt;code&gt;LISTS_PUSH_SERVER&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Android&lt;/strong&gt; получает толчки через &lt;a href="https://unifiedpush.org"&gt;UnifiedPush&lt;/a&gt;
. На телефон ставится приложение-распространитель, например ntfy, и в настройках синхронизации Lists включается «Синхронизировать сразу после правки на другом устройстве». Работает это и при закрытом приложении.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Об адресах устройства договариваются сами, через то же хранилище: каждое публикует там, куда ему слать толчок. Mac и веб-сервер, которые пользуются одним сервером ntfy, слушают одну тему со случайным именем из 32 знаков, и один запрос будит всех. Телефон остаётся при адресе, который выдал распространитель.&lt;/p&gt;
&lt;p&gt;На сервере без входа имя темы — единственная защита. Сервер ntfy при этом видит, когда вы что-то правили, но не видит, что именно. Для своего сервера ntfy с закрытым доступом Lists принимает токен; он хранится в связке ключей macOS или шифруется ключом из Android Keystore.&lt;/p&gt;
&lt;p&gt;Толчок необязателен. Запросы уходят уже после синхронизации и её не задерживают, а потерянный толчок означает только то, что изменение придёт по расписанию. При проверке на эмуляторе задача, добавленная на другом устройстве, дошла до телефона с убитым процессом приложения примерно за три секунды&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Телефон, который ограничивает фоновую работу, такие запуски откладывает. В настройках синхронизации Lists показывает, ограничено ли приложение, и предлагает снять ограничение батареи; то же стоит сделать для приложения ntfy.&lt;/p&gt;
&lt;h2 id="веб-интерфейс"&gt;&lt;a href="#%d0%b2%d0%b5%d0%b1-%d0%b8%d0%bd%d1%82%d0%b5%d1%80%d1%84%d0%b5%d0%b9%d1%81" class="post_hash"&gt;Веб-интерфейс&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Приложения для iOS нет, и вместо него сделан веб-интерфейс. Чисто браузерным он быть не может: серверы WebDAV и CalDAV не разрешают запросы с чужих страниц. Поэтому вместе с ним идёт небольшой сервер &lt;code&gt;lists-web&lt;/code&gt;. Для остальных это ещё одно устройство: он держит свою копию данных и синхронизируется наравне с приложениями.&lt;/p&gt;
&lt;p&gt;&lt;figure class="image-figure"&gt;&lt;img src="https://static.juev.org/2026/10/lists-web-desktop.png"
alt="Веб-интерфейс Lists: список с открытой карточкой задачи"
loading="lazy"
decoding="async"&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div class="shareding-gallery"&gt;
&lt;a href="https://static.juev.org/2026/10/lists-web-phone.png"&gt;&lt;img src="https://static.juev.org/2026/10/lists-web-phone.png" alt="Веб-интерфейс Lists на телефоне: представление «Сегодня»" width="390" height="844" loading="lazy"&gt;&lt;span&gt;Сегодня&lt;/span&gt;&lt;/a&gt;
&lt;a href="https://static.juev.org/2026/10/lists-web-phone-task.png"&gt;&lt;img src="https://static.juev.org/2026/10/lists-web-phone-task.png" alt="Веб-интерфейс Lists на телефоне: карточка задачи" width="390" height="844" loading="lazy"&gt;&lt;span&gt;Задача&lt;/span&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class="image-figure"&gt;&lt;img src="https://static.juev.org/2026/10/lists-web-filter.png"
alt="Сохранённый фильтр в веб-интерфейсе Lists"
loading="lazy"
decoding="async"&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Запускается он одной командой:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker run -d --name lists &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -p 8080:8080 &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -v lists-data:/data &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -e &lt;span class="nv"&gt;LISTS_WEB_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;придумайте пароль&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ghcr.io/juev/lists:0.2.0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Образ собран для amd64 и arm64. Вход по паролю или через OpenID Connect, синхронизация задаётся переменными окружения. Страница рассчитана и на телефон, её можно добавить на экран «Домой». Сервер сделан для одного человека: все, кто вошёл, видят одни и те же данные&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h2 id="чем-lists-отличается"&gt;&lt;a href="#%d1%87%d0%b5%d0%bc-lists-%d0%be%d1%82%d0%bb%d0%b8%d1%87%d0%b0%d0%b5%d1%82%d1%81%d1%8f" class="post_hash"&gt;Чем Lists отличается&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Данные у вас.&lt;/strong&gt; Они лежат на устройстве и в хранилище, которое вы выбрали сами. Учётной записи нет, чужого сервера с вашими задачами тоже. Если проект завтра остановится, приложение продолжит работать, а с CalDAV задачи останутся ещё и в стандартном формате.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Работает без сети.&lt;/strong&gt; Сеть нужна только для обмена изменениями. Самолёт, метро и упавший сервер ничего не меняют.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Одна логика на всех платформах.&lt;/strong&gt; Хранение, слияние правок, синхронизация, повторы и разбор строки быстрого ввода написаны один раз, на Rust, и подключены к приложениям на Swift и Kotlin. Два независимо написанных слияния рано или поздно разошлись бы в мелочи. Интерфейс при этом у каждой платформы свой: SwiftUI на macOS, Jetpack Compose на Android&lt;sup id="fnref:6"&gt;&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref"&gt;6&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Правки не теряются.&lt;/strong&gt; Слияние по полям избавляет от файлов вида «конфликтная копия» и от вопросов, какую версию оставить.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Переезд несложный.&lt;/strong&gt; Lists читает резервную копию 2Do, CSV из Todoist, доску Trello в JSON и списки Microsoft To Do. Повторный импорт того же файла обновляет уже перенесённое.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Резервные копии.&lt;/strong&gt; Приложения для macOS и Android раз в сутки сохраняют копию данных на устройстве и хранят последние пять. Копию можно восстановить, сохранить в другое место или перенести на другое устройство. Перед обновлением, которое меняет формат хранения, копия делается сама.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Код открыт.&lt;/strong&gt; Лицензия MIT, спецификации лежат в репозитории рядом с кодом.&lt;/p&gt;
&lt;h2 id="что-ещё-не-сделано"&gt;&lt;a href="#%d1%87%d1%82%d0%be-%d0%b5%d1%89%d1%91-%d0%bd%d0%b5-%d1%81%d0%b4%d0%b5%d0%bb%d0%b0%d0%bd%d0%be" class="post_hash"&gt;Что ещё не сделано&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Проекту неделя. Первый коммит сделан 5 октября, с тех пор вышло тринадцать версий, считая предварительные; последняя стабильная 0.2.0, готовится 0.3.0. Работа продолжается, и вот чего пока нет:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;приложения для iOS; телефон от Apple обслуживает веб-интерфейс;&lt;/li&gt;
&lt;li&gt;шифрования данных в хранилище: приложение кладёт их туда открыто;&lt;/li&gt;
&lt;li&gt;перетаскивания задач и списков на Android, и дата окончания повтора там не задаётся;&lt;/li&gt;
&lt;li&gt;своих правил повтора и уведомлений в веб-интерфейсе;&lt;/li&gt;
&lt;li&gt;сборок под Intel для macOS и под что-либо, кроме arm64, для Android.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Часть сделанного проверена тестами и на эмуляторе, но не на разных настоящих устройствах и серверах. Что именно и чем проверено, записано в конце спецификации&lt;sup id="fnref:7"&gt;&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref"&gt;7&lt;/a&gt;&lt;/sup&gt;. Ошибки тоже есть: найденные записаны в задачах репозитория.&lt;/p&gt;
&lt;h2 id="как-попробовать"&gt;&lt;a href="#%d0%ba%d0%b0%d0%ba-%d0%bf%d0%be%d0%bf%d1%80%d0%be%d0%b1%d0%be%d0%b2%d0%b0%d1%82%d1%8c" class="post_hash"&gt;Как попробовать&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Сборки лежат на &lt;a href="https://github.com/juev/lists/releases"&gt;странице релизов&lt;/a&gt;
:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;macOS 14 или новее на Apple silicon: образ диска &lt;code&gt;.dmg&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Android 8.0 или новее на arm64: файл &lt;code&gt;.apk&lt;/code&gt;, подписанный одним и тем же ключом от версии к версии;&lt;/li&gt;
&lt;li&gt;веб-сервер: образ &lt;code&gt;ghcr.io/juev/lists&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Начать можно без синхронизации, она подключается позже. Если уже есть список дел в другом приложении, его можно импортировать.&lt;/p&gt;
&lt;p&gt;Попробуйте. Замечания, ошибки и пожелания оставляйте в &lt;a href="https://github.com/juev/lists/issues"&gt;задачах репозитория&lt;/a&gt;
: разбираю я их именно там.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;&lt;a href="https://github.com/juev/lists/blob/main/docs/usage.md"&gt;docs/usage.md&lt;/a&gt;
— руководство по приложениям и веб-интерфейсу.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;&lt;a href="https://github.com/juev/lists/blob/main/docs/specs/sync.md"&gt;docs/specs/sync.md&lt;/a&gt;
— модель данных, правила слияния и формат хранилища.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;&lt;a href="https://github.com/juev/lists/blob/main/docs/specs/caldav.md"&gt;docs/specs/caldav.md&lt;/a&gt;
— что передаётся через CalDAV и что нет.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;Раздел «Толчок между устройствами» в &lt;a href="https://github.com/juev/lists/blob/main/docs/specs/sync.md"&gt;docs/specs/sync.md&lt;/a&gt;
: проверка от 6 октября на эмуляторе с приложением ntfy 1.25.2.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:5"&gt;
&lt;p&gt;&lt;a href="https://github.com/juev/lists/blob/main/docs/self-hosting.md"&gt;docs/self-hosting.md&lt;/a&gt;
— Docker, настройки, вход и обратный прокси.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:6"&gt;
&lt;p&gt;&lt;a href="https://github.com/juev/lists/blob/main/docs/architecture.md"&gt;docs/architecture.md&lt;/a&gt;
— принятые решения и отклонённые варианты.&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:7"&gt;
&lt;p&gt;Раздел «Что реализовано» в &lt;a href="https://github.com/juev/lists/blob/main/docs/specs/product.md"&gt;docs/specs/product.md&lt;/a&gt;
.&amp;#160;&lt;a href="#fnref:7" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;p&gt;Читайте оригинал на странице: &lt;a href="https://www.juev.org/2026/10/11/lists/" alt="Lists: список дел, который хранится у вас"&gt;Lists: список дел, который хранится у вас&lt;/a&gt; (Denis Evsyukov)&lt;/p&gt;</description></item><item><title>freshgo: порт FreshRSS на Go, который перестал быть портом</title><link>https://www.juev.org/2026/10/10/freshgo/</link><pubDate>Sat, 10 Oct 2026 21:20:10 +0300</pubDate><author>denis@evsyukov.org (Denis Evsyukov)</author><guid>https://www.juev.org/2026/10/10/freshgo/</guid><description>&lt;p&gt;RSS мне нравится тем, что между мной и автором никого нет. Я сам выбираю сайты, и их обновления приходят ко мне все и по порядку. Лента не решает за меня, что показать первым, а что придержать.&lt;/p&gt;
&lt;p&gt;Читалки при этом я менял. Долгое время пользовался &lt;a href="https://miniflux.app/"&gt;miniflux&lt;/a&gt;
и даже переносил его на отдельный хостинг&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;. Потом перешёл на &lt;a href="https://freshrss.org"&gt;FreshRSS&lt;/a&gt;
и поставил его на свой сервер&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;. Он меня в целом устраивал: ленты обновлялись, клиенты на телефоне работали. Но постоянно что-то не нравилось, по мелочи и в разных местах, и ни одна из этих мелочей не тянула на причину переезжать.&lt;/p&gt;
&lt;p&gt;В итоге я сделал свою читалку: с помощью Claude написал &lt;a href="https://github.com/juev/freshgo"&gt;freshgo&lt;/a&gt;
.&lt;/p&gt;
&lt;p&gt;&lt;figure class="image-figure"&gt;&lt;img src="https://static.juev.org/2026/10/freshgo-classic.png"
alt="Экран чтения freshgo в оформлении Reader classic"
loading="lazy"
decoding="async"&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id="сначала-порт"&gt;&lt;a href="#%d1%81%d0%bd%d0%b0%d1%87%d0%b0%d0%bb%d0%b0-%d0%bf%d0%be%d1%80%d1%82" class="post_hash"&gt;Сначала порт&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Задумывался freshgo как полный порт FreshRSS на Go: один исполняемый файл, который забирает данные работающей установки и встаёт на её место. Первый коммит сделан утром 6 октября. К вечеру того же дня в репозитории были хранение в SQLite и PostgreSQL, импорт из FreshRSS, разбор лент всех видов, которые умеет оригинал, его язык поиска и фильтры, Google Reader API и каркас веб-интерфейса&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Образцом служил запущенный FreshRSS 1.30.1. В репозитории лежит то, что он выдал на фиксированном наборе лент и что ответил на список запросов API, и тесты сверяют поведение freshgo с этими ответами&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Переезд занимает одну команду: &lt;code&gt;freshgo import&lt;/code&gt; читает каталог данных FreshRSS и переносит пользователей с паролями, ленты с настройками, статьи с отметками о прочтении, метки и значки. Мои 84 ленты и шесть тысяч статей импорт перенёс за 1,1 секунды. Обратной дороги нет, базу freshgo в FreshRSS не вернуть, поэтому каталог оригинала лучше сохранить, пока не убедитесь, что всё на месте.&lt;/p&gt;
&lt;p&gt;Клиентам о переезде знать не нужно. FreshRSS отдаёт Google Reader API по адресу &lt;code&gt;/api/greader.php&lt;/code&gt;, и freshgo отвечает по тому же пути, с прежними паролями API, уже выданными токенами и идентификаторами статей. Тот же API доступен и по обычным путям Google Reader, &lt;code&gt;/accounts/&lt;/code&gt; и &lt;code&gt;/reader/&lt;/code&gt;, так что клиенту можно дать просто адрес сервера. Fever API не реализован.&lt;/p&gt;
&lt;h2 id="потом-своё"&gt;&lt;a href="#%d0%bf%d0%be%d1%82%d0%be%d0%bc-%d1%81%d0%b2%d0%be%d1%91" class="post_hash"&gt;Потом своё&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Чистым портом freshgo пробыл недолго. Версия 0.1.0 вышла 8 октября, и уже в ней было то, чего в оригинале нет; остальное добавилось за следующие два дня.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Интерфейс Google Reader.&lt;/strong&gt; Веб-интерфейс у freshgo свой, и собран он по образцу Google Reader: слева дерево категорий и лент, справа статьи по одной строке, статья раскрывается на месте. Оформлений два, «Reader classic» и «Reader 2011», по двум версиям оригинала. У каждого есть светлые и тёмные цвета; тёмные пришлось придумывать, у Google Reader их не было. Страницы собирает сервер, любое действие остаётся ссылкой или формой и работает без JavaScript, а скрипт добавляет клавиши и действия без перезагрузки страницы.&lt;/p&gt;
&lt;p&gt;&lt;figure class="image-figure"&gt;&lt;img src="https://static.juev.org/2026/10/freshgo-2011-dark.png"
alt="Оформление Reader 2011 с тёмными цветами"
loading="lazy"
decoding="async"&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;figure class="image-figure"&gt;&lt;img src="https://static.juev.org/2026/10/freshgo-entry.png"
alt="Раскрытая статья: действия стоят между заголовком и текстом"
loading="lazy"
decoding="async"&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Телефон и PWA.&lt;/strong&gt; Под узкий экран интерфейс доработан отдельно. Меню и управление списком убираются за кнопки. Касание строки раскрывает статью и закрывает предыдущую. Внизу закреплена панель: следующая и предыдущая статья, отметка о прочтении, звёздочка. freshgo можно установить как приложение, с телефона через «На экран Домой», с компьютера через «Установить». Для этого нужен HTTPS, и читать без сети нельзя: это тот же сайт в отдельном окне.&lt;/p&gt;
&lt;div class="shareding-gallery"&gt;
&lt;a href="https://static.juev.org/2026/10/freshgo-phone-list.png"&gt;&lt;img src="https://static.juev.org/2026/10/freshgo-phone-list.png" alt="Список статей freshgo на телефоне" width="390" height="844" loading="lazy"&gt;&lt;span&gt;Список&lt;/span&gt;&lt;/a&gt;
&lt;a href="https://static.juev.org/2026/10/freshgo-phone-entry.png"&gt;&lt;img src="https://static.juev.org/2026/10/freshgo-phone-entry.png" alt="Раскрытая статья freshgo на телефоне" width="390" height="844" loading="lazy"&gt;&lt;span&gt;Статья&lt;/span&gt;&lt;/a&gt;
&lt;a href="https://static.juev.org/2026/10/freshgo-phone-tree.png"&gt;&lt;img src="https://static.juev.org/2026/10/freshgo-phone-tree.png" alt="Дерево подписок freshgo на телефоне" width="390" height="844" loading="lazy"&gt;&lt;span&gt;Подписки&lt;/span&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Картинки через сервер.&lt;/strong&gt; Обычно картинки статьи браузер грузит с тех сайтов, где они лежат, и эти сайты видят адрес читателя и время, когда он открыл статью. freshgo может отдавать картинки со своего адреса. Схему я взял у miniflux&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt;: адреса подменяются в момент показа, каждый подписан, и сервер загружает только то, что сам вставил в статью. Режимов три: все картинки, только те, что отдаются по &lt;code&gt;http&lt;/code&gt; (их страница на &lt;code&gt;https&lt;/code&gt; иначе не покажет вовсе), и никакие. Картинки нигде не сохраняются, сервер передаёт их насквозь. Работает это и в веб-интерфейсе, и для клиентов Google Reader API.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Полный текст.&lt;/strong&gt; Многие ленты отдают только анонс. FreshRSS умеет забирать текст со страницы статьи по CSS-селектору, и селектор для каждой ленты надо написать самому. В freshgo к этому добавились три вещи:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;статья находится на странице без селектора, так же, как её находит режим чтения в браузере;&lt;/li&gt;
&lt;li&gt;текст можно запросить для одной статьи клавишей &lt;code&gt;f&lt;/code&gt;, что бы ни было настроено у ленты;&lt;/li&gt;
&lt;li&gt;страницы ленты может читать настоящий браузер.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Третий пункт появился из-за сайтов за проверкой Cloudflare. Лента у &lt;code&gt;openai.com&lt;/code&gt; читается, а на запрос страницы статьи сервер получает 403 и «Just a moment…», напрямую и через прокси, с User-Agent и curl, и Firefox&lt;sup id="fnref:6"&gt;&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref"&gt;6&lt;/a&gt;&lt;/sup&gt;. Теперь рядом с freshgo можно запустить браузер с протоколом DevTools и отметить ленты, страницы которых читает он. Из того, что я пробовал, самым лёгким оказался &lt;a href="https://lightpanda.io"&gt;Lightpanda&lt;/a&gt;
: около 30 МиБ в простое и несколько сотен, пока он читает страницу. Проверку, которая требует щёлкнуть или собрать картинку, так не пройти; статья в этом случае остаётся с текстом из ленты.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Перевод.&lt;/strong&gt; У раскрытой статьи есть действие «Перевести», клавиша &lt;code&gt;T&lt;/code&gt;. Переводит языковая модель: подходит любой сервис с API, совместимым с OpenAI, адрес, ключ и модель задаются тремя настройками сервера. Текст уходит в модель по абзацам и появляется на экране по мере готовности. Разметку модель не видит совсем: ссылки, картинки и код заменяются номерными метками и возвращаются на место без изменений. Готовый перевод хранится вместе со статьёй, второй раз за него платить не придётся. Помнить стоит одно: текст статьи при каждом запросе перевода уходит с вашего сервера в тот сервис, который вы назвали.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;WebSub.&lt;/strong&gt; Если лента объявляет хаб, freshgo подписывается на него, и новые статьи приходят в момент публикации; сама лента после этого опрашивается раз в сутки. В первой версии подписка зависела от опроса: пока документ ленты не менялся, сервер к хабу не обращался, и редко обновляемая лента могла неделями оставаться без подписки. Теперь адрес хаба хранится вместе с лентой, читается и из заголовков &lt;code&gt;Link&lt;/code&gt;, а подписки продлеваются по сроку&lt;sup id="fnref:7"&gt;&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref"&gt;7&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Остальное перечислю коротко: вход через OpenID Connect, один прокси на все ленты с возможностью задать лентам свой, управление с клавиатуры с палитрой команд по &lt;code&gt;Ctrl+K&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="что-показали-замеры"&gt;&lt;a href="#%d1%87%d1%82%d0%be-%d0%bf%d0%be%d0%ba%d0%b0%d0%b7%d0%b0%d0%bb%d0%b8-%d0%b7%d0%b0%d0%bc%d0%b5%d1%80%d1%8b" class="post_hash"&gt;Что показали замеры&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Писать на Go ради того, чтобы было на Go, смысла мало, поэтому я попросил сравнение. Оба сервера работали на одной машине с одними и теми же подписками: один пользователь, 84 ленты, около 6000 статей, база в SQLite&lt;sup id="fnref:8"&gt;&lt;a href="#fn:8" class="footnote-ref" role="doc-noteref"&gt;8&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th style="text-align: right"&gt;FreshRSS 1.30.1&lt;/th&gt;
&lt;th style="text-align: right"&gt;freshgo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Процессорное время за сутки&lt;/td&gt;
&lt;td style="text-align: right"&gt;109 с&lt;/td&gt;
&lt;td style="text-align: right"&gt;95 с&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Память без дискового кэша, средняя / наибольшая&lt;/td&gt;
&lt;td style="text-align: right"&gt;116 / 178 МиБ&lt;/td&gt;
&lt;td style="text-align: right"&gt;42 / 64 МиБ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Образ контейнера&lt;/td&gt;
&lt;td style="text-align: right"&gt;384 МБ&lt;/td&gt;
&lt;td style="text-align: right"&gt;38,3 МБ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Данные на диске&lt;/td&gt;
&lt;td style="text-align: right"&gt;189 МиБ&lt;/td&gt;
&lt;td style="text-align: right"&gt;49 МиБ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Записано на диск за сутки&lt;/td&gt;
&lt;td style="text-align: right"&gt;1,2 ГиБ&lt;/td&gt;
&lt;td style="text-align: right"&gt;0,54 ГиБ&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Процессора оба тратят одинаково мало: время обновления лент уходит на ожидание чужих сайтов, а не на вычисления. Памяти freshgo нужно втрое меньше. На диске разница вчетверо, и почти вся она в кэше: FreshRSS хранит копию каждого загруженного документа ленты, freshgo запоминает только &lt;code&gt;ETag&lt;/code&gt; и &lt;code&gt;Last-Modified&lt;/code&gt;. Сами статьи занимают в обеих базах одинаково.&lt;/p&gt;
&lt;p&gt;Со скоростью ответов получилось интереснее. В первом замере freshgo версии 0.2.3 проиграл оригиналу именно там, где клиент проводит время при синхронизации:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Запрос&lt;/th&gt;
&lt;th style="text-align: right"&gt;FreshRSS&lt;/th&gt;
&lt;th style="text-align: right"&gt;freshgo 0.2.3&lt;/th&gt;
&lt;th style="text-align: right"&gt;freshgo 0.2.7&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stream/contents&lt;/code&gt;, 50 статей&lt;/td&gt;
&lt;td style="text-align: right"&gt;4,6 мс&lt;/td&gt;
&lt;td style="text-align: right"&gt;7,3 мс&lt;/td&gt;
&lt;td style="text-align: right"&gt;3,3–3,7 мс&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stream/contents&lt;/code&gt;, 200 статей&lt;/td&gt;
&lt;td style="text-align: right"&gt;13,8–18,4 мс&lt;/td&gt;
&lt;td style="text-align: right"&gt;25,7 мс&lt;/td&gt;
&lt;td style="text-align: right"&gt;11,3–11,7 мс&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Список непрочитанных&lt;/td&gt;
&lt;td style="text-align: right"&gt;1,5 мс&lt;/td&gt;
&lt;td style="text-align: right"&gt;9,7 мс&lt;/td&gt;
&lt;td style="text-align: right"&gt;0,7 мс&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Главная страница, 20 непрочитанных&lt;/td&gt;
&lt;td style="text-align: right"&gt;8,8 мс&lt;/td&gt;
&lt;td style="text-align: right"&gt;10,2 мс&lt;/td&gt;
&lt;td style="text-align: right"&gt;5,3–5,8 мс&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stream/contents&lt;/code&gt;, 8 соединений&lt;/td&gt;
&lt;td style="text-align: right"&gt;694 запроса/с&lt;/td&gt;
&lt;td style="text-align: right"&gt;426 запросов/с&lt;/td&gt;
&lt;td style="text-align: right"&gt;1118–1125 запросов/с&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Причины нашлись обычные. Список непрочитанных открывал каждую строку пользователя ради одного флага. Текст статьи чистился заново при каждом показе. Адреса картинок подменялись через полный разбор HTML. После индексов, чистки текста при записи и подмены адресов без разбора документа freshgo отвечает быстрее FreshRSS на девяти запросах из десяти и наравне на одном.&lt;/p&gt;
&lt;p&gt;Для одного читателя всё это несколько миллисекунд в любую сторону. Разница станет заметной при многих пользователях или с клиентами, которые забирают статьи тысячами.&lt;/p&gt;
&lt;h2 id="чего-ждать"&gt;&lt;a href="#%d1%87%d0%b5%d0%b3%d0%be-%d0%b6%d0%b4%d0%b0%d1%82%d1%8c" class="post_hash"&gt;Чего ждать&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Проекту меньше недели. За это время вышло двенадцать версий, считая три предварительные; последняя 0.2.7.&lt;/li&gt;
&lt;li&gt;Проверен он одной установкой, моей: один пользователь, SQLite. PostgreSQL поддерживается и покрыт тестами, но в сравнении не участвовал.&lt;/li&gt;
&lt;li&gt;Импорт читает FreshRSS на SQLite и PostgreSQL. MySQL и MariaDB он не читает.&lt;/li&gt;
&lt;li&gt;PHP-расширения FreshRSS не работают. Точки расширения есть, но обработчики для них собираются вместе с сервером.&lt;/li&gt;
&lt;li&gt;Часть поведения отличается от оригинала намеренно, список есть в README&lt;sup id="fnref:9"&gt;&lt;a href="#fn:9" class="footnote-ref" role="doc-noteref"&gt;9&lt;/a&gt;&lt;/sup&gt;.&lt;/li&gt;
&lt;li&gt;Это независимый проект. Вопросы по нему авторам FreshRSS задавать не нужно.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Теперь на моём сервере работает freshgo, и я могу рекомендовать его вам. Код и инструкции лежат в репозитории &lt;a href="https://github.com/juev/freshgo"&gt;juev/freshgo&lt;/a&gt;
, образ опубликован как &lt;code&gt;ghcr.io/juev/freshgo&lt;/code&gt;. Если что-то работает не так или чего-то не хватает, заведите issue.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;&lt;a href="https://www.juev.org/2020/12/20/miniflux-on-heroku/"&gt;Miniflux on Heroku&lt;/a&gt;
.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;&lt;a href="https://www.juev.org/2025/01/20/docker-localhost/"&gt;Настройка Docker для работы с локальным прокси-сервером&lt;/a&gt;
: заметка времён переноса FreshRSS на свой сервер.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;&lt;a href="https://github.com/juev/freshgo/commits/main"&gt;История коммитов juev/freshgo&lt;/a&gt;
.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;&lt;a href="https://github.com/juev/freshgo/tree/main/testdata/reference"&gt;testdata/reference&lt;/a&gt;
и спецификации в &lt;a href="https://github.com/juev/freshgo/tree/main/docs/specs"&gt;docs/specs&lt;/a&gt;
.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:5"&gt;
&lt;p&gt;&lt;a href="https://github.com/juev/freshgo/issues/4"&gt;Issue #4&lt;/a&gt;
: разбор того, как отдаёт картинки miniflux, и что из этого взято.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:6"&gt;
&lt;p&gt;&lt;a href="https://github.com/juev/freshgo/issues/55"&gt;Issue #55&lt;/a&gt;
: что было испробовано против проверки Cloudflare и сколько памяти занимает каждый вариант.&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:7"&gt;
&lt;p&gt;&lt;a href="https://github.com/juev/freshgo/issues/18"&gt;Issue #18&lt;/a&gt;
.&amp;#160;&lt;a href="#fnref:7" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:8"&gt;
&lt;p&gt;&lt;a href="https://github.com/juev/freshgo/pull/77"&gt;freshgo and FreshRSS measured side by side&lt;/a&gt;
— условия, полные таблицы и способ повторить. Процессор, память и диск измерены у версии 0.2.3, время ответов у версии 0.2.7.&amp;#160;&lt;a href="#fnref:8" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:9"&gt;
&lt;p&gt;Раздел &lt;a href="https://github.com/juev/freshgo#what-behaves-differently"&gt;What behaves differently&lt;/a&gt;
.&amp;#160;&lt;a href="#fnref:9" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;p&gt;Читайте оригинал на странице: &lt;a href="https://www.juev.org/2026/10/10/freshgo/" alt="freshgo: порт FreshRSS на Go, который перестал быть портом"&gt;freshgo: порт FreshRSS на Go, который перестал быть портом&lt;/a&gt; (Denis Evsyukov)&lt;/p&gt;</description></item><item><title>Ссылки #411</title><link>https://www.juev.org/2026/10/10/weblinks-411/</link><pubDate>Sat, 10 Oct 2026 09:31:16 +0300</pubDate><author>denis@evsyukov.org (Denis Evsyukov)</author><guid>https://www.juev.org/2026/10/10/weblinks-411/</guid><description>&lt;p&gt;На этой неделе в подборке: скрытые функции слежки в мессенджере Max, разбор ссылок в Git, HTTP-туннели на SSH и nginx с временными ссылками и документация вместо памяти для AI-агентов.&lt;/p&gt;
&lt;h2 id="-безопасность"&gt;&lt;a href="#-%d0%b1%d0%b5%d0%b7%d0%be%d0%bf%d0%b0%d1%81%d0%bd%d0%be%d1%81%d1%82%d1%8c" class="post_hash"&gt;🔐 Безопасность&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://interseclab.org/research/max/"&gt;The Max Messenger: An Analysis of Russia’s State-Mandated Messaging Application | InterSecLab&lt;/a&gt;
&lt;/strong&gt;
Max — российский мессенджер, функции которого VK может менять для отдельных пользователей. В исследовании рассматриваются возможности слежки, которые могут включаться незаметно для пользователя.&lt;/p&gt;
&lt;h2 id="-разработка"&gt;&lt;a href="#-%d1%80%d0%b0%d0%b7%d1%80%d0%b0%d0%b1%d0%be%d1%82%d0%ba%d0%b0" class="post_hash"&gt;💻 Разработка&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://matklad.github.io/2026/10/07/git-ref.html"&gt;On Git Refs&lt;/a&gt;
&lt;/strong&gt;
Фрагмент посвящён тому, как автор уточнил своё понимание Git. Для этого он предлагает рассмотреть две команды, но сами команды и объяснение в предоставленном тексте отсутствуют.&lt;/p&gt;
&lt;h2 id="-инфраструктура"&gt;&lt;a href="#-%d0%b8%d0%bd%d1%84%d1%80%d0%b0%d1%81%d1%82%d1%80%d1%83%d0%ba%d1%82%d1%83%d1%80%d0%b0" class="post_hash"&gt;🖥️ Инфраструктура&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://vincent.bernat.ch/en/blog/2026-http-over-ssh"&gt;Self-hosted HTTP tunnels with SSH and nginx&lt;/a&gt;
&lt;/strong&gt;
Связка OpenSSH и nginx позволяет организовать HTTP-туннель без установки дополнительного ПО: для этого используются удалённая переадресация портов и модуль secure_link. Результат — доступная для передачи HTTPS-ссылка с токеном, срок действия которого истекает.&lt;/p&gt;
&lt;h2 id="-агенты"&gt;&lt;a href="#-%d0%b0%d0%b3%d0%b5%d0%bd%d1%82%d1%8b" class="post_hash"&gt;🤖 Агенты&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://liao.gg/blog/agents-dont-need-memory"&gt;Agents Don&amp;rsquo;t Need Memory. They Need Documentation. | Kevin Liao&lt;/a&gt;
&lt;/strong&gt;
Для AI-агентов предлагается использовать документацию проекта вместо извлечения нужного контекста из старых разговоров. Краткое описание формулирует этот подход, но не раскрывает, как вести документацию, и не приводит результатов сравнения.&lt;/p&gt;
&lt;h2 id="а-ещё"&gt;&lt;a href="#%d0%b0-%d0%b5%d1%89%d1%91" class="post_hash"&gt;А ещё&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://serokell.io/blog/teaching-haskell-in-the-age-of-llms"&gt;Teaching Haskell in the Age of LLMs, Part 1: Ban or Embrace?&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sheets.works/data-viz/holding-up-the-internet"&gt;The people holding up the internet | Data Drop&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://letsencrypt.org/2026/10/07/64-day-certs.html"&gt;64-Day Certificate Lifetimes Coming Feb 2027 - Let&amp;rsquo;s Encrypt&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cactuscompute.com/blog/whistle"&gt;Whistle: Speech to Text in 16.9 MB | Cactus&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.sregym.com/blog/jev-driven-sre-diagnosis"&gt;Jev-Driven SRE Diagnosis: What Worked and What Failed | SREGym Blog&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.google/innovation-and-ai/technology/developers-tools/embeddinggemma-2/"&gt;EmbeddingGemma 2 is a best-in-class open model for natively multimodal embeddings&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openai.com/index/sharing-ai-progress-in-mathematics/"&gt;Sharing AI progress in mathematics | OpenAI&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonwillison.net/2026/Oct/4/qwen38-addition-in-words/"&gt;Research: Qwen3.8 27B addition in words&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://spwoodcock.dev/blog/2026-08-05-graphene-pixel-esim-fix/"&gt;How to fix the eSIM eUICC on a Google Pixel running GrapheneOS&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://inco.ai/blog/splash/"&gt;Splash: A Local Engine Built Around the Model — Inco AI&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://earendil.com/posts/pi-1-0/"&gt;Pi 1.0 | Earendil&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.blog/engineering/architecture-optimization/improving-site-performance-by-shipping-more-css/"&gt;Improving site performance by shipping more CSS - The GitHub Blog&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lightpanda.io/blog/posts/lightpanda-1-0"&gt;Lightpanda 1.0: Out of Beta and Ready for Production - Blog | Lightpanda&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;За прошедшую неделю собрал 57 ссылок.&lt;/p&gt;
&lt;p&gt;Хорошей вам недели!&lt;/p&gt;
&lt;p&gt;Читайте оригинал на странице: &lt;a href="https://www.juev.org/2026/10/10/weblinks-411/" alt="Ссылки #411"&gt;Ссылки #411&lt;/a&gt; (Denis Evsyukov)&lt;/p&gt;</description></item><item><title>Ссылки #410</title><link>https://www.juev.org/2026/10/03/weblinks-410/</link><pubDate>Sat, 03 Oct 2026 11:57:42 +0300</pubDate><author>denis@evsyukov.org (Denis Evsyukov)</author><guid>https://www.juev.org/2026/10/03/weblinks-410/</guid><description>&lt;p&gt;На этой неделе в подборке: как водяные знаки меняют поведение LLM-агентов, четырёхкратное ускорение prompt lookup в llama.cpp, подводные камни AT TIME ZONE &amp;lsquo;UTC&amp;rsquo; в Postgres и сайт, спрятанный в Tor.&lt;/p&gt;
&lt;h2 id="-ии"&gt;&lt;a href="#-%d0%b8%d0%b8" class="post_hash"&gt;🤖 ИИ&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.lasso.security/blog/the-provenance-tax-understanding-the-impact-of-llm-watermarking-on-ai-agent-behavior"&gt;The Provenance Tax: Understanding the Impact of LLM Watermarking on AI Agent Behavior&lt;/a&gt;
&lt;/strong&gt;
Проверка водяных знаков SynthID-Text на шести моделях выявила, что маркировка меняет точность вызовов инструментов и ослабляет отказы при атаке через внедрение подсказок. На некоторых моделях вызванные маркировкой изменения поведения сильнее, чем при изменении температуры.&lt;/p&gt;
&lt;h2 id="-производительность"&gt;&lt;a href="#-%d0%bf%d1%80%d0%be%d0%b8%d0%b7%d0%b2%d0%be%d0%b4%d0%b8%d1%82%d0%b5%d0%bb%d1%8c%d0%bd%d0%be%d1%81%d1%82%d1%8c" class="post_hash"&gt;⚡ Производительность&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://jadidbourbaki.github.io/blog/prompt-lookup-llama-cpp/"&gt;42x Faster Prompt Lookup Drafting in llama.cpp&lt;/a&gt;
&lt;/strong&gt;
Четыре изменения кэшей n-грамм в llama.cpp ускоряют подготовку токенов для prompt lookup decoding: обработка одного чернового токена становится быстрее до 41,6 раза, загрузка статического кэша — до 23,5 раза. Пиковое потребление памяти при этом снижается до 2,65 раза, но это верхние значения улучшений, а не гарантированный результат для каждого запуска.&lt;/p&gt;
&lt;h2 id="-базы-данных"&gt;&lt;a href="#-%d0%b1%d0%b0%d0%b7%d1%8b-%d0%b4%d0%b0%d0%bd%d0%bd%d1%8b%d1%85" class="post_hash"&gt;🗄️ Базы данных&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://bookofrevenue.com/blog/6ab81e9a97a13f0001f7e4e1/postgres-at-time-zone-u-does-not-do-what-you-think-it-does"&gt;Postgres AT TIME ZONE &amp;lsquo;UTC&amp;rsquo; does NOT do what you think it does&lt;/a&gt;
&lt;/strong&gt;
В PostgreSQL выражение AT TIME ZONE &amp;lsquo;UTC&amp;rsquo; преобразует timestamptz в timestamp без часового пояса, поэтому дальнейшие операции уже не учитывают исходный тип времени. При сравнении такого значения с timestamptz результат зависит от часового пояса сервера: равенство сработает, только если он установлен в UTC.&lt;/p&gt;
&lt;h2 id="-приватность"&gt;&lt;a href="#-%d0%bf%d1%80%d0%b8%d0%b2%d0%b0%d1%82%d0%bd%d0%be%d1%81%d1%82%d1%8c" class="post_hash"&gt;🔍 Приватность&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://david.alvarezrosa.com/posts/self-hosting-on-the-dark-web/"&gt;Self-Hosting on the Dark Web | David Álvarez Rosa&lt;/a&gt;
&lt;/strong&gt;
Сайт размещён как скрытый сервис Tor и доступен по адресу .onion, который работает только внутри сети Tor. Описание сообщает о доступности сайта через Tor, но не раскрывает подробностей настройки или способа размещения.&lt;/p&gt;
&lt;h2 id="а-ещё"&gt;&lt;a href="#%d0%b0-%d0%b5%d1%89%d1%91" class="post_hash"&gt;А ещё&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://earendil.com/posts/you-said-no-mcp/"&gt;“You Said No MCP!” | Earendil&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://buttondown.com/hillelwayne/archive/what-tla-can-and-cant-check/"&gt;What TLA+ can and can&amp;rsquo;t check • Buttondown&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.netlify.com/blog/edge-functions-firecracker-microvms/"&gt;5x faster Edge Functions: How we replaced v8 isolates with Firecracker MicroVMs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lapcatsoftware.com/articles/2026/9/10.html"&gt;Web browsers phoning home on launch&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.instapaper.com/2026/09/29/instapaper-api-v2/"&gt;Instapaper API v2 | Instapaper Blog&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.cloudflare.com/cloudflare-certificate-authority/"&gt;Building a certificate authority for the whole Internet | Cloudflare Blog&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.alexewerlof.com/p/coding-is-not-solved"&gt;Coding is NOT solved - Alex Ewerlöf Notes&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ssp.sh/brain/the-problem-is-not-the-ai-code-but-nobody-knows-anything-anymore/"&gt;The Problem is not the AI Code, but Nobody Knows Anything Anymore&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://soapbox.pub/armada"&gt;Armada: The Open Source, Encrypted Discord Alternative | Soapbox&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.gitea.com/release-of-runner-4.0.0/"&gt;Gitea Runner 4.0.0 is released | Gitea Blog&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://eoinhiggins.substack.com/p/there-are-no-rogue-ai-agents"&gt;There are no “rogue” AI agents&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.docker.com/products/docker-sandboxes/"&gt;Docker Sandboxes для агентов&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://alignment.openai.com/misalignment-reports/an-agent-used-dns-to-reach-an-external-chatbot/"&gt;An agent used DNS to reach an external chatbot · OpenAI Alignment&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;За прошедшую неделю собрал 87 ссылок.&lt;/p&gt;
&lt;p&gt;Хорошей вам недели!&lt;/p&gt;
&lt;p&gt;Читайте оригинал на странице: &lt;a href="https://www.juev.org/2026/10/03/weblinks-410/" alt="Ссылки #410"&gt;Ссылки #410&lt;/a&gt; (Denis Evsyukov)&lt;/p&gt;</description></item><item><title>linkding на Go: сутки на порт и неделя на то, что всплыло после</title><link>https://www.juev.org/2026/10/02/linkding-go/</link><pubDate>Fri, 02 Oct 2026 10:05:17 +0300</pubDate><author>denis@evsyukov.org (Denis Evsyukov)</author><guid>https://www.juev.org/2026/10/02/linkding-go/</guid><description>&lt;p&gt;Закладки я давно храню в &lt;a href="https://github.com/sissbruecker/linkding"&gt;linkding&lt;/a&gt;
. Привык к нему настолько, что на прошлой неделе написал для него клиент под Android&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;. К самому серверу претензия у меня была одна, зато постоянная.&lt;/p&gt;
&lt;p&gt;linkding написан на Python и Django. Настройки запуска я не менял, а по умолчанию образ поднимает uWSGI в два процесса по два потока&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;. Одновременно сервер обрабатывает четыре запроса, остальные ждут в очереди. При этом мой linkding смотрит в интернет, и кроме меня к нему ходят сканеры и боты, которые перебирают адреса. Время от времени он просто переставал отвечать, и приходилось ждать, пока отпустит. Число процессов можно поднять, но каждый занимает память, а пользуется сервисом один человек.&lt;/p&gt;
&lt;p&gt;В какой-то момент я задумался, нельзя ли перенести linkding на Go. Писать руками порт чужого проекта с админкой, OIDC, импортом, снимками страниц и двумя базами данных я бы не стал. Поэтому поручил анализ и работу Codex.&lt;/p&gt;
&lt;h2 id="сутки"&gt;&lt;a href="#%d1%81%d1%83%d1%82%d0%ba%d0%b8" class="post_hash"&gt;Сутки&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Сначала появился документ с условиями приёмки&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;. В нём зафиксирована версия оригинала, v1.47.0, и перечислено, что должно совпасть: маршруты, поля JSON, коды ответов, тексты ошибок, внешний вид страниц, настройки &lt;code&gt;LD_*&lt;/code&gt;. Отдельной строкой записано главное правило всей работы: если документация и код оригинала расходятся, образцом считается ответ запущенного linkding этой версии.&lt;/p&gt;
&lt;p&gt;Так и шла работа. Каждый шаг сверялся с Python-версией, и при любом расхождении порт дорабатывался. По истории репозитория это видно лучше, чем по моему пересказу: десятки коммитов подряд начинаются со слова &lt;code&gt;match&lt;/code&gt;. Совпадение формы ошибки при битом заголовке с токеном. Совпадение ответов на &lt;code&gt;HEAD&lt;/code&gt; и &lt;code&gt;OPTIONS&lt;/code&gt;. Порядок вариантов сортировки в выпадающем списке. Счётчики в фильтрах админки. Переводы админки, взятые из каталогов Django.&lt;/p&gt;
&lt;p&gt;Первый коммит датирован вечером 25 сентября, релиз v1.47.0 вышел днём 26-го&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;. Между ними 76 коммитов. Codex работал чуть больше суток почти без остановок, и на выходе получился сервер, который собирается без CGO в один исполняемый файл и умеет SQLite и PostgreSQL.&lt;/p&gt;
&lt;p&gt;Существующую установку порт принимает через отдельную команду миграции&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt;. Она переносит пользователей, хеши паролей, закладки, теги, токены API и файлы в новую базу, а исходную не трогает, так что откатиться можно в любой момент. Пароли и токены остаются прежними, заново нужно только войти.&lt;/p&gt;
&lt;h2 id="первый-запуск-oidc"&gt;&lt;a href="#%d0%bf%d0%b5%d1%80%d0%b2%d1%8b%d0%b9-%d0%b7%d0%b0%d0%bf%d1%83%d1%81%d0%ba-oidc" class="post_hash"&gt;Первый запуск: OIDC&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Я перенёс данные, запустил порт на месте оригинала и не смог войти.&lt;/p&gt;
&lt;p&gt;Перед linkding у меня стоит Caddy: он принимает HTTPS, а до приложения запрос идёт по обычному HTTP. Порт видел у себя HTTP и именно с этой схемой собирал адрес возврата для OIDC. Провайдер такой адрес не принимал.&lt;/p&gt;
&lt;p&gt;В оригинале эту задачу решает промежуточный слой Django, который читает заголовки прокси. В порте пришлось чинить код: с настройкой &lt;code&gt;LD_USE_X_FORWARDED_HOST&lt;/code&gt; сервер берёт схему и имя хоста из заголовков &lt;code&gt;X-Forwarded-Proto&lt;/code&gt; и &lt;code&gt;X-Forwarded-Host&lt;/code&gt;&lt;sup id="fnref:6"&gt;&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref"&gt;6&lt;/a&gt;&lt;/sup&gt;. Без неё он эти заголовки игнорирует, иначе адрес возврата мог бы подставить любой клиент; на оба случая есть тесты. Исправление вышло в v1.47.1, через два часа после первого релиза.&lt;/p&gt;
&lt;h2 id="go-оказался-медленнее"&gt;&lt;a href="#go-%d0%be%d0%ba%d0%b0%d0%b7%d0%b0%d0%bb%d1%81%d1%8f-%d0%bc%d0%b5%d0%b4%d0%bb%d0%b5%d0%bd%d0%bd%d0%b5%d0%b5" class="post_hash"&gt;Go оказался медленнее&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Ради чего всё затевалось, нужно было проверить. Я попросил нагрузочное сравнение: один и тот же набор из 10 000 закладок, оба сервера в контейнерах с одинаковыми ограничениями, запускаются по очереди&lt;sup id="fnref:7"&gt;&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref"&gt;7&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Результат на SQLite меня удивил.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Запрос, 8 клиентов&lt;/th&gt;
&lt;th style="text-align: right"&gt;Python, запросов/с&lt;/th&gt;
&lt;th style="text-align: right"&gt;Go, запросов/с&lt;/th&gt;
&lt;th style="text-align: right"&gt;Go к Python&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Список&lt;/td&gt;
&lt;td style="text-align: right"&gt;177&lt;/td&gt;
&lt;td style="text-align: right"&gt;235&lt;/td&gt;
&lt;td style="text-align: right"&gt;1,33&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Поиск&lt;/td&gt;
&lt;td style="text-align: right"&gt;66&lt;/td&gt;
&lt;td style="text-align: right"&gt;55&lt;/td&gt;
&lt;td style="text-align: right"&gt;0,84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Поиск с тегом&lt;/td&gt;
&lt;td style="text-align: right"&gt;59&lt;/td&gt;
&lt;td style="text-align: right"&gt;41&lt;/td&gt;
&lt;td style="text-align: right"&gt;0,70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Страница интерфейса&lt;/td&gt;
&lt;td style="text-align: right"&gt;90&lt;/td&gt;
&lt;td style="text-align: right"&gt;71&lt;/td&gt;
&lt;td style="text-align: right"&gt;0,79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Создание закладки&lt;/td&gt;
&lt;td style="text-align: right"&gt;347&lt;/td&gt;
&lt;td style="text-align: right"&gt;307&lt;/td&gt;
&lt;td style="text-align: right"&gt;0,89&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Порт проигрывал оригиналу почти везде, где работали несколько клиентов сразу. А в том же прогоне нашлась вещь похуже скорости: при восьми параллельных клиентах 664 из 1000 запросов на создание закладки вернули HTTP 500. SQLite не пускал вторую пишущую транзакцию, пока шла первая, а порт не ждал. Это закрыл тот же релиз v1.47.1: транзакции теперь открываются в режиме &lt;code&gt;IMMEDIATE&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Дальше началась обычная работа с базой. Разбор запросов показал, что язык тут ни при чём:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Страницу из ста закладок порт собирал сотней отдельных запросов, по одному на закладку с её тегами. Python забирал теги для всей страницы разом.&lt;/li&gt;
&lt;li&gt;При создании закладки проверка на дубликат не попадала в индекс и просматривала все закладки владельца.&lt;/li&gt;
&lt;li&gt;Поиск по тегу повторял соединение таблиц для каждой закладки-кандидата.&lt;/li&gt;
&lt;li&gt;На каждое слово в поиске функция сравнения без учёта регистра вызывалась четыре раза.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Каждая правка мерилась отдельной парой прогонов «до и после»&lt;sup id="fnref:8"&gt;&lt;a href="#fn:8" class="footnote-ref" role="doc-noteref"&gt;8&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Что изменилось&lt;/th&gt;
&lt;th style="text-align: right"&gt;Было, запросов/с&lt;/th&gt;
&lt;th style="text-align: right"&gt;Стало, запросов/с&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Список, 32 клиента&lt;/td&gt;
&lt;td style="text-align: right"&gt;244&lt;/td&gt;
&lt;td style="text-align: right"&gt;349&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Поиск, 32 клиента&lt;/td&gt;
&lt;td style="text-align: right"&gt;57&lt;/td&gt;
&lt;td style="text-align: right"&gt;107&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Страница интерфейса, 32 клиента&lt;/td&gt;
&lt;td style="text-align: right"&gt;69&lt;/td&gt;
&lt;td style="text-align: right"&gt;231&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Создание закладки, 8 клиентов&lt;/td&gt;
&lt;td style="text-align: right"&gt;315&lt;/td&gt;
&lt;td style="text-align: right"&gt;1462&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Пары относятся к разным экспериментам, перемножать их в одно общее ускорение нельзя. И победа не полная: на поиске с тегом при 32 клиентах хвост задержек у Go в последнем сравнении остался длиннее, чем у Python. Справедливости ради, оригинал тоже выигрывает от настройки базы: после &lt;code&gt;ANALYZE&lt;/code&gt; создание закладок на Python ускорилось с 348 до 412 запросов в секунду.&lt;/p&gt;
&lt;p&gt;Мне важнее было другое измерение: сколько ресурсов сервер тратит при одинаковой нагрузке. Оба получали по 50 запросов в секунду&lt;sup id="fnref:9"&gt;&lt;a href="#fn:9" class="footnote-ref" role="doc-noteref"&gt;9&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th style="text-align: right"&gt;Python&lt;/th&gt;
&lt;th style="text-align: right"&gt;Go&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Процессор, ядер (четыре сценария API)&lt;/td&gt;
&lt;td style="text-align: right"&gt;0,43–1,63&lt;/td&gt;
&lt;td style="text-align: right"&gt;0,18–0,64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Память под нагрузкой, МиБ&lt;/td&gt;
&lt;td style="text-align: right"&gt;120–158&lt;/td&gt;
&lt;td style="text-align: right"&gt;14–24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Память в простое, МиБ&lt;/td&gt;
&lt;td style="text-align: right"&gt;68&lt;/td&gt;
&lt;td style="text-align: right"&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Это и есть ответ на исходную претензию. Сканеры никуда не делись, но теперь их обслуживает один процесс, которому хватает двух десятков мегабайт.&lt;/p&gt;
&lt;h2 id="неделя-на-собственном-сервере"&gt;&lt;a href="#%d0%bd%d0%b5%d0%b4%d0%b5%d0%bb%d1%8f-%d0%bd%d0%b0-%d1%81%d0%be%d0%b1%d1%81%d1%82%d0%b2%d0%b5%d0%bd%d0%bd%d0%be%d0%bc-%d1%81%d0%b5%d1%80%d0%b2%d0%b5%d1%80%d0%b5" class="post_hash"&gt;Неделя на собственном сервере&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Тесты на совпадение с оригиналом проверяют то, что догадались проверить. Остальное находится, когда начинаешь пользоваться.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Фильтр, который сбрасывался.&lt;/strong&gt; Я открыл закладки с тегом &lt;code&gt;#inbox&lt;/code&gt;, отметил первую страницу и отправил в архив. В строке поиска по-прежнему стояло &lt;code&gt;#inbox&lt;/code&gt;, а список под ней показывал все закладки подряд. Порт не передавал текущий запрос в адрес действия, оригинал передаёт. При проверке соседних форм нашлось то же самое в поиске и в его настройках: они теряли активные фильтры, которых не было среди видимых полей&lt;sup id="fnref:10"&gt;&lt;a href="#fn:10" class="footnote-ref" role="doc-noteref"&gt;10&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Длинный заголовок.&lt;/strong&gt; Закладка со страницы, у которой заголовок длиннее 512 символов, не сохранялась: сервер отвечал ошибкой 400. Заодно выяснилось, что некоторые пути записи это ограничение обходили и упирались уже в базу. Теперь заголовок нормализуется и обрезается до 512 символов Юникода в одном месте, через которое проходят API, формы, админка, импорт и фоновое обновление метаданных&lt;sup id="fnref:11"&gt;&lt;a href="#fn:11" class="footnote-ref" role="doc-noteref"&gt;11&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Молчащий сервер.&lt;/strong&gt; Разбираясь с заголовками, я заметил, что порт вообще не пишет в консоль, кто и зачем к нему приходил. Для сервера, на который ходят сканеры, это неудобно. Журнал запросов появился в том же релизе; токены лент из адресов в нём вырезаются.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Общий адрес у многих владельцев.&lt;/strong&gt; Эту проблему показал разбор запросов, сам я с ней столкнуться не мог: если одну и ту же ссылку сохранили тысячи пользователей, поиск дубликата просматривал их всех. Составной индекс по адресу и владельцу поднял скорость создания в таком сценарии с 533 до 6884 запросов в секунду&lt;sup id="fnref:12"&gt;&lt;a href="#fn:12" class="footnote-ref" role="doc-noteref"&gt;12&lt;/a&gt;&lt;/sup&gt;. На моей установке с одним пользователем разницы нет, но оставлять это не хотелось.&lt;/p&gt;
&lt;p&gt;За пять дней вышло восемь релизов, от v1.47.0 до v1.47.7.&lt;/p&gt;
&lt;h2 id="что-в-итоге"&gt;&lt;a href="#%d1%87%d1%82%d0%be-%d0%b2-%d0%b8%d1%82%d0%be%d0%b3%d0%b5" class="post_hash"&gt;Что в итоге&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Сейчас у меня работает порт, и разницы с оригиналом я не замечаю: тот же интерфейс, те же расширения для браузера, тот же адрес, ShareDing ходит в тот же API.&lt;/p&gt;
&lt;p&gt;Что стоит знать, прежде чем пробовать:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Порт привязан к linkding v1.47.0. Новые версии оригинала сами в него не попадут.&lt;/li&gt;
&lt;li&gt;Это независимый проект. Оригинал по-прежнему ведёт его автор, Sascha Ißbrücker (&lt;a href="https://github.com/sissbruecker"&gt;sissbruecker&lt;/a&gt;
), и вопросы по порту ему задавать не нужно.&lt;/li&gt;
&lt;li&gt;Все измерения сделаны на ноутбуке короткими прогонами. Они показывают, где порт быстрее или медленнее на одних и тех же данных, и ничего не обещают о предельной нагрузке.&lt;/li&gt;
&lt;li&gt;Проверен он одной установкой — моей.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Последний пункт я могу исправить только с вашей помощью. Код лежит в репозитории &lt;a href="https://github.com/juev/linkding"&gt;juev/linkding&lt;/a&gt;
, готовые сборки и образы — в релизах, порядок переноса описан в руководстве по миграции&lt;sup id="fnref1:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt;. Исходная база при переносе остаётся нетронутой, так что попробовать можно без риска для закладок. Если что-то работает не так, как в оригинале, или чего-то не хватает, заведите issue.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;&lt;a href="https://www.juev.org/2026/09/29/shareding-android/"&gt;ShareDing для Android: ссылки в linkding, даже когда сервер недоступен&lt;/a&gt;
.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;&lt;a href="https://github.com/sissbruecker/linkding/blob/v1.47.0/uwsgi.ini"&gt;uwsgi.ini&lt;/a&gt;
в linkding v1.47.0: &lt;code&gt;processes = 2&lt;/code&gt;, &lt;code&gt;threads = 2&lt;/code&gt;.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;&lt;a href="https://github.com/juev/linkding/blob/main/docs/specs/linkding-parity.md"&gt;linkding v1.47.0 migration contract&lt;/a&gt;
.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;&lt;a href="https://github.com/juev/linkding/releases"&gt;Релизы juev/linkding&lt;/a&gt;
.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:5"&gt;
&lt;p&gt;&lt;a href="https://github.com/juev/linkding/blob/main/docs/migration.md"&gt;Migrate from Python linkding v1.47.0&lt;/a&gt;
.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href="#fnref1:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:6"&gt;
&lt;p&gt;Коммит &lt;a href="https://github.com/juev/linkding/commit/120980f"&gt;120980f&lt;/a&gt;
: схема из заголовка прокси для OIDC и режим транзакций SQLite. Настройка описана в &lt;a href="https://github.com/juev/linkding/blob/main/docs/installation.md"&gt;руководстве по установке&lt;/a&gt;
.&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:7"&gt;
&lt;p&gt;&lt;a href="https://github.com/juev/linkding/blob/main/docs/performance/2026-09-26-linkding-comparison.md"&gt;Local load comparison: Python linkding and the Go port&lt;/a&gt;
— условия, полные таблицы для SQLite и PostgreSQL, способ повторить.&amp;#160;&lt;a href="#fnref:7" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:8"&gt;
&lt;p&gt;&lt;a href="https://github.com/juev/linkding/blob/main/docs/performance/2026-09-26-optimization-summary.md"&gt;Performance optimization results&lt;/a&gt;
.&amp;#160;&lt;a href="#fnref:8" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:9"&gt;
&lt;p&gt;&lt;a href="https://github.com/juev/linkding/blob/main/docs/performance/2026-09-26-resource-comparison.md"&gt;CPU and memory at equal request rates&lt;/a&gt;
. Память — анонимная память контейнера, медиана трёх прогонов.&amp;#160;&lt;a href="#fnref:9" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:10"&gt;
&lt;p&gt;&lt;a href="https://github.com/juev/linkding/pull/3"&gt;Pull request #3&lt;/a&gt;
, вошёл в v1.47.6.&amp;#160;&lt;a href="#fnref:10" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:11"&gt;
&lt;p&gt;Коммит &lt;a href="https://github.com/juev/linkding/commit/2d80805"&gt;2d80805&lt;/a&gt;
, вошёл в v1.47.7.&amp;#160;&lt;a href="#fnref:11" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:12"&gt;
&lt;p&gt;&lt;a href="https://github.com/juev/linkding/pull/1"&gt;Pull request #1&lt;/a&gt;
, вошёл в v1.47.5.&amp;#160;&lt;a href="#fnref:12" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;p&gt;Читайте оригинал на странице: &lt;a href="https://www.juev.org/2026/10/02/linkding-go/" alt="linkding на Go: сутки на порт и неделя на то, что всплыло после"&gt;linkding на Go: сутки на порт и неделя на то, что всплыло после&lt;/a&gt; (Denis Evsyukov)&lt;/p&gt;</description></item></channel></rss>