docs: решено перевести хранилище и файлы записей на PocketBase
- разведка pocketbase-admin-fit ответила замером панели версии 0.39.10: записка в docs/research/pocketbase.md, решение — в ADR - панель показывает файлы и пользователей только своих, поэтому файлы переезжают в её раскладку, а вход идёт через её провайдера OIDC - периметр расширился панелью на /_/ и паролем суперпользователя; закрывает её Authelia на прокси, задачи в беклоге у этого нет
This commit is contained in:
@@ -0,0 +1,80 @@
|
|||||||
|
# Хранилище, файлы и вход переезжают в PocketBase
|
||||||
|
|
||||||
|
- **Дата:** 2026-08-11
|
||||||
|
- **Источник:** [../research/pocketbase.md](../research/pocketbase.md) — записка
|
||||||
|
разведки `pocketbase-admin-fit`
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
PocketBase заменяет SQLite с goqu и goose и берёт на себя три вещи разом:
|
||||||
|
состояние задач и метаданные, файлы записей своим полем и своей раскладкой на
|
||||||
|
диске, вход пользователей через Authelia своим провайдером OIDC. Панель
|
||||||
|
администратора работает по всем трём частям только в таком составе.
|
||||||
|
|
||||||
|
## Почему
|
||||||
|
|
||||||
|
Разведка мерила панель по трём частям, и порознь ни одна перевода не оправдывала.
|
||||||
|
Правку записей панель даёт целиком, а две остальные упираются в то, где лежат
|
||||||
|
данные. Цитата из источника, раздел про пользователей:
|
||||||
|
|
||||||
|
> Панель показывает свою коллекцию пользователей и ничего больше. Отсюда
|
||||||
|
> следствие для целевого входа: **пользователи Authelia в панели не появятся,
|
||||||
|
> если вход делает само приложение**.
|
||||||
|
|
||||||
|
И раздел про файлы:
|
||||||
|
|
||||||
|
> Сегодняшняя раскладка `data/files` с именами-UUID панели не видна. Путь строкой
|
||||||
|
> она покажет строкой — прослушать и скачать запись по ней нельзя. Способа
|
||||||
|
> сослаться на файл, уже лежащий на диске мимо её каталога, нет.
|
||||||
|
|
||||||
|
Там же то, что связывает файлы с сохранностью архива:
|
||||||
|
|
||||||
|
> **Резервные копии накрывают ровно её каталог.** Файлы, оставленные снаружи, в
|
||||||
|
> них не попадут — то есть панель и встроенное резервное копирование покупаются
|
||||||
|
> одной и той же ценой.
|
||||||
|
|
||||||
|
Отвергнуты два половинчатых пути, и оба по одной причине — они покупают перевод,
|
||||||
|
не покупая того, ради чего он затевался:
|
||||||
|
|
||||||
|
> **Держать файлы на диске как сейчас, а в базе — путь строкой.** Отвергнуто:
|
||||||
|
> панель тогда не даёт по файлам ничего, и встроенные копии их не накрывают.
|
||||||
|
> Довод, ради которого перевод затевался, пропадает целиком.
|
||||||
|
>
|
||||||
|
> **Оставить вход у приложения, а PocketBase взять только хранилищем.**
|
||||||
|
> Отвергнуто: пользователей панель в этом случае не показывает вовсе, и одна из
|
||||||
|
> трёх частей вопроса остаётся без ответа навсегда, а не до какой-то задачи.
|
||||||
|
|
||||||
|
Запись попадает в журнал по двум основаниям сразу. **Откат дорогой:** меняется
|
||||||
|
раскладка файлов на диске, а она в `../../CLAUDE.md` названа необратимой.
|
||||||
|
**Пересматривается прежнее решение:** вход через OIDC собирались делать в самом
|
||||||
|
приложении — так это записано в `../architecture.md`, «Открытые вопросы», и так
|
||||||
|
поставлена задача `oidc-login`. Парного статуса «заменено на» прежняя запись не
|
||||||
|
получает: своего ADR у неё нет, решение жило открытым вопросом архитектуры.
|
||||||
|
|
||||||
|
## Последствия
|
||||||
|
|
||||||
|
- `+` панель даёт владельцу править записи, видеть пользователей и слушать сами
|
||||||
|
файлы. Проверено на версии 0.39.10; в библиотечной сборке панель отдаётся по
|
||||||
|
адресу `/_/` того же порта.
|
||||||
|
- `+` база и записи съезжаются под один каталог, и копия сервера накрывает их
|
||||||
|
разом. Своё копирование по расписанию с выгрузкой в S3-совместимое хранилище у
|
||||||
|
PocketBase тоже есть, но копии сервер уже делает своими средствами — берём мы
|
||||||
|
встроенное или нет, здесь не решено.
|
||||||
|
- `+` требование CGO уходит: PocketBase ходит в SQLite через
|
||||||
|
`modernc.org/sqlite`, и пробник собрался при `CGO_ENABLED=0`. Свойство стека в
|
||||||
|
`../../CLAUDE.md` перестаёт быть верным.
|
||||||
|
- `−` раскладка `data/files` меняется необратимо: файл ложится в
|
||||||
|
`pb_data/storage/<коллекция>/<запись>/<имя>_<10 случайных символов>` рядом с
|
||||||
|
файлом атрибутов. Момент перехода назначает человек; данные прежней базы не
|
||||||
|
переносятся по прежнему решению задачи `pocketbase-storage`.
|
||||||
|
- `−` вход перестаёт быть нашим: задача `oidc-login` переписывается с
|
||||||
|
собственной обработки ответа провайдера на настройку провайдера в PocketBase.
|
||||||
|
Что делать с сессией и где она живёт, решает уже не наш код.
|
||||||
|
- `−` появляется секрет, которого не было: пароль суперпользователя панели. Сама
|
||||||
|
PocketBase Authelia к панели не подпускает — ни OIDC, ни второй фактор у
|
||||||
|
коллекции суперпользователей включить не удалось. Своё ограничение по списку
|
||||||
|
адресов у неё есть, но им же можно запереть себя: сброса в наборе команд нет.
|
||||||
|
- `−` панель висит на том же порту, что и приложение, а порт опубликован в
|
||||||
|
интернет через обратный прокси. **Закрывает её контур:** тем же решением адрес
|
||||||
|
`/_/` закрывает Authelia на прокси, пропуская группу администраторов. Приложение
|
||||||
|
тут ни при чём, и задачи в беклоге у этого нет.
|
||||||
+2
-3
@@ -32,9 +32,8 @@
|
|||||||
|
|
||||||
| Дата | Запись | Статус |
|
| Дата | Запись | Статус |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| 2026-08-11 | [Хранилище, файлы и вход переезжают в PocketBase](ADR-2026-08-11-pocketbase-storage-with-admin-panel.md) | |
|
||||||
| 2026-08-11 | [Проверки не зовут внешних программ](ADR-2026-08-11-stub-adapters-in-tests.md) | |
|
| 2026-08-11 | [Проверки не зовут внешних программ](ADR-2026-08-11-stub-adapters-in-tests.md) | |
|
||||||
|
|
||||||
Решения, принятые до заведения канона 2026-08-10, источника в архиве изменений
|
Решения, принятые до заведения канона 2026-08-10, источника в архиве изменений
|
||||||
не имеют — сочинять их задним числом правило запрещает. Ближайшие кандидаты
|
не имеют — сочинять их задним числом правило запрещает.
|
||||||
назовёт первое же изменение, которое тронет хранилище или вход: замена SQLite на
|
|
||||||
PocketBase и вход через OIDC оба подпадают под критерий «дорогой откат».
|
|
||||||
|
|||||||
+17
-5
@@ -119,11 +119,18 @@
|
|||||||
|
|
||||||
## Открытые вопросы
|
## Открытые вопросы
|
||||||
|
|
||||||
- **Хранилище.** Пробуем PocketBase взамен SQLite с goqu и goose. Не решено, чем
|
- **Хранилище.** PocketBase заменяет SQLite с goqu и goose, файлы переезжают в её
|
||||||
становится конвейер задач: таблицей PocketBase с тем же захватом или чем-то
|
раскладку на диске — решено 2026-08-11,
|
||||||
другим. Данные не переносим — начинаем с чистого листа.
|
[ADR](adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md), замер панели
|
||||||
- **Учётные записи.** Вход через OIDC, провайдер — Authelia. Не решено, где
|
в [research/pocketbase.md](research/pocketbase.md). Требование CGO этим
|
||||||
живёт сессия и как связываются пользователь Telegram и пользователь веба.
|
снимается. Не решено, чем становится конвейер задач: таблицей PocketBase с тем
|
||||||
|
же захватом или чем-то другим — разведка `job-queue-choice`. Данные не
|
||||||
|
переносим — начинаем с чистого листа.
|
||||||
|
- **Учётные записи.** Вход через OIDC, провайдер — Authelia, а ответ провайдера
|
||||||
|
обрабатывает PocketBase, а не наш код (тот же ADR). Не решено, где живёт сессия
|
||||||
|
и как связываются пользователь Telegram и пользователь веба. Панель
|
||||||
|
администратора при этом Authelia не закрывает: у неё свой пароль
|
||||||
|
суперпользователя.
|
||||||
- **Приложение.** Экранов нет вовсе, есть только API. Решено делать SPA,
|
- **Приложение.** Экранов нет вовсе, есть только API. Решено делать SPA,
|
||||||
устанавливаемое на телефон; фреймворк выбирает разведка
|
устанавливаемое на телефон; фреймворк выбирает разведка
|
||||||
`spa-framework-choice`, и до её итога
|
`spa-framework-choice`, и до её итога
|
||||||
@@ -149,6 +156,11 @@
|
|||||||
`usage-accounting`.
|
`usage-accounting`.
|
||||||
- **Срок хранения.** Записи и тексты решено хранить бессрочно (паспорт,
|
- **Срок хранения.** Записи и тексты решено хранить бессрочно (паспорт,
|
||||||
2026-08-11), а рост каталога `data/files` ничем не ограничен и не наблюдается.
|
2026-08-11), а рост каталога `data/files` ничем не ограничен и не наблюдается.
|
||||||
|
- **Резервные копии.** Копии делает сервер своими средствами, и приложение о них
|
||||||
|
ничего не знает. После переезда на PocketBase не решено, хватит ли копировать
|
||||||
|
её каталог файлами, или приложению нужна команда выгрузки: база под нагрузкой
|
||||||
|
копируется файлом не всегда целой. Своё копирование по расписанию у PocketBase
|
||||||
|
есть — берём мы его или нет, тоже не решено.
|
||||||
- **Формат для распознавания.** Конвертер отдаёт ogg/vorbis (`libvorbis`), а
|
- **Формат для распознавания.** Конвертер отдаёт ogg/vorbis (`libvorbis`), а
|
||||||
SpeechKit получает `ContainerAudio_OGG_OPUS`. Расхождение не разобрано: то ли
|
SpeechKit получает `ContainerAudio_OGG_OPUS`. Расхождение не разобрано: то ли
|
||||||
сервис определяет содержимое сам, то ли часть записей теряется на этом.
|
сервис определяет содержимое сам, то ли часть записей теряется на этом.
|
||||||
|
|||||||
+8
-4
@@ -58,7 +58,10 @@
|
|||||||
- **Собственные модели.** Не обучаем и не держим у себя ни модель распознавания,
|
- **Собственные модели.** Не обучаем и не держим у себя ни модель распознавания,
|
||||||
ни языковую модель: и речь, и выводы из текста считает внешний сервис.
|
ни языковую модель: и речь, и выводы из текста считает внешний сервис.
|
||||||
- **Управление учётными записями.** Пользователей заводит и проверяет внешний
|
- **Управление учётными записями.** Пользователей заводит и проверяет внешний
|
||||||
провайдер, свою регистрацию и свои пароли не делаем.
|
провайдер, свою регистрацию и свои пароли не делаем. Одно исключение появилось
|
||||||
|
2026-08-11 вместе с решением про PocketBase: в её панель администратора
|
||||||
|
владелец входит своим паролем, потому что закрыть её провайдером она не
|
||||||
|
умеет.
|
||||||
- **Живая расшифровка.** Работаем с готовой записью, поток в реальном времени не
|
- **Живая расшифровка.** Работаем с готовой записью, поток в реальном времени не
|
||||||
обрабатываем.
|
обрабатываем.
|
||||||
- **Диктофон.** Запись звука делает телефон, а приложение принимает готовый
|
- **Диктофон.** Запись звука делает телефон, а приложение принимает готовый
|
||||||
@@ -106,6 +109,7 @@
|
|||||||
которой пользуемся: она и задаёт потолок по длине записи и формату.
|
которой пользуемся: она и задаёт потолок по длине записи и формату.
|
||||||
- **Whisper и его серверные обёртки** — запасной путь, если внешний сервис
|
- **Whisper и его серверные обёртки** — запасной путь, если внешний сервис
|
||||||
перестанет устраивать по цене или по качеству русской речи.
|
перестанет устраивать по цене или по качеству русской речи.
|
||||||
- **PocketBase** — кандидат в хранилище взамен сегодняшнего SQLite. Источником
|
- **PocketBase** — хранилище взамен сегодняшнего SQLite, решено 2026-08-11
|
||||||
учётных записей его не рассматриваем: вход решено делать через OIDC у Authelia
|
([adr](adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md)). Учётные
|
||||||
([architecture.md](architecture.md), «Открытые вопросы»).
|
записи оно хранит и получает от Authelia своим провайдером OIDC, но источником
|
||||||
|
их не становится: заводит и проверяет людей по-прежнему Authelia.
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
|
|
||||||
## Как снималось
|
## Как снималось
|
||||||
|
|
||||||
Ничего не снималось. Записей нет: замеров на живом потоке не делали, поведение
|
На живом потоке не снималось ничего: поведение внешних сервисов на границах не
|
||||||
внешних сервисов на границах не проверяли.
|
проверяли. Единственная запись сделана на пустой базе в песочнице.
|
||||||
|
|
||||||
Внешних источников, о которых разведка нужна, четыре — Telegram Bot API, Yandex
|
Внешних источников, о которых разведка нужна, четыре — Telegram Bot API, Yandex
|
||||||
SpeechKit, Yandex Object Storage и `ffmpeg`. Мерить нужно то, что стоит
|
SpeechKit, Yandex Object Storage и `ffmpeg`. Мерить нужно то, что стоит
|
||||||
@@ -19,4 +19,6 @@ SpeechKit, Yandex Object Storage и `ffmpeg`. Мерить нужно то, чт
|
|||||||
|
|
||||||
## Записи
|
## Записи
|
||||||
|
|
||||||
Записей нет.
|
| Дата | Запись | О чём |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 2026-08-11 | [PocketBase: что даёт панель администратора](pocketbase.md) | Записи, пользователи и файлы в панели версии 0.39.10 |
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# PocketBase: что даёт панель администратора
|
||||||
|
|
||||||
|
Отвечает на вопрос разведки `pocketbase-admin-fit`: что панель показывает и
|
||||||
|
правит по трём частям — записи, пользователи, файлы, — и хватает ли этого, чтобы
|
||||||
|
держать перевод хранилища в планах.
|
||||||
|
|
||||||
|
## Как снималось
|
||||||
|
|
||||||
|
Версия **0.39.10**, выпуск от 2026-07-30 (`./pocketbase --version`). Смотрел на
|
||||||
|
пустой базе в каталоге вне репозитория, боевые данные не участвовали. Прогонов
|
||||||
|
было два:
|
||||||
|
|
||||||
|
- **готовый бинарник** — `pocketbase serve --http=127.0.0.1:8099`, суперпользователь
|
||||||
|
заведён командой `pocketbase superuser create`. Возможности панели снимал её же
|
||||||
|
запросами (`/api/collections`, `/api/logs`, `/api/backups`, `/api/crons`,
|
||||||
|
`/api/settings`) и поиском по её собранному коду;
|
||||||
|
- **своя сборка**, где PocketBase подключён библиотекой к пустому приложению на
|
||||||
|
Go, — так, как предполагает задача `pocketbase-storage`.
|
||||||
|
|
||||||
|
Оба прогона удалены вместе с песочницей.
|
||||||
|
|
||||||
|
## Правка записей — работает целиком
|
||||||
|
|
||||||
|
Панель показывает каждую коллекцию таблицей, отбирает записи своим языком
|
||||||
|
фильтров, сортирует, создаёт, правит и удаляет их по одной. Сверх таблицы в ней
|
||||||
|
есть выгрузка списка в CSV, журнал запросов с временем ответа и кодом, резервные
|
||||||
|
копии с загрузкой и восстановлением, список заданий планировщика.
|
||||||
|
|
||||||
|
Групповой операции над отмеченными записями в панели нет: удаление идёт по
|
||||||
|
одной. Проверял поиском по её коду — строк вида «удалить отмеченное» в нём не
|
||||||
|
нашлось, тогда как «Export as CSV» и «Download JSON» нашлись.
|
||||||
|
|
||||||
|
## Пользователи — только те, кого туда положат
|
||||||
|
|
||||||
|
Панель показывает свою коллекцию пользователей и ничего больше. Отсюда следствие
|
||||||
|
для целевого входа: **пользователи Authelia в панели не появятся, если вход
|
||||||
|
делает само приложение**. Пустая база заводит шесть коллекций, из них одна
|
||||||
|
пользовательская (`users`) и пять служебных, включая `_externalAuths` — связь
|
||||||
|
записи с внешним провайдером.
|
||||||
|
|
||||||
|
Второй путь есть, и он работает: **вход можно отдать самой PocketBase**. У
|
||||||
|
пользовательской коллекции настраивается провайдер `oidc` с произвольными
|
||||||
|
адресами; я включил его на адреса вида `https://auth.example.com/api/oidc/...`,
|
||||||
|
и клиент немедленно стал получать провайдера в списке способов входа. Тогда
|
||||||
|
учётные записи заводятся сами, и панель их видит.
|
||||||
|
|
||||||
|
**В саму панель Authelia не пускает.** Вход суперпользователя — своя почта и свой
|
||||||
|
пароль:
|
||||||
|
|
||||||
|
- включить `oidc` у коллекции суперпользователей не удалось: запрос принимается,
|
||||||
|
но возвращает коллекцию с выключенным `oauth2`;
|
||||||
|
- включить второй фактор у неё же не удалось тоже — ответ `403`.
|
||||||
|
|
||||||
|
Ограничить панель списком адресов можно: настройка `superuserIPs` принимает
|
||||||
|
адреса и подсети. **Ею же можно запереть себя** — после того как я поставил туда
|
||||||
|
чужой адрес, все запросы суперпользователя, включая запрос на сброс настройки,
|
||||||
|
стали отвечать `403`. Команды сброса в наборе нет: он состоит из `migrate`,
|
||||||
|
`superuser`, `update` и `serve`.
|
||||||
|
|
||||||
|
## Файлы — только свои
|
||||||
|
|
||||||
|
Файл живёт полем записи, и раскладку на диске выбирает PocketBase:
|
||||||
|
|
||||||
|
```
|
||||||
|
pb_data/storage/<коллекция>/<запись>/<имя>_<10 случайных символов>.ogg
|
||||||
|
pb_data/storage/<коллекция>/<запись>/<имя>_<10 случайных символов>.ogg.attrs
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверено загрузкой файла в 200 КБ: имя `sample.ogg` превратилось в
|
||||||
|
`sample_uztrv6wvz3.ogg`, рядом лёг файл атрибутов.
|
||||||
|
|
||||||
|
Сегодняшняя раскладка `data/files` с именами-UUID панели не видна. Путь строкой
|
||||||
|
она покажет строкой — прослушать и скачать запись по ней нельзя. Способа сослаться
|
||||||
|
на файл, уже лежащий на диске мимо её каталога, нет.
|
||||||
|
|
||||||
|
Поле помечается защищённым, и тогда файл не отдаётся по прямой ссылке: без токена
|
||||||
|
ответ `404`, с выданным файловым токеном — `200`.
|
||||||
|
|
||||||
|
**Резервные копии накрывают ровно её каталог.** Файлы, оставленные снаружи, в них
|
||||||
|
не попадут — то есть панель и встроенное резервное копирование покупаются одной и
|
||||||
|
той же ценой.
|
||||||
|
|
||||||
|
## Побочное: CGO уходит
|
||||||
|
|
||||||
|
Библиотечная сборка встала при `CGO_ENABLED=0` — PocketBase ходит в SQLite через
|
||||||
|
`modernc.org/sqlite`, а не через `mattn/go-sqlite3`. Требование CGO записано
|
||||||
|
сегодня свойством стека в `../../CLAUDE.md`, и перевод его снимает.
|
||||||
|
|
||||||
|
Бинарник пробника — 33 954 634 байта против 43 498 904 у сегодняшнего приложения
|
||||||
|
(`go build` без флагов). **Числа не сравнимы напрямую:** в пробнике нет ни бота,
|
||||||
|
ни клиента SpeechKit, ни клиента Object Storage. Что даст сборка после перевода,
|
||||||
|
не замерялось.
|
||||||
|
|
||||||
|
Панель отдаётся по адресу `/_/` того же порта, что и остальное приложение, — и в
|
||||||
|
библиотечной сборке тоже: пустое приложение с одним своим обработчиком отвечало
|
||||||
|
на `/_/` кодом `200`.
|
||||||
|
|
||||||
|
## Что отвергнуто и почему
|
||||||
|
|
||||||
|
- **Держать файлы на диске как сейчас, а в базе — путь строкой.** Отвергнуто:
|
||||||
|
панель тогда не даёт по файлам ничего, и встроенные копии их не накрывают.
|
||||||
|
Довод, ради которого перевод затевался, пропадает целиком.
|
||||||
|
- **Оставить вход у приложения, а PocketBase взять только хранилищем.**
|
||||||
|
Отвергнуто: пользователей панель в этом случае не показывает вовсе, и одна из
|
||||||
|
трёх частей вопроса остаётся без ответа навсегда, а не до какой-то задачи.
|
||||||
|
- **Отказаться от перевода.** Отвергнуто человеком на чекпоинте 2026-08-11:
|
||||||
|
вместе с панелью отказ выбрасывал бы уход CGO и встроенное резервное
|
||||||
|
копирование, которых у сервиса-архива нет никаких.
|
||||||
@@ -17,6 +17,17 @@
|
|||||||
сделало сервис архивом. Оба сдвига описаны ниже разделами «Куда
|
сделало сервис архивом. Оба сдвига описаны ниже разделами «Куда
|
||||||
уходит содержимое записи» и «Что вне модели».
|
уходит содержимое записи» и «Что вне модели».
|
||||||
|
|
||||||
|
**Третий сдвиг — панель администратора.** Решением от 2026-08-11
|
||||||
|
([adr](adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md)) хранилищем
|
||||||
|
становится PocketBase, и вместе с ним на том же порту появляется панель по
|
||||||
|
адресу `/_/`: доступ ко всем записям, всем файлам и всем пользователям разом.
|
||||||
|
Порт опубликован в интернет через обратный прокси, а сама PocketBase вход в
|
||||||
|
панель через Authelia не пускает — у неё свой пароль суперпользователя.
|
||||||
|
**Закрывает панель контур, а не приложение:** решением владельца от 2026-08-11
|
||||||
|
адрес `/_/` закрывает Authelia на обратном прокси, пропуская только группу
|
||||||
|
администраторов. Задачи в беклоге у этого нет — работа принадлежит выкладке, а
|
||||||
|
она вне модели («Что вне модели», строка про контур).
|
||||||
|
|
||||||
Отсюда главное следствие, из которого читается всё остальное: **`POST /api/audio`
|
Отсюда главное следствие, из которого читается всё остальное: **`POST /api/audio`
|
||||||
доступен кому угодно из интернета**. Отправитель не назван, не ограничен по числу
|
доступен кому угодно из интернета**. Отправитель не назван, не ограничен по числу
|
||||||
запросов и не ограничен по размеру файла.
|
запросов и не ограничен по размеру файла.
|
||||||
@@ -93,6 +104,14 @@ Telegram отправителю.
|
|||||||
заодно сообщал бы, что запись у кого-то уже есть.
|
заодно сообщал бы, что запись у кого-то уже есть.
|
||||||
- **Файлы фрагментов** (`long-audio-chunking`) ложатся рядом с исходным в тот же
|
- **Файлы фрагментов** (`long-audio-chunking`) ложатся рядом с исходным в тот же
|
||||||
плоский каталог — раскладка `data/files` меняется, и это необратимо.
|
плоский каталог — раскладка `data/files` меняется, и это необратимо.
|
||||||
|
- **Раскладку выбирает PocketBase** (`pocketbase-storage`), и плоского каталога
|
||||||
|
не остаётся вовсе: файл ложится в
|
||||||
|
`pb_data/storage/<коллекция>/<запись>/<имя>_<10 случайных символов>` рядом с
|
||||||
|
файлом атрибутов. Имя, данное отправителем, в путь при этом попадает — сегодня
|
||||||
|
от него берётся только расширение. Файл уходит не с диска напрямую, а по ссылке
|
||||||
|
вида `/api/files/<коллекция>/<запись>/<имя>`; закрытым он становится, только если
|
||||||
|
поле помечено защищённым, и тогда нужен отдельный файловый токен. Замер —
|
||||||
|
[research/pocketbase.md](research/pocketbase.md).
|
||||||
- **Имя отправляемого документа** (`long-text-delivery`) собирается из
|
- **Имя отправляемого документа** (`long-text-delivery`) собирается из
|
||||||
идентификатора задачи: имя, данное пользователем, в него не попадает.
|
идентификатора задачи: имя, данное пользователем, в него не попадает.
|
||||||
|
|
||||||
@@ -126,6 +145,14 @@ Telegram отправителю.
|
|||||||
Откуда он берётся — из группы OIDC или из конфигурации — не решено
|
Откуда он берётся — из группы OIDC или из конфигурации — не решено
|
||||||
(`admin-stats-screen`).
|
(`admin-stats-screen`).
|
||||||
|
|
||||||
|
**Панель администратора в эту таблицу не входит и разграничению не подчиняется.**
|
||||||
|
Суперпользователь PocketBase видит все записи, все файлы и всех пользователей
|
||||||
|
мимо любого из четырёх механизмов, а пускает его свой пароль, а не Authelia.
|
||||||
|
Замер показал, что закрыть панель провайдером OIDC или вторым фактором нельзя:
|
||||||
|
обе настройки у коллекции суперпользователей отклоняются. Остаётся ограничение
|
||||||
|
по списку адресов (`superuserIPs`), и оно же запирает владельца, если список
|
||||||
|
задан неверно: сброса в наборе команд нет.
|
||||||
|
|
||||||
## Что чувствительнее чего
|
## Что чувствительнее чего
|
||||||
|
|
||||||
1. **Содержимое записей и расшифровок.** Голосовые сообщения — личная переписка;
|
1. **Содержимое записей и расшифровок.** Голосовые сообщения — личная переписка;
|
||||||
@@ -153,6 +180,10 @@ Telegram отправителю.
|
|||||||
5. **Статистика потребления** (`usage-accounting`). Текста записей не содержит,
|
5. **Статистика потребления** (`usage-accounting`). Текста записей не содержит,
|
||||||
но говорит, кто и когда пользовался сервисом и сколько; страница расхода
|
но говорит, кто и когда пользовался сервисом и сколько; страница расхода
|
||||||
открыта только владельцу.
|
открыта только владельцу.
|
||||||
|
6. **Пароль суперпользователя панели** (`pocketbase-storage`). Открывает все
|
||||||
|
записи, все файлы и всех пользователей разом, то есть стоит вровень с самым
|
||||||
|
чувствительным из списка выше. Второй секрет после токенов пользователей,
|
||||||
|
который лежит **не в конфигурации**: его отпечаток хранит сама база.
|
||||||
|
|
||||||
Тексты расшифровок в логи не пишутся — логируется длина текста и
|
Тексты расшифровок в логи не пишутся — логируется длина текста и
|
||||||
идентификаторы. **Имя файла, данное отправителем, пишется**: строка
|
идентификаторы. **Имя файла, данное отправителем, пишется**: строка
|
||||||
|
|||||||
+2
-2
@@ -25,8 +25,8 @@
|
|||||||
- [🧹 Задать таймауты обращениям к внешним сервисам](items/external-call-timeouts.md) — Ни у Telegram, ни у Object Storage, ни у SpeechKit нет таймаута: молчащий собеседник держит шаг конвейера до истечения часового захвата.
|
- [🧹 Задать таймауты обращениям к внешним сервисам](items/external-call-timeouts.md) — Ни у Telegram, ни у Object Storage, ни у SpeechKit нет таймаута: молчащий собеседник держит шаг конвейера до истечения часового захвата.
|
||||||
- [🔬 Админка PocketBase: данные, пользователи и файлы](items/pocketbase-admin-fit.md) — Переход на PocketBase решён ради его панели администратора, а что она даёт по данным, пользователям и файлам на диске — не проверено.
|
- [🔬 Админка PocketBase: данные, пользователи и файлы](items/pocketbase-admin-fit.md) — Переход на PocketBase решён ради его панели администратора, а что она даёт по данным, пользователям и файлам на диске — не проверено.
|
||||||
- [🔬 Очередь задач: своя таблица или готовая библиотека](items/job-queue-choice.md) — Очередь написана вручную: захват двумя запросами без транзакции, протухание временем, опрос раз в секунду вхолостую тремя воркерами.
|
- [🔬 Очередь задач: своя таблица или готовая библиотека](items/job-queue-choice.md) — Очередь написана вручную: захват двумя запросами без транзакции, протухание временем, опрос раз в секунду вхолостую тремя воркерами.
|
||||||
- [🧹 Перевести хранилище на встроенный PocketBase](items/pocketbase-storage.md) — Из трёх доводов за перевод остался один: учётные записи ушли к Authelia, страницу статистики рисует само приложение, а панель администратора не проверена.
|
- [🧹 Перевести хранилище и файлы записей на встроенный PocketBase](items/pocketbase-storage.md) — Записи, метаданные и файлы лежат порознь, и владелец не видит их ничем: панель PocketBase покажет и то и другое, только если они переедут к ней.
|
||||||
- [✨ Пускать в приложение только после входа через OIDC](items/oidc-login.md) — HTTP API открыт наружу без аутентификации: любой из интернета заводит задачи за наши деньги и читает чужие расшифровки по идентификатору.
|
- [✨ Пускать в приложение только после входа через OIDC](items/oidc-login.md) — HTTP API открыт наружу без аутентификации: любой из интернета заводит задачи за наши деньги и читает чужие расшифровки по идентификатору. Ответ провайдера разбирает PocketBase, а не наш код.
|
||||||
- [✨ Привязать запись к владельцу и отдавать только свои](items/record-ownership.md) — У задачи и файла нет владельца, поэтому знание UUID задачи и есть право её читать.
|
- [✨ Привязать запись к владельцу и отдавать только свои](items/record-ownership.md) — У задачи и файла нет владельца, поэтому знание UUID задачи и есть право её читать.
|
||||||
- [✨ Сопоставить пользователя Telegram с учётной записью](items/telegram-account-link.md) — Белый список сверяется с именем пользователя Telegram, которое владелец меняет в любой момент, а записи из бота ни с кем не связаны.
|
- [✨ Сопоставить пользователя Telegram с учётной записью](items/telegram-account-link.md) — Белый список сверяется с именем пользователя Telegram, которое владелец меняет в любой момент, а записи из бота ни с кем не связаны.
|
||||||
- [✨ Свести приём и чтение записей к одному контракту для приложения](items/json-api-for-spa.md) — Сегодняшний API отвечает 404 на любую ошибку чтения и 500 на любую ошибку приёма: строить на нём экраны нечем.
|
- [✨ Свести приём и чтение записей к одному контракту для приложения](items/json-api-for-spa.md) — Сегодняшний API отвечает 404 на любую ошибку чтения и 500 на любую ошибку приёма: строить на нём экраны нечем.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
- **Тип:** feature
|
- **Тип:** feature
|
||||||
- **Категория:** Очередь
|
- **Категория:** Очередь
|
||||||
- **Зачем:** HTTP API открыт наружу без аутентификации: любой из интернета заводит задачи за наши деньги и читает чужие расшифровки по идентификатору.
|
- **Зачем:** HTTP API открыт наружу без аутентификации: любой из интернета заводит задачи за наши деньги и читает чужие расшифровки по идентификатору. Ответ провайдера разбирает PocketBase, а не наш код.
|
||||||
- **Теги:** goal:multi-user, question
|
- **Теги:** goal:multi-user, question
|
||||||
|
|
||||||
Двигает пункт 1 «Завершения» цели: неаутентифицированный запрос к записям не
|
Двигает пункт 1 «Завершения» цели: неаутентифицированный запрос к записям не
|
||||||
@@ -13,12 +13,22 @@
|
|||||||
здесь ещё нет: после входа видно всё, что видно сейчас, — этим занимается
|
здесь ещё нет: после входа видно всё, что видно сейчас, — этим занимается
|
||||||
`record-ownership`.
|
`record-ownership`.
|
||||||
|
|
||||||
|
**Ответ провайдера разбирает PocketBase, а не наш код** — решено 2026-08-11
|
||||||
|
([adr](../../docs/adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md)).
|
||||||
|
У её коллекции пользователей настраивается провайдер `oidc` с адресами Authelia,
|
||||||
|
и учётные записи заводятся сами; проверено на версии 0.39.10,
|
||||||
|
[docs/research/pocketbase.md](../../docs/research/pocketbase.md). Отсюда порядок:
|
||||||
|
задача идёт после `pocketbase-storage`, до неё настраивать нечего.
|
||||||
|
|
||||||
## Затрагивает
|
## Затрагивает
|
||||||
|
|
||||||
- новые эндпоинты входа и выхода, обработка ответа провайдера;
|
- настройка провайдера `oidc` у коллекции пользователей PocketBase: адреса
|
||||||
|
Authelia, идентификатор клиента, секрет, соответствие полей учётной записи;
|
||||||
|
- эндпоинты входа и выхода, которые PocketBase приносит своими;
|
||||||
- `POST /api/audio` и `GET /api/status/:id` — оба уходят за аутентификацию;
|
- `POST /api/audio` и `GET /api/status/:id` — оба уходят за аутентификацию;
|
||||||
- `GET /health` и `GET /metrics` — решить и записать, остаются ли открытыми;
|
- `GET /health` и `GET /metrics` — решить и записать, остаются ли открытыми;
|
||||||
- хранение сессии: таблица или подписанная кука;
|
- хранение сессии: её ведёт PocketBase, и решить надо, чем она предъявляется
|
||||||
|
приложению;
|
||||||
- секция конфигурации под провайдера: адрес, идентификатор клиента, секрет;
|
- секция конфигурации под провайдера: адрес, идентификатор клиента, секрет;
|
||||||
- `docs/security.md` — периметр меняется, и первая его строка перестаёт быть
|
- `docs/security.md` — периметр меняется, и первая его строка перестаёт быть
|
||||||
верной;
|
верной;
|
||||||
@@ -42,7 +52,9 @@
|
|||||||
|
|
||||||
Владельца у записи здесь не заводим и выборку не сужаем: после входа видно
|
Владельца у записи здесь не заводим и выборку не сужаем: после входа видно
|
||||||
столько же, сколько сейчас. Инвариант «бот отвечает только тем, кто в белом
|
столько же, сколько сейчас. Инвариант «бот отвечает только тем, кто в белом
|
||||||
списке» не трогаем — он живёт до `telegram-account-link`.
|
списке» не трогаем — он живёт до `telegram-account-link`. Панель администратора
|
||||||
|
тоже не трогаем: наружу её закрывает Authelia на обратном прокси, а это работа
|
||||||
|
выкладки.
|
||||||
|
|
||||||
## Вопросы
|
## Вопросы
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
# 🧹 Перевести хранилище на встроенный PocketBase
|
# 🧹 Перевести хранилище и файлы записей на встроенный PocketBase
|
||||||
|
|
||||||
- **Тип:** chore
|
- **Тип:** chore
|
||||||
- **Категория:** Очередь
|
- **Категория:** Очередь
|
||||||
- **Зачем:** Из трёх доводов за перевод остался один: учётные записи ушли к Authelia, страницу статистики рисует само приложение, а панель администратора не проверена.
|
- **Зачем:** Записи, метаданные и файлы лежат порознь, и владелец не видит их ничем: панель PocketBase покажет и то и другое, только если они переедут к ней.
|
||||||
|
|
||||||
PocketBase встраивается библиотекой в тот же бинарник и приносит хранилище,
|
PocketBase встраивается библиотекой в тот же бинарник и приносит хранилище,
|
||||||
учётные записи и панель администратора разом. Наблюдаемое поведение сервиса
|
учётные записи и панель администратора разом. Конвейер и оба входа работают
|
||||||
после перевода не меняется: те же два входа, тот же конвейер, тот же текст на
|
по-прежнему, а снаружи прибавляется панель по адресу `/_/`.
|
||||||
выходе.
|
|
||||||
|
|
||||||
Из трёх доводов 2026-08-11 осталось полтора: учётные записи заводит Authelia по
|
Разведка `pocketbase-admin-fit` довод проверила, и 2026-08-11 принято решение
|
||||||
OIDC, страницу статистики рисует само приложение. Довод про панель
|
[перевести хранилище вместе с файлами](../../docs/adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md):
|
||||||
администратора проверяет разведка `pocketbase-admin-fit`, и её ответ решает,
|
панель показывает файлы и пользователей только тех, что лежат у неё, а половина
|
||||||
берётся эта задача или уходит в `REJECTED.md`.
|
перевода довода не окупает. Замер — [docs/research/pocketbase.md](../../docs/research/pocketbase.md).
|
||||||
|
|
||||||
|
**Файлы переезжают в раскладку PocketBase**, а плоского каталога `data/files` с
|
||||||
|
именами-UUID не остаётся. Это необратимо, и момент перехода назначает человек.
|
||||||
|
Вход в этой задаче не трогаем: его переводит `oidc-login`.
|
||||||
|
|
||||||
Данные не переносим — база заводится с чистого листа, и это решение принято
|
Данные не переносим — база заводится с чистого листа, и это решение принято
|
||||||
сознательно.
|
сознательно.
|
||||||
@@ -24,10 +27,20 @@ OIDC, страницу статистики рисует само приложе
|
|||||||
- `internal/adapter/repo/sqlite` целиком, включая захват задачи через
|
- `internal/adapter/repo/sqlite` целиком, включая захват задачи через
|
||||||
`FindAndAcquire`;
|
`FindAndAcquire`;
|
||||||
- `internal/contract`, интерфейсы `FileRepository` и `TranscriptJobRepository`;
|
- `internal/contract`, интерфейсы `FileRepository` и `TranscriptJobRepository`;
|
||||||
- ключ конфигурации `[database] path` и раскладка каталога `data/`;
|
- ключ конфигурации `[database] path`, ключ `[storage] path` и раскладка
|
||||||
|
каталога `data/`;
|
||||||
|
- **формат файла на диске:** запись переезжает в поле коллекции, путь становится
|
||||||
|
`pb_data/storage/<коллекция>/<запись>/<имя>_<10 случайных символов>` рядом с
|
||||||
|
файлом атрибутов. Имя, данное отправителем, впервые попадает в путь целиком;
|
||||||
|
- отдача файла: вместо чтения с диска — ссылка `/api/files/...`, и решение,
|
||||||
|
помечать ли поле защищённым;
|
||||||
- сборка образа: PocketBase тянет свой набор зависимостей, а `mattn/go-sqlite3`
|
- сборка образа: PocketBase тянет свой набор зависимостей, а `mattn/go-sqlite3`
|
||||||
с его требованием CGO может уйти;
|
с его требованием CGO уходит — библиотека ходит в SQLite через
|
||||||
- `docs/database.md` — схема, представление данных и таблица настроек.
|
`modernc.org/sqlite`;
|
||||||
|
- пароль суперпользователя панели: где заводится и как попадает на сервер;
|
||||||
|
- `docs/database.md` — схема, представление данных и таблица настроек;
|
||||||
|
- `CLAUDE.md` — строка стека про CGO и раздел про запреты с путями;
|
||||||
|
- `docs/security.md` — раздел «Из чего строятся пути и ключи».
|
||||||
|
|
||||||
## Критерии приёмки
|
## Критерии приёмки
|
||||||
|
|
||||||
@@ -39,6 +52,9 @@ OIDC, страницу статистики рисует само приложе
|
|||||||
получает запись.
|
получает запись.
|
||||||
- Задача, брошенная на середине, достаётся снова по истечении срока захвата.
|
- Задача, брошенная на середине, достаётся снова по истечении срока захвата.
|
||||||
Оракул — тест с проставленным задним числом `acquire_time`.
|
Оракул — тест с проставленным задним числом `acquire_time`.
|
||||||
|
- Принятая запись видна в панели строкой и скачивается из неё файлом. Оракул —
|
||||||
|
прогон записи через `POST /api/audio` на пустом каталоге, затем запрос
|
||||||
|
`/api/files/...` за тем же файлом: длина совпадает с загруженной.
|
||||||
- `docs/database.md` описывает новую схему, а старые упоминания goose и goqu из
|
- `docs/database.md` описывает новую схему, а старые упоминания goose и goqu из
|
||||||
документов канона убраны. Оракул — `task gate`, шаг `docs.py check`.
|
документов канона убраны. Оракул — `task gate`, шаг `docs.py check`.
|
||||||
|
|
||||||
@@ -46,4 +62,5 @@ OIDC, страницу статистики рисует само приложе
|
|||||||
|
|
||||||
Данные прежней базы не переносим и не пытаемся сохранить; выкладку не запускаем;
|
Данные прежней базы не переносим и не пытаемся сохранить; выкладку не запускаем;
|
||||||
смена формата хранения на сервере необратима, и момент перехода назначает
|
смена формата хранения на сервере необратима, и момент перехода назначает
|
||||||
человек.
|
человек. Вход не трогаем — он на `oidc-login`. Панель наружу закрывает Authelia
|
||||||
|
на обратном прокси: это работа выкладки, а не приложения.
|
||||||
|
|||||||
Reference in New Issue
Block a user