Контекст экосистемы для ИИ-агента

Точка входа для агента, работающего над задачами, которые выходят за пределы одного репозитория. Здесь то, чего не видно из кода soniks-client: устройство соседних проектов, контракты между ними и подтверждённые дефекты на их стороне.

Что здесь не дублируется: устройство самого клиента — оно в docs/development/; правила работы с этим репозиторием — они в CLAUDE.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/<id>/ с client_version, client_metadata

метаданные прохода

PUT /api/observations/<id>/ 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/<id>/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/<id>/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/<id>/state, retained) в момент сохранения формы

MQTT wss://sonik.space/mqtt

с 2026-09-10 (src/soniks_client/mqtt.py): логин station-<id>, пароль — токен владельца; подписка stations/<id>/state, публикация stations/<id>/status (тот же документ, что POST) и stations/<id>/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 пишется в лог предупреждением — это счётчик готовности парка к переключению. Расписание всей сети читается кем угодно без токена, пока флаг не переключён.

Предупреждение

Отсюда раньше делался вывод «станция с протухшим токеном продолжит получать задания, но будет числиться 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], где TDEMODDATA_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/<id>/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/<id>/ поле 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 в дорожной карте).

Предупреждение

До 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.satellitenull=True. Всё, что читает obj.satellite.* в сериализаторах расписания, обязано это переживать: исключение там роняет запрос расписания целиком, а не одно задание.

Проверять этот класс отказов надо на живом железе: код возврата диспетчера клиентом не проверяется, упавший граф выглядит как досрочно закончившееся наблюдение. Инструмент — tools/stand.sh deploy, печатающий этот дифф после каждой выкатки; см. Стенд.

Два декодера на один сигнал

Неочевидная вещь, которую видно только при чтении обоих репозиториев сразу.

  1. flowgraph_dispatchersatnogs_*.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-скрипты. На выбор флоуграфа они не влияют: его делает диспетчер по своей таблице.

Предупреждение

Из этого раньше делался вывод «расхождения между копиями безвредны». Он неверен, и на нём два обхода продержался баг 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://<host>/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 секунд, дублируя поллинг клиента.

Предупреждение

Путь /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 в Дорожная карта: надёжность сети.

Ловушки, видимые только межрепозиторно

Имя .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 обнуляет разбивку наблюдений по версиям клиента и оставляет парк без точки отката. Подробности — «Выкатка образа» в Участие в разработке.

Практическое следствие для правок 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/

Дорожная карта: надёжность сети

Что уже закрыто из техдолга и что было отклонено осознанно

Дорожная карта: закрытие техдолга

Устройство клиента, жизненный цикл прохода

Архитектура

Режимы демодуляции, как добавить режим

Потоковый граф

Судьба файлов по итогу выгрузки

Загрузка данных

Формат .dat, отрисовка, анализ сигнала

Водопад

Устройство флоуграфов и диспетчера

soniks-flowgraphs/docs/ai/context.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 коммитов (52a4abd29ece8db4), и большинство утверждений о нём выше пришлось перепроверить. Что из этого касается клиента:

  • платформа (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 коммитов (8dd678e46158eaf7): настройки станции и state/, MQTT-брокер и ReleaseChannel (9b58f1f7), команды станции и обследование приёмника (96806c28), registry-прокси (7f922b5e); из остального станции касается только 282eb9cb, где close_old_connections() в мосте MQTT перенесён из handle_message в on_message — иначе мост ронял соединение с БД внутри транзакции. Глобус, фильтры списков и модалка спутника — фронт. Правки этой сессии (ключ agent, валидатор digest, миграция 0057) на 2026-09-11 не закоммичены.

Если соседний репозиторий ушёл вперёд — перепроверьте затронутые утверждения и обновите эту страницу вместе с правкой.