# Контекст экосистемы для ИИ-агента Точка входа для агента, работающего над задачами, которые выходят за пределы одного репозитория. Здесь то, чего **не видно из кода `soniks-client`**: устройство соседних проектов, контракты между ними и подтверждённые дефекты на их стороне. Что здесь **не** дублируется: устройство самого клиента — оно в [`docs/development/`](../development/architecture.md); правила работы с этим репозиторием — они в `CLAUDE.md`; план работ — он в [](../roadmap-network.md). ## Как этим пользоваться Читать целиком, если задача касается портала, флоуграфов, обновления станций или приёма данных. Пропустить, если правите изолированный кусок клиента. **Все факты ниже проверены чтением кода** в даты и на коммитах из таблицы «Снимок». Соседние репозитории живут своей жизнью и коммитятся другими людьми — **прежде чем опираться на конкретный номер строки, перечитайте это место.** Утверждения о поведении держатся дольше, чем координаты. ## Карта экосистемы Шесть репозиториев, все в одном родительском каталоге: | Репозиторий | Что | Реальное состояние | |---|---|---| | `soniks-client` | клиент станции, Python 3.11 | этот репозиторий; до 2026-09-14 — `soniks/soniks-client-new`, старые URL редиректятся | | `soniks-flowgraphs` | флоуграфы GNU Radio + `flowgraph_dispatcher` | форк `satnogs-flowgraphs`. Своя `docs/ai/context.md` — читать её при работе там. С 2026-09-12 приёмный тракт до демодулятора — один на все графы: источник истины `generic/fm.grc`, раскладка `tools/sync_frontend.py`, `hierarchical/` удалён | | `gr-soniks` | форк `gr-satnogs`, C++ OOT-модуль | **чистое зеркало: `soniks` совпадает с `master` коммит-в-коммит, ноль своих правок.** `project(gr-satnogs)` и namespace `satnogs` сохранены, поэтому `.grc` менять не придётся. **Подключён к сборке 2026-08-24**: `Dockerfile` клонирует его по тегу `v3.1.0.1`, а не апстрим LSF. Читается анонимно по HTTPS; имена `.deb` (`gr-satnogs`, `libgnuradio-satnogs`) от апстримовых не отличаются | | `soniks-network` | портал, Python 3.12 / Django 5.2 LTS (с 2026-09-02) | форк `satnogs-network`. Один активный разработчик. Своя `docs/ai/context.md` — читать её при работе там; её инвариант 7 — это решение 21 дорожной карты | | `soniks-monitor` | монитор станции, 1357 строк | студенческий, в **личном** репозитории `BUSH222/soniks-monitor`. Серверной части под него не существует | | `soniks-agent` | агент обновлений станции, Python 3.11 | **написан 2026-09-11** (агент v1, решения 38–44 и 60–66 дорожной карты): сервис того же compose-проекта, что клиент; читает `state.release`, пишет digest в `docker-compose.override.yml`, L1/L2, откат, ключ `agent` в `status/`. Свой образ, свой `README.md`. На 2026-09-11 без git и без опубликованного образа — блок в шаблонах compose клиента закомментирован | ### Границы работы Правило `CLAUDE.md` — работать только внутри корня репозитория — остаётся в силе по умолчанию. Чтение соседних репозиториев допустимо **только когда человек явно дал доступ в текущей сессии**. Записывать в них — только по отдельной просьбе. Корневой `.env` — живая станция мейнтейнера с рабочим токеном. Не читать, не логировать. Шаблон для правок — `client/.env`. ## Портал: то, что нужно знать клиенту Стек (с 2026-09-02): Python 3.12, Django 5.2 LTS, DRF 3.18, Celery 5.2, PostgreSQL 15, Redis 7.4, Debian bookworm. **WSGI, gunicorn.** Ни channels, ни WebSocket в самом Django. Push к станции — с 2026-09-10 через **MQTT**: рядом с порталом стоит Mosquitto (`mosquitto-go-auth`, профиль compose `mqtt`), nginx отдаёт его как `wss://sonik.space/mqtt`; портал публикует документ состояния станции при сохранении формы, мост `manage.py mqtt_bridge` пишет статусы станций в БД. REST остаётся источником истины и фолбэком (решение 47 дорожной карты). Артефакты наблюдений лежат на файловой системе (`MEDIA_ROOT`), S3-флаги по умолчанию выключены. `bin/djangoctl.sh` запускает gunicorn с `nproc*2+1` воркерами и таймаутом 600 с (`GUNICORN_WORKERS`, `GUNICORN_TIMEOUT` в `.env` сервера переопределяют). ~~Реальный скрипт в продакшне подменяется из CI~~ — с 2026-08-27 не подменяется, а с 2026-09-02 образ портала собирается в CI и выкатка тянет его из registry GitLab: репозиторная версия и есть прод. ### Пять запросов, которыми живёт станция | Запрос | Что делает | |---|---| | `GET /api/jobs/?ground_station=&lat=&lon=&alt=&capabilities=…` | расписание. **Он же единственный heartbeat**: `last_seen` пишется только здесь, и только если `ground_station.owner == request.user`. Состояние станции на портале **не хранится** — выводится из `last_seen` (порог `STATION_HEARTBEAT_TIME`, 60 мин), `is_available` и `testing` при каждом обращении | | `PUT /api/observations//` с `client_version`, `client_metadata` | метаданные прохода | | `PUT /api/observations//` multipart | один артефакт за запрос: `payload`, `waterfall` или `demoddata`. Кадр с меткой дальше 5 минут от окна наблюдения — `400 frame_outside_window`, навсегда | | `GET /api/v2/bundles/latest/` | sat-data bundle, анонимно (`src/soniks_client/sat_data.py`, с 2026-08-28) | | `POST /api/v2/stations//status/` | с 2026-09-10: `{"modes": [...], "satellites": [...], "client_version": ..., "config": {...}, "sdr": {...}, "calibration": {...}}` — режимы из `MODES`, версия, итог применения конфигурации с портала и фактические значения (`api.status_body()`); с 2026-09-11 — найденные приёмники с возможностями и итог калибровки усиления (`src/soniks_client/sdr_survey.py`), оба ключа только после обследования. Портал **сливает** документ по ключам верхнего уровня в `Station.reported_status` (решение 53) и не планирует станции режим вне списка; станция без `modes` умеет всё (правило 21). Клиент шлёт при каждом изменении документа, после сверки. После успешной публикации неизвестный режим клиент отказывает, а не подменяет на FM (решение 45). Тем же путём с 2026-09-11 пишет **soniks-agent** — только свой ключ `agent` (текущий и предыдущий digest, режим, доступный образ, итог последнего применения); ключ, не объявленный в `StationReportedStatusSerializer` портала, отбрасывается молча | | `GET /api/v2/stations//state/` | с 2026-09-10: документ состояния — `generation`, `config` (плоский словарь с именами переменных окружения), `location`, `release`; с 2026-09-11 — `commands` (`rescan_sdr`, `calibrate_gain` с `at` и `frequencies`; выполняется раз на отметку `at`, только вне прохода). `ETag`/`If-None-Match`, 304. Забирается на старте и на каждой сверке (`src/soniks_client/remote_config.py`); тот же документ приходит по MQTT (`stations//state`, retained) в момент сохранения формы | | MQTT `wss://sonik.space/mqtt` | с 2026-09-10 (`src/soniks_client/mqtt.py`): логин `station-`, пароль — токен владельца; подписка `stations//state`, публикация `stations//status` (тот же документ, что POST) и `stations//online` (retained `1`, Last Will `0`). Необязателен: без брокера всё то же самое раз в минуту через REST | Клиентская сторона — `src/soniks_client/api.py` (расписание, статус, выгрузка), `src/soniks_client/remote_config.py` (состояние, через сессию `api`), `src/soniks_client/mqtt.py` (брокер) и `src/soniks_client/sat_data.py` (bundle, своя `requests.Session`); других сетевых вызовов в `src/` нет. **Что отдаёт `/api/jobs/`, описано машинно.** С 2026-09-02 портал держит `openapi.yml` в `soniks-network-api-client/` и сверяет его с кодом в CI на каждом пуше. Для вопроса «какие поля и какого типа приходят в задании» это первоисточник; читать сериализатор нужно только чтобы понять, *почему*. ### Подтверждённые дефекты на стороне портала Проверено чтением кода. Влияют на клиент напрямую, поэтому перечислены здесь, а не только в роадмапе. ~~**`rate_observation` считает Good любой кадр.**~~ **Закрыто 2026-08-24** (Фаза 0.16). Было: `observation.transmitter.downlink_mode not in ["CW", "FM"]` сравнивало `ForeignKey` со строками и было истинно всегда, любой кадр давал `status = 100`. Теперь оценка читает строку `Observation.transmitter_mode` — снимок передатчика на момент планирования (с 2026-08-28 наблюдение хранит такие снимки для всех `transmitter_*` и `station_*`). История оценок на проде **не пересчитана и не будет** — пересчёт на паузе решением мейнтейнера 2026-09-10. Значит `success_rate` за период до починки завышен, и это принято: любой вывод об успешности наблюдений строить на данных после выкатки правки портала. **`GET /api/jobs/` открыт анонимно** — по умолчанию. `JobView.get_permissions()` отдаёт `AllowAny` при `JOBS_ALLOW_ANONYMOUS=True` (умолчание) и `IsAuthenticated` при `False`; каждый анонимный запрос с `ground_station` пишется в лог предупреждением — это счётчик готовности парка к переключению. Расписание всей сети читается кем угодно без токена, пока флаг не переключён. ```{warning} Отсюда раньше делался вывод «станция с протухшим токеном продолжит получать задания, но будет числиться Offline». Он неверен. Клиент шлёт `Authorization: Token …` (`src/soniks_client/api.py:40`), а DRF `TokenAuthentication` на невалидный токен отвечает 401 **до** проверки прав, независимо от `permission_classes`. Такая станция ослепла уже сегодня. Анонимный доступ — про третьих лиц, а не про парк. ``` **Токен пользовательский, а не станционный.** Создаётся сигналом `post_save` на `User` (`network/users/models.py`). Один токен на все станции владельца; ротация из UI молча убивает heartbeat всего парка (с 2026-08-29 она хотя бы только по `POST` — до этого её вызывал `GET`, то есть любая чужая страница). Класс `ClientIDAuthentication` (`network/api/authentication.py`) написан для станционной аутентификации и **никуда не подключён** — на 2026-09-10 тоже; почему это не одна строка, разобрано в дорожной карте (Фаза 1, «Здоровье и идентичность»). **Справочник режимов пополняется автоматически** из фида SatNOGS DB (`network/base/tasks.py:489`) и ни с чем не сверяется. Портал может запланировать режим, которого нет в таблице клиента, — с 2026-09-10 только станции, которая режимы не объявила (решение 31); объявившая такой проход отказывает (решение 45). ~~**Идемпотентность загрузки — по имени файла, а не по содержимому.**~~ **Закрыто 2026-08-27** (`soniks-network@b2f64840`): дубликат разбирается по SHA-256 принятого тела. Те же байты — 200 без записи в хранилище, другие — перезапись оборвавшейся загрузки. Поля `Observation.payload_sha256`, `Observation.waterfall_sha256`, `DemodData.sha256`. **Клиент в этом не участвует и участвовать не должен**: хеш считает портал (`_artifact_sha256` в `network/api/views.py`), станции знать о нём незачем. Пустой хеш означает строку, записанную до миграции `0035` — там сохраняется прежнее поведение: разбор по имени файла и 403 с подстрокой `has already been uploaded`, на которой стоит `api.py` во всём парке. Опечатку «Watefall» исправили в Фазе 0.20, подстроку — сохранили дословно. ~~**Timestamp разбирается из имени файла** `demoddata` через `split("_")[2]` без обработки исключений — кривое имя даёт 500.~~ **Закрыто 2026-08-24** (Фаза 0.19): разбор в `_frame_datetime()`, контракт имени — в одной функции `demoddata_path()`, кривое имя даёт `400` с кодом `malformed_filename`. **Кадр вне окна наблюдения портал отвергает — с 2026-08-28.** Метка из имени кадра обязана попадать в `[start − T, end + T]`, где `T` — `DEMODDATA_TIME_TOLERANCE_MINUTES` (5 минут по умолчанию); иначе `400` с кодом `frame_outside_window`. Допуск — на уход часов станции, не на кадр из другого прохода. Метку ставит диспетчер по часам станции, поэтому станция с часами, ушедшими дальше пяти минут, получит `400` **на каждый кадр**. С 2026-09-10 клиент считает любой `400` окончательным (`FileRejectedError`), по той же схеме, что 404: первый отказ — файл в `incomplete/`, повторный — файл закрывается как выгруженный (решение 24 дорожной карты). Уход часов станции виден в `/healthz` полем `clock_skew_seconds` — клиент сверяет часы с заголовком `Date` ответа `/api/jobs/` (решение 25). ~~**«Конфигурация станции» на странице станции — бутафория.**~~ **Закрыто 2026-09-10**: четыре поля `satnogs_*` удалены, вместо них `Station.desired_config` (плоский словарь, тот же словарь имён, что `MANAGED_FIELDS` клиента и `STATION_CONFIG_FIELDS` портала — `network/base/station_config.py`), `config_generation`, форма `stations//config/`. Заготовки `get_default_station_configuration*` остаются: на них ссылается миграция 0005. **Список наблюдений ограничен по частоте — с 2026-08-29.** `GET /api/observations/` отдаёт 60 запросов в час анониму и 240 с токеном (`OBSERVATIONS_LIST_*_THROTTLE_RATE`); карточка, планирование и выгрузка артефактов не ограничены, станция лимита не видит. Видит его `tools/portal_stats.py`: он листает список постранично до `PAGE_LIMIT` (200) страниц, и два прогона в час подряд упрутся в 429. Запускать с токеном и не чаще раза в час на большой выборке. ## Контракт клиент ↔ диспетчер `src/soniks_client/flowgraph.py` строит командную строку механически: `--{key}={value}` из словаря `self.parameters`, `None` не передаётся. Имя поля настроек и имя аргумента связаны **вручную** (`RX_SAMP_RATE` → `--samp-rate-rx`). Диспетчер (`flowgraph_dispatcher.py`) вызывает `parse_args()`, **не** `parse_known_args()`. Значит любой аргумент, которого он не знает, — это выход с кодом 2 **до** запуска графа и подписчиков. `Popen` при этом успешен, клиент через секунду видит мёртвый процесс и штатно закрывает наблюдение. **Симптом рассинхронизации версий: наблюдение «успешно», данных нет вообще — ни кадров, ни аудио, ни водопада.** ### Состояние контракта, проверено на стенде 2026-08-24 Дифф «что клиент шлёт» против `flowgraph_dispatcher --help`: | Аргумент | Состояние | |---|---| | `--norad-cat-id` | клиент шлёт, диспетчер **не знает**. Не стреляет: портал не отдаёт `norad_cat_id` (см. ниже), значит значение всегда `None` и аргумент не добавляется | | `--lo-transverter` | клиент шлёт, диспетчер **не знает**. Не стреляет: `FLOWGRAPH__LO_TRANSVERTER` по умолчанию `None`. Задать его на станции — заглушить её насмерть и тихо | | `--dev-args` | до 2026-09-12 принимался, форвардился и терялся: `soapy_source` во всех графах держал `dev_args: ''`. Теперь доезжает до устройства. `--doppler-correction-per-sec` — наоборот: параметр снят из графов, диспетчер его принимает и обнуляет | | `--framing` | диспетчер принимает, клиент не шлёт никогда | | остальные 27 | совпадают | **Портал отдаёт `norad_cat_id` только по опт-ину** (с 2026-08-27). Без параметра `capabilities` в ответе `/api/jobs/` тех же одиннадцать полей, что и раньше: `id`, `start`, `end`, `ground_station`, `tle0..2`, `frequency`, `mode`, `transmitter`, `baud` — и `JobData.from_dict` получает через `.get()` свой `None`. С `capabilities=norad_cat_id` добавляется двенадцатое поле. С 2026-09-07 опт-ин — список из трёх имён (`JobSerializer.OPT_IN_FIELDS`): `norad_cat_id`, `transmitter_parameters` (снимок `Transmitter.params` на момент планирования — материал Фазы 3.2, выбор декодера по параметрам передатчика, а не по одному `mode`) и `max_altitude` (максимальная высота прохода). Клиент объявляет **только `norad_cat_id`**; остальные два появятся в `CAPABILITIES`, когда у них будет потребитель в клиенте, не раньше. В `/api/observations//` поле `norad_cat_id` было всегда — данных порталу хватало и раньше, не хватало поля в `JobSerializer`. Заодно проверено: этот эндпоинт открыт анонимно и возвращает `client_metadata` строкой, внутри которой `signal.snr_db` — тоже строка вида `"13.8 dB"` (так форматирует `Waterfall.get_signal_metadata()`). На этом стоит проверка SNR в вердикте стенда. `--norad-cat-id` в клиенте — **задел, а не оплошность**: gr-satellites выбирает декодер по NORAD через satyaml, и без этого аргумента функционал не включить. Все три звена контракта готовы: клиент шлёт, диспетчер принимает (в исходниках `soniks-flowgraphs`, до станции доезжает только пересобранным образом), `JobSerializer` отдаёт по опт-ину. Практический вывод: **оба неизвестных аргумента латентны**, сеть не пишет пустоту на каждом проходе. Но безусловное добавление `norad_cat_id` в `JobSerializer` глушит парк одномоментно и без единой ошибки в логе. Диспетчер научился 2026-08-24: `parse_known_args()` плюс явные `--norad-cat-id` и `--lo-transverter`, которые принимаются и обнуляются. Правка написана заново — `origin/soniks-update` мёржу не подлежит (решение 19 в дорожной карте). ```{warning} До 2026-08-27 отсюда следовало: «предусловие для портала снимает не коммит, а пересобранный образ». **Утверждение снято решением 21** дорожной карты: парк не обновится целиком никогда, значит выкатка гейтом быть не может. Опасная комбинация — клиент шлёт `--norad-cat-id`, диспетчер того же образа его не знает — стоит на станциях уже сейчас: аргумент появился в клиенте в `2.2.0` (1 июля), образ парк получил 19 июля. Станции на `1.x` аргумент не шлют и безопасны. Гейт вместо выкатки — **опт-ин**: клиент объявляет `capabilities` в `GET /api/jobs/` (`CAPABILITIES` в `src/soniks_client/api.py`), портал отдаёт поле только попросившему. **Обе половины сделаны 2026-08-27**: `JobSerializer.to_representation()` убирает `norad_cat_id` из ответа тому, кто возможность не объявил, `JobCapabilitiesTest` сторожит неизменность ответа для всех остальных. ``` Три вещи, которые видно только из портала и о которых стоит помнить: * `ObservationViewFilter` — обычный `FilterSet` без строгости, django-filter неизвестные query-параметры **игнорирует**. Поэтому клиент мог слать `capabilities` в портал, который о нём не знал, без единого последствия. Введение строгости сломало бы именно новых клиентов — на это стоит тест; * `JobView.list` до 2026-08-27 строил `JobSerializer(...)` напрямую, без контекста. Любая логика сериализатора, зависящая от `request`, там молча не работала. Теперь `self.get_serializer(...)`, как в `retrieve`; * `Observation.satellite` — `null=True`. Всё, что читает `obj.satellite.*` в сериализаторах расписания, обязано это переживать: исключение там роняет запрос расписания целиком, а не одно задание. Проверять этот класс отказов надо на живом железе: код возврата диспетчера клиентом **не проверяется**, упавший граф выглядит как досрочно закончившееся наблюдение. Инструмент — `tools/stand.sh deploy`, печатающий этот дифф после каждой выкатки; см. [](../development/stand.md). ## Два декодера на один сигнал Неочевидная вещь, которую видно только при чтении обоих репозиториев сразу. 1. **`flowgraph_dispatcher` → `satnogs_*.py` → ZMQ → файл на кадр.** Реалтайм, выбор декодера по `mode` от портала. Кадры подхватывает watchdog и шлёт пачками ещё до конца прохода. 2. **`gr_satellites`**, запускаемый из `satnogs-pre` (`scripts/grsat.py`). Питается UDP-потоком IQ **от того же графа**, выбор декодера по NORAD ID через satyaml. С 2026-09-12 (Фаза 3.2) запускается только если клиент нашёл satyaml для NORAD: решение — `sat_data.decoder_for()`, едет в скрипт переменной `SONIKS_DECODER` и в `client_metadata` ключом `radio.decoder`. ~~Кадры копятся в KISS и разбираются только на `stop` — то есть уже после остановки watchdog, и уезжают лишь заданием выгрузки после прохода. **Не реалтайм.**~~ **Реалтайм с 2026-09-10** (Фаза 3.1): `grsat.py stream` читает `--kiss_server` и пишет кадр файлом сразу, watchdog шлёт его пачкой; разбор `--kiss_out` на `stop` остался страховкой, дубли портал отсеивает по sha256. С 2026-09-12 (Фаза 3.5) satyaml значит больше, чем выбор декодера: если режим портала клиенту неизвестен, а satyaml для NORAD есть, проход не отказывается — satnogs-граф берётся по модуляции первого передатчика (`AFSK`/`BPSK`/`FSK`), потому что gr-satellites от него нужен только IQ; исходный режим уезжает в `radio.mode_requested`. Станция публикует список NORAD из satyaml ключом `satellites` в `status/`; портал с 2026-09-12 принимает его и пропускает такой спутник при любом режиме — во всех путях планирования и в SQL-фильтре запусков (решение 74). Bundle с 2026-09-12 подтягивается и между проходами (`resync_sat_data`), а не только при старте контейнера. Из двадцати флоуграфов блоки gr-satellites используют два: `geoscan_gfsk_decoder` и `usp_fsk_decoder`. Дефреймеры gr-satellites не отдают метаданные PMT, из-за чего в форке правился `zmq_frame_decoder_sub.py`. Таблицы `SCRIPTS` и `MODES` в `src/core/configs/flowgraph.py` — **копия**, клиент читает из них только `script_filename` и только чтобы передать имя в pre/post-скрипты. На выбор флоуграфа они не влияют: его делает диспетчер по своей таблице. ```{warning} Из этого раньше делался вывод «расхождения между копиями безвредны». Он неверен, и на нём два обхода продержался баг `SSTV_PD120`. Потребитель у имени есть: `scripts/find_samp_rate.py` определяет по подстрокам в нём частоту дискретизации IQ — и для UDP-потока в gr-satellites (`grsat.py`), и для имени IQ-дампа (`iq_dump_rename.sh`). Имя из чужого режима даёт неверную частоту обоим, без единой строки в логе. Расхождение таблиц безвредно только для выбора графа. ``` ## Станция и внешняя сеть Часть парка стоит в закрытых сетях, где доступен **только `sonik.space`**. Сегодня станция обращается наружу в двух местах, и оба — за образами: | Куда | За чем | Когда | |---|---|---| | `docker.io/librespace/hamlib` | rigctld, rotctld | при `docker compose up` | | `docker.io/sonikspace/soniks-client` | клиент | при `docker compose up` | ~~Ещё два обращения — клоны satyaml из GitHub и GitLab при каждом старте контейнера.~~ **Закрыто 2026-08-28.** `scripts/liveupdate-satyaml.sh` больше не клонирует ничего: satyaml и `sat.cfg` приезжают sat-data bundle'ом с портала (`GET /api/v2/bundles/latest/`, модуль `src/soniks_client/sat_data.py`), кэшируются на постоянном томе и сверяются по sha256. Скрипт остался тонкой точкой входа под прежним именем — на нём стоит `command:` в compose всего парка. Три вещи по этому пути, которые стоит помнить: * **старт не зависит от портала.** Портал лежит, архив битый, хеш не сошёлся — предупреждение в лог и работа на кэше либо на данных из образа; * **применение отделено от скачивания.** Кэш переживает перезапуск, а каталог satyaml внутри пакета `satellites` — нет: он в эфемерной ФС контейнера. Поэтому кэш раскладывается на **каждом** старте, а не только при новой версии; * **версия видна постфактум**: `client_metadata.radio.sat_data` рядом с `flowgraphs` и `exit_code`. `None` означает станцию на данных из образа. **Практическое следствие для любой задачи об обновлении:** пока образы лежат в публичных registry, станции в закрытых сетях недостижимы **кодом** — включая исправления критических багов. Данными они достижимы уже сейчас: новый спутник доезжает bundle'ом без пересборки образа. Ограничение формы registry: Docker-клиент всегда ходит в `https:///v2/...`. Повесить registry на префикс пути нельзя, поддомен в тех же сетях скорее всего тоже закрыт. Значит `/v2/` в корне `sonik.space` и имена вида ~~`sonik.space/soniks-client:2.3.0`~~ `sonik.space/sonikspace/soniks-client:2.3.0`: registry — pull-through cache Docker Hub (решение 34 дорожной карты), путь отображается на Docker Hub один-в-один, push в него нет, digest совпадает. Сервис — профиль `registry` в compose портала, nginx пропускает только `sonikspace/*` и `librespace/hamlib`. На 2026-09-10 на серверах не включён, и шаблоны compose станции смотрят на Docker Hub до подтверждённого pull (решение 37). ## soniks-monitor — материал, а не основа агента Не просто дашборд: в нём есть куски станционного агента — идентичность (`STATION__ID` + `STATION__TOKEN`), постоянная связь с сетью, доступ к логам контейнеров (то есть `docker.sock`), системная телеметрия, знание о текущем наблюдении. Он читает водопад прямо из `/tmp/.soniks/output`, то есть делит spool с клиентом, и сам опрашивает API раз в 30 секунд, дублируя поллинг клиента. ```{warning} Путь `/tmp/.soniks/output` монитор считает константой, а он больше не таков: пункт 9 Фазы 0 задал в обоих compose клиента `PATHS__BASE=/var/lib/soniks-client/data`, чтобы очередь `incomplete/` переживала перезапуск. Монитор, поднятый рядом с обновлённым клиентом, водопадов не найдёт и не скажет об этом — путь у него захардкожен. Чинить на стороне монитора, когда до него дойдёт очередь. ``` Ходит по WebSocket (`WS_URL`, `LOG_POST_URL`) — эндпоинтов под это в `soniks-network` **нет**. ~~Принятое решение: монитор и OTA-агент — один демон.~~ **Смягчено решением 30**: агент пишется с нуля, монитор — материал Фазы 5 (логи через `docker.sock`, разбор водопада). Основой он служить не может: из Docker он только читает логи, секрет станции уходит в URL и в лог, данные — на сторонний сервер. Контракт агента v1 — решения 38–44 в [](../roadmap-network.md). ## Ловушки, видимые только межрепозиторно **Имя .deb флоуграфов — часть контракта, и оно уже разъезжалось.** `Dockerfile` клиента собирает `soniks-flowgraphs` в пакет и ставит его по маске из родительского каталога. Имя пакета задаётся `debian/control` **в соседнем репозитории**, и 2026-08-22 он был переименован из `satnogs-flowgraphs` в `soniks-flowgraphs` (`soniks-flowgraphs@248e606`). Клиентская маска осталась старой, glob перестал раскрываться, apt упал с `Unsupported file` — то есть образ не собирался вовсе с 22 августа. Найдено только сборкой на стенде 2026-08-24: ни один тест и ни один линтер этого не видит, а джоба `docker` в CI идёт **только по тегу**, поэтому пайплайны оставались зелёными. Практический вывод: любая правка `debian/control` во флоуграфах — это правка `Dockerfile` клиента. Проверяется единственным способом — реальной сборкой. С 2026-08-24 сборка есть в CI и без тега (джоба `docker_build_check`: по расписанию автоматически, на ветках вручную), а `FLOWGRAPHS_BRANCH` пиннут тегом — то есть имя `.deb` теперь может разъехаться только вместе с осознанным подъёмом пина. **Версия станции определена наполовину** (было — «не определена»). Закрыто 2026-08-24: gr-satnogs берётся из форка `gr-soniks` по тегу `v3.1.0.1`, flowgraphs — по тегу `2.6.0`, оба коммита записываются в образ и уезжают порталу в `client_metadata.radio` (`version` и `flowgraphs`). Версию клиента задаёт тег GitLab и только он: в репозитории лежат заглушки `0.0.0`, а `CLIENT_VERSION` подставляет CI из `$CI_COMMIT_TAG`. Остаётся незакрытым: плавающий тег `latest-addons`, который тянут оба `docker-compose.yml`. Satyaml из git при старте контейнера закрыт 2026-08-28 bundle'ом (см. выше): определения теперь версионированы и их версия уезжает порталу. То есть по собранному образу код теперь восстановим точно, а вот **какой именно образ стоит на конкретной станции — по-прежнему нет**. Это Фаза 2 (агент и desired-state), не Фаза 1. **Поставка образов ручная, и из кода этого не видно.** Джоба `docker` в `.gitlab-ci.yml` каналом поставки никогда не была: в registry лежат однопланформенные `arm64`-образы, ни одного manifest list, а версионных тегов образов нет вовсе — парк живёт на `latest-addons`. Собирает и заливает человек командой `./build.sh <версия> --push`; мультиарх под QEMU в CI — незаконченная работа, падающая на раннере. С 2026-08-27 образ расслоён на `soniks-base` (гигабайты, пиннутый неизменяемый тег) и клиентский слой (десятки мегабайт), а `build.sh` отказывается публиковать без версии: образ с `0.0.0` обнуляет разбивку наблюдений по версиям клиента и оставляет парк без точки отката. Подробности — «Выкатка образа» в [](../development/contributing.md). Практическое следствие для правок `Dockerfile`: `*_BRANCH` — теги, а не ветки. Чтобы в образ попал новый код соседнего репозитория, там нужен **новый тег**; правка в ветке до образа не доезжает вовсе. **Водопады разных режимов несопоставимы.** Полоса равна `out_samp_rate`, а он зависит от бодрейта: 38.4 кГц для 9600, 48 кГц для FM, 66.56 кГц для APT и SSTV_PD120. Любое сравнение `noise_db` между режимами некорректно — это блокирует алгоритмы автоподбора усиления. **Аппаратная AGC выключена во всех графах намеренно.** Для узкополосных пакетных сигналов она подстраивается под сильнейший сигнал в полосе, обычно помеху. Не включать «чтобы улучшить приём». **Двадцатикратная копипаста в `.grc`.** Блок `soapy_source` с ~45 полями (включая мёртвый канал 1) и доплер-сниппет продублированы построчно. Любая правка обработки усиления или адреса rigctld — это двадцать одинаковых изменений. Каталог `hierarchical/` с шестью блоками задуман как лекарство и не используется ни одним графом. **Ничего не проверяется по-настоящему без SDR.** ~~Тестов нет на `execute_observation`, `main.Application`, `api.py`, `communication_session.py` (360 строк конкурентного кода), `waterfall/deviation.py` (519 строк DSP).~~ Закрыты 2026-08-26/27 (`tests/test_execute_observation.py`, `test_application.py`, `test_api_transport.py`, `antenna/test_communication_session.py`) и 2026-09-12 (`tests/test_deviation.py` — оценщик на синтетических спектрограммах, 93 %); порог покрытия в CI — 80%. Реальная проверка по-прежнему — прогон на стенде либо `scripts/test-flowgraph.sh`, но **только по команде человека** (решение 23 дорожной карты): без команды правка закрывается тестами репозитория, и работа идёт дальше. В `soniks-flowgraphs` `tests/e2e_check.py` подключён к CI с 2026-08-24. ## Где искать дальше | Вопрос | Где | |---|---| | План работ, принятые решения, контракты `/api/v2/` | [](../roadmap-network.md) | | Что уже закрыто из техдолга и **что было отклонено осознанно** | [](../roadmap.md) | | Устройство клиента, жизненный цикл прохода | [](../development/architecture.md) | | Режимы демодуляции, как добавить режим | [](../development/flowgraph.md) | | Судьба файлов по итогу выгрузки | [](../development/upload.md) | | Формат `.dat`, отрисовка, анализ сигнала | [](../development/waterfall.md) | | Устройство флоуграфов и диспетчера | `soniks-flowgraphs/docs/ai/context.md` | Прежде чем предлагать вернуться к чему-то, загляните в [](../roadmap.md): часть очевидных на вид правок там уже рассмотрена и **отклонена с обоснованием** — в частности процесс-wide лок на наложившиеся проходы. Не переигрывайте такие решения молча. ## Снимок Состояние, на котором проверялись все факты выше. | Репозиторий | Ветка | Коммит | Дата проверки | |---|---|---|---| | `soniks-client-new` | `soniks` | `661ea65` | 2026-09-10 | | `soniks-flowgraphs` | `soniks` | `14a8d60` (тег `2.6.0`) + незакоммиченный пакет 6 | 2026-09-12: золотой фронтенд, `dev_args`, без `hierarchical/` и `doppler_correction_per_sec` — коммит и CI `grcc` за мейнтейнером | | `gr-soniks` | `soniks` | `4defcfc` (тег `v3.1.0.1`) | 2026-09-10, без изменений | | `soniks-network` | `soniks` | `dab775a9` + незакоммиченный пакет 7 | 2026-09-12: `satellites` в статусе и воротах планирования (решение 74) — коммит за мейнтейнером | | `soniks-agent` | — | без git | 2026-09-11, создан | | `soniks-satyaml` | `main` | `3b6d763` | 2026-09-10 | | `soniks-monitor` | `main` | `3b1e526` | 2026-09-10, без изменений | `soniks-satyaml` появился в таблице вместе с sat-data bundle: теперь это не только источник определений, но и **сборщик** архива, который едет на станции, и в нём же лежит `sat.cfg`. `3b6d763` — коммит, который добавил CI и перенёс `sat.cfg`; сабмодуль в клиенте на него переставлен. **Портал за 28 августа — 10 сентября ушёл на 115 коммитов** (`52a4abd2` → `9ece8db4`), и большинство утверждений о нём выше пришлось перепроверить. Что из этого касается клиента: * **платформа** (2026-09-02, `cba0d33e`): Python 3.12, Django 5.2 LTS, DRF 3.18, Redis 7.4, bookworm. Образ собирается в CI и выкатывается из registry GitLab, а не собирается на сервере; * **правило 21 закреплено и расширено**: инвариант 7 в его `docs/ai/context.md`, `urls_v2.py` для всего нового, три опт-ин поля в `/api/jobs/` (`b903cc93`, 2026-09-07), аналитика под `/api/v2/` с алиасами. Удалённое из модели `vetted_status` продолжает отдаваться вычисленным; * **строже к присланному**: `400 frame_outside_window` на кадр вне окна (`25f25da7`, 2026-08-28), троттлинг списка наблюдений (`f379b938`); * **модель станции** (2026-09-06, `6c42f5e7`): состояние не хранится, четвёртое состояние `unavailable`, в v1 отдаётся как `Offline`; * **снимок передатчика на наблюдении** (2026-08-28): оценка и опт-ин `transmitter_parameters` читают его, а не справочник; * **доступ** (`79babbb9`, 2026-08-29): ротация токена только по `POST`, анонимный `POST /api/observations/` отвечает 401, а не 400; * **не изменилось**: `has already been uploaded` дословно, `/api/v2/bundles/`, content-hash, `JOBS_ALLOW_ANONYMOUS`, `OBS_AUDIO_DURATION_TOLERANCE`, `ClientIDAuthentication` не подключён, бутафорские поля конфигурации на месте, registry на `sonik.space` нет, `clean_observations` удалена без замены. Остальное — фронт (Bootstrap 5.3, снятие jQuery, дизайн-система), TLE и автопланирование, страницы спутников и запусков — станции не касается. **10–11 сентября портал ушёл ещё на 8 коммитов** (`8dd678e4` → `6158eaf7`): настройки станции и `state/`, MQTT-брокер и `ReleaseChannel` (`9b58f1f7`), команды станции и обследование приёмника (`96806c28`), registry-прокси (`7f922b5e`); из остального станции касается только `282eb9cb`, где `close_old_connections()` в мосте MQTT перенесён из `handle_message` в `on_message` — иначе мост ронял соединение с БД внутри транзакции. Глобус, фильтры списков и модалка спутника — фронт. Правки этой сессии (ключ `agent`, валидатор digest, миграция 0057) на 2026-09-11 не закоммичены. Если соседний репозиторий ушёл вперёд — перепроверьте затронутые утверждения и обновите эту страницу вместе с правкой.