Доработка документации
This commit is contained in:
251
docs/api.md
Normal file
251
docs/api.md
Normal file
@@ -0,0 +1,251 @@
|
||||
# Спецификация HTTP API
|
||||
|
||||
Документ описывает HTTP API сервера `gpio-monitor-server`. Источник истины — [cmd/server/main.go](../cmd/server/main.go).
|
||||
|
||||
**Аудитория:** разработчики и интеграторы.
|
||||
|
||||
## Содержание
|
||||
|
||||
- [Общие сведения](#общие-сведения)
|
||||
- [GET /api/health](#get-apihealth)
|
||||
- [GET /api/latest](#get-apilatest)
|
||||
- [GET /api/history](#get-apihistory)
|
||||
- [GET /api/stream](#get-apistream)
|
||||
- [GET /api/cam](#get-apicam)
|
||||
- [GET /api/log/files](#get-apilogfiles)
|
||||
- [GET /api/log/data](#get-apilogdata)
|
||||
- [GET /api/log/events](#get-apilogevents)
|
||||
- [Известные особенности](#известные-особенности)
|
||||
|
||||
## Общие сведения
|
||||
|
||||
- **Адрес:** порт задаётся флагом `-port` (по умолчанию `:8080`).
|
||||
- **Метод:** все эндпоинты — только `GET` (плюс `OPTIONS` для CORS preflight).
|
||||
- **Формат ответов:** JSON (`Content-Type: application/json`), кроме `/api/cam` (MJPEG-поток).
|
||||
- **CORS:** на всех эндпоинтах, кроме `/api/cam`, стоит обёртка `cors()`: `Access-Control-Allow-Origin: *`, `Access-Control-Allow-Methods: GET, OPTIONS`. Запрос `OPTIONS` возвращает `200` без тела. `/api/cam` выставляет `Access-Control-Allow-Origin: *` самостоятельно, но `OPTIONS` не обрабатывает.
|
||||
- **Ошибки:** возвращаются как `text/plain` с русскоязычным сообщением и кодом `400`/`404`/`500`/`503`. Неизвестный путь под `/api/` → `404 not found`.
|
||||
- **Статика:** все пути вне `/api/` обслуживаются встроенным веб-дашбордом (`go:embed`, см. [architecture.md](architecture.md)).
|
||||
|
||||
Расшифровка полей `value`/`amplitude`/`signal` — в [data-formats.md](data-formats.md).
|
||||
|
||||
## GET /api/health
|
||||
|
||||
Статус сервера, соединения с pipe и сводная статистика. Основной эндпоинт для опроса дашбордом.
|
||||
|
||||
Пример ответа:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "На связи",
|
||||
"uptime_sec": 12345.67,
|
||||
"last_data_ms": 42,
|
||||
"pipe_alive": true,
|
||||
"has_data": true,
|
||||
"latest": 66,
|
||||
"stats": {
|
||||
"size": 10240,
|
||||
"filled": 10240,
|
||||
"last_write": "2026-07-17T12:34:56.789012345+03:00",
|
||||
"total_bytes": 1234567,
|
||||
"total_bits": 9876536,
|
||||
"bytes_per_sec": 100.5,
|
||||
"bits_per_sec": 804.0
|
||||
},
|
||||
"server_time": "17.07.2026 12:34",
|
||||
"sound_level": 27
|
||||
}
|
||||
```
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `status` | string | `"На связи"`, если последняя запись в буфер была менее 15 секунд назад, иначе `"Нет связи"` |
|
||||
| `uptime_sec` | float | Время работы сервера в секундах |
|
||||
| `last_data_ms` | int | Миллисекунд с момента последней записи данных |
|
||||
| `pipe_alive` | bool | Была ли запись за последние 3 секунды |
|
||||
| `has_data` | bool | Есть ли хоть один байт в кольцевом буфере |
|
||||
| `latest` | int (0–255) | Последний сырой байт GPIO; `0`, если данных ещё не было |
|
||||
| `stats.size` | int | Ёмкость кольцевого буфера (флаг `-buffer-size`) |
|
||||
| `stats.filled` | int | Сколько ячеек буфера заполнено |
|
||||
| `stats.last_write` | string (RFC 3339) | Время последней записи |
|
||||
| `stats.total_bytes` / `total_bits` | int | Всего принято байт/бит с момента запуска |
|
||||
| `stats.bytes_per_sec` / `bits_per_sec` | float | Текущая скорость приёма |
|
||||
| `server_time` | string | Серверное время в формате `ДД.ММ.ГГГГ ЧЧ:ММ` (два пробела между датой и временем) |
|
||||
| `sound_level` | int (0–100) | Уровень звука с микрофона; `0`, если аудиомонитор не запущен |
|
||||
|
||||
## GET /api/latest
|
||||
|
||||
Последний байт и короткая история.
|
||||
|
||||
Ответ при наличии данных:
|
||||
|
||||
```json
|
||||
{
|
||||
"latest": 66,
|
||||
"history": [64, 65, 66, 66, 65, 64, 66, 67, 66, 66]
|
||||
}
|
||||
```
|
||||
|
||||
- `history` — до 10 последних байт, **от старых к новым** (последний элемент = `latest`).
|
||||
- Если буфер пуст, возвращается `200` с телом `{"error": "no data"}` (не HTTP-ошибка).
|
||||
|
||||
## GET /api/history
|
||||
|
||||
История для графика.
|
||||
|
||||
```json
|
||||
{
|
||||
"bytes": [64, 65, 66, "...", 66]
|
||||
}
|
||||
```
|
||||
|
||||
- `bytes` — массив целых (0–255), до **300** последних байт, от старых к новым. Если данных меньше — вернётся сколько есть.
|
||||
|
||||
## GET /api/stream
|
||||
|
||||
Статус видеопотока камеры.
|
||||
|
||||
```json
|
||||
{
|
||||
"cam": "/api/cam",
|
||||
"available": true,
|
||||
"source": "http://192.168.1.10:1984/api/stream.mjpeg?src=cam_mjpeg"
|
||||
}
|
||||
```
|
||||
|
||||
| Поле | Описание |
|
||||
|---|---|
|
||||
| `cam` | Путь к прокси-эндпоинту MJPEG (всегда `/api/cam`) |
|
||||
| `available` | Доступен ли поток: сервер делает запрос `http://localhost:1984/api/streams?src=cam_mjpeg` и проверяет код 200 |
|
||||
| `source` | Прямой URL потока go2rtc, построенный из хоста запроса (`r.Host`) и порта 1984 |
|
||||
|
||||
⚠️ Проверка `available` захардкожена на `localhost:1984` и **не учитывает флаг `-camera-url`** (main.go:460). См. [tech-debt.md](tech-debt.md).
|
||||
|
||||
## GET /api/cam
|
||||
|
||||
Прокси MJPEG-потока камеры. Сервер запрашивает URL из флага `-camera-url` (по умолчанию `http://127.0.0.1:1984/api/stream.mjpeg?src=cam_mjpeg`) и ретранслирует поток клиенту с исходными заголовками, сбрасывая буфер после каждого чтения (chunk 32 КБ).
|
||||
|
||||
Ошибки:
|
||||
|
||||
| Код | Тело | Когда |
|
||||
|---|---|---|
|
||||
| `503` | `камера недоступна` | go2rtc не отвечает |
|
||||
| `500` | `поток не поддерживается` | ResponseWriter не поддерживает Flush |
|
||||
|
||||
## GET /api/log/files
|
||||
|
||||
Список бинарных лог-файлов `gpio-*.bin` из каталога данных (см. [пути хранения](data-formats.md#пути-хранения)).
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "gpio-2026-07-17-12.bin",
|
||||
"path": "/var/log/gpio-monitoring/data/gpio-2026-07-17-12.bin",
|
||||
"size": 34567,
|
||||
"mod_time": "2026-07-17T12:59:59+03:00",
|
||||
"time": "17.07.2026 12:00",
|
||||
"date": "2026-07-17",
|
||||
"hour": 12,
|
||||
"is_active": false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
- Массив отсортирован по `mod_time`, новые первыми.
|
||||
- `time`/`date`/`hour` вычисляются из имени файла; если имя не соответствует шаблону `gpio-YYYY-MM-DD-HH.bin` — из `mod_time`.
|
||||
- `is_active` в текущей реализации всегда `false` (зарезервировано).
|
||||
- ⚠️ Если файлов нет, возвращается `null`, а не `[]` (сериализация nil-слайса).
|
||||
|
||||
## GET /api/log/data
|
||||
|
||||
Чтение сэмплов из конкретного `.bin`-файла с пагинацией.
|
||||
|
||||
**Параметры запроса:**
|
||||
|
||||
| Параметр | Обязательный | По умолчанию | Описание |
|
||||
|---|---|---|---|
|
||||
| `file` | да | — | Имя файла (например `gpio-2026-07-17-12.bin`); путь очищается через `filepath.Base` — path traversal невозможен |
|
||||
| `page` | нет | 1 | Номер страницы (значения < 1 игнорируются; больше максимума — прижимается к последней) |
|
||||
| `page_size` | нет | 100 | Размер страницы |
|
||||
|
||||
Пример: `GET /api/log/data?file=gpio-2026-07-17-12.bin&page=1&page_size=50`
|
||||
|
||||
```json
|
||||
{
|
||||
"filename": "gpio-2026-07-17-12.bin",
|
||||
"total": 3600,
|
||||
"page": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 72,
|
||||
"has_previous": false,
|
||||
"has_next": true,
|
||||
"start_index": 3551,
|
||||
"end_index": 3600,
|
||||
"samples": [
|
||||
{
|
||||
"ts": 1789034096123456,
|
||||
"time": "17.07.2026 12:34:56",
|
||||
"value": 66,
|
||||
"amplitude": 1,
|
||||
"signal": 2,
|
||||
"strength_name": "Средний"
|
||||
}
|
||||
],
|
||||
"stats": {
|
||||
"total_points": 3600,
|
||||
"amplitude_over_0": 120,
|
||||
"signal_over_10": 15
|
||||
},
|
||||
"order": "newest_first"
|
||||
}
|
||||
```
|
||||
|
||||
- **Порядок `newest_first`:** страница 1 содержит самые новые сэмплы; внутри страницы сэмплы также идут от новых к старым.
|
||||
- `ts` — метка времени в **микросекундах** Unix; `time` — она же в формате `ДД.ММ.ГГГГ ЧЧ:ММ:СС`.
|
||||
- `value` — сырой байт; `amplitude` — биты 6–7 (0–3); `signal` — биты 0–5 (0–63); `strength_name` — текстовое имя уровня (см. [data-formats.md](data-formats.md#байт-gpio)).
|
||||
- `stats` считается по **всему файлу**, а не по странице: `amplitude_over_0` — сэмплы с амплитудой > 0, `signal_over_10` — с сигналом > 10.
|
||||
- `start_index`/`end_index` — 1-based диапазон в хронологическом порядке файла.
|
||||
- Для пустого файла возвращается `total: 0` и пустой массив `samples`.
|
||||
|
||||
Ошибки: `400 отсутствует параметр file`, `404 файл не найден`, `500` при ошибке чтения.
|
||||
|
||||
⚠️ Файл целиком читается в память до пагинации — учитывайте при больших `.bin`-файлах ([tech-debt.md](tech-debt.md)).
|
||||
|
||||
## GET /api/log/events
|
||||
|
||||
Системные события из `events_human.log` с пагинацией.
|
||||
|
||||
**Параметры:** `page` (по умолчанию 1), `page_size` (по умолчанию 100) — семантика как у `/api/log/data`.
|
||||
|
||||
```json
|
||||
{
|
||||
"events": [
|
||||
{ "time": "2026-07-17 12:00:00.001", "event": "ПЛАНОВАЯ_РОТАЦИЯ" },
|
||||
{ "time": "2026-07-17 11:59:12.512", "event": "ОБНАРУЖЕНО_12_ОБЪЕКТОВ_СИЛА_2" }
|
||||
],
|
||||
"total": 254,
|
||||
"page": 1,
|
||||
"page_size": 100,
|
||||
"total_pages": 3,
|
||||
"has_previous": false,
|
||||
"has_next": true,
|
||||
"returned": 100,
|
||||
"start_index": 155,
|
||||
"end_index": 254,
|
||||
"order": "newest_first"
|
||||
}
|
||||
```
|
||||
|
||||
- Парсятся только строки вида `[время] EVENT: имя`; прочие строки учитываются в `total`, но не попадают в `events` (поэтому `returned` может быть меньше размера страницы).
|
||||
- Словарь имён событий — в [data-formats.md](data-formats.md#события-системы).
|
||||
|
||||
Ошибки: `500`, если файл событий недоступен.
|
||||
|
||||
## Известные особенности
|
||||
|
||||
Зафиксированы также в [tech-debt.md](tech-debt.md):
|
||||
|
||||
1. `/api/stream` проверяет доступность камеры по захардкоженному `localhost:1984`, игнорируя `-camera-url`.
|
||||
2. В `main.go` есть неиспользуемая функция `handleCamProxy` (двойник `handleCamProxyWithURL` с захардкоженным URL) — роутер её не вызывает.
|
||||
3. `/api/latest` при пустом буфере возвращает `200` с `{"error": "no data"}`, а не код ошибки.
|
||||
4. `/api/log/files` при отсутствии файлов возвращает `null` вместо пустого массива.
|
||||
5. CORS открыт для всех источников (`*`) — рассчитано на доверенную локальную сеть.
|
||||
150
docs/architecture.md
Normal file
150
docs/architecture.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# Архитектура системы
|
||||
|
||||
Описание устройства GPIO Monitor: компоненты, поток данных, конкурентность, топологии развёртывания.
|
||||
|
||||
**Аудитория:** разработчики.
|
||||
|
||||
## Содержание
|
||||
|
||||
- [Общая схема](#общая-схема)
|
||||
- [Захват GPIO и доставка данных](#захват-gpio-и-доставка-данных)
|
||||
- [Компоненты Go-сервера](#компоненты-go-сервера)
|
||||
- [Порядок инициализации](#порядок-инициализации)
|
||||
- [Конкурентность](#конкурентность)
|
||||
- [Встраивание фронтенда](#встраивание-фронтенда)
|
||||
- [Топологии развёртывания](#топологии-развёртывания)
|
||||
|
||||
## Общая схема
|
||||
|
||||
```
|
||||
GPIO-шина (8 бит)
|
||||
│ прерывание по стробу WR
|
||||
▼
|
||||
gpio-interrupt (C + WiringPi) ← отдельная программа, вне этого репозитория
|
||||
│ stdout → socket (systemd)
|
||||
▼
|
||||
FIFO pipe /tmp/gpio_pipe ← создаёт systemd (monitor-gpio.socket)
|
||||
│ сырые байты
|
||||
▼
|
||||
┌──────────────────────── Go-сервер (gpio-monitor-server) ────────────────────────┐
|
||||
│ PipeReader (internal/pipe) │
|
||||
│ ├─► RingBuffer (internal/adapter) — RAM, для UI │
|
||||
│ ├─► RotatingLogger → DataLogger — .bin-архив на диске │
|
||||
│ ├─► HumanLogger — gpio_human.log │
|
||||
│ ├─► EventLogger (алерты count > 10) — events.log / events_human.log │
|
||||
│ └─► Monitor (watchdog тишины) │
|
||||
│ │
|
||||
│ Retention (internal/logger) — фоновая очистка .bin │
|
||||
│ audio.Monitor (internal/audio) — уровень звука через arecord │
|
||||
│ │
|
||||
│ HTTP :8080 │
|
||||
│ ├─ /api/* — JSON API (см. api.md) │
|
||||
│ ├─ /api/cam — прокси MJPEG с go2rtc (:1984) │
|
||||
│ └─ /* — встроенный веб-дашборд (go:embed) │
|
||||
└─────────────────────────────────────────────────────────────────────────────────┘
|
||||
▲
|
||||
│ HTTP-опрос (~1 раз/с)
|
||||
Браузер (dashboard.html / logs.html, TypeScript — см. frontend.md)
|
||||
```
|
||||
|
||||
Форматы данных на каждом участке — в [data-formats.md](data-formats.md), API — в [api.md](api.md).
|
||||
|
||||
## Захват GPIO и доставка данных
|
||||
|
||||
Захват выполняет C-программа `gpio-interrupt` (WiringPi), которая по прерыванию строба читает биты шины и пишет байты в stdout. **Её исходников в этом репозитории нет** — она живёт на устройстве в `/home/user/WiringPi/examples` (см. [tech-debt.md](tech-debt.md)).
|
||||
|
||||
Доставка построена на **systemd socket-activation** ([scripts/monitor-gpio.socket](../scripts/monitor-gpio.socket), [scripts/monitor-gpio.service](../scripts/monitor-gpio.service)):
|
||||
|
||||
1. `monitor-gpio.socket` создаёт FIFO `/tmp/gpio_pipe` (`ListenFIFO`, права 0666, `RemoveOnStop=yes`);
|
||||
2. `monitor-gpio.service` запускает `gpio-interrupt` со `StandardOutput=socket` — stdout программы направляется прямо в FIFO;
|
||||
3. Go-сервер открывает FIFO на чтение (флаг `-pipe`).
|
||||
|
||||
Такая схема развязывает жизненные циклы: капчер и сервер можно перезапускать независимо, pipe создаёт и убирает systemd.
|
||||
|
||||
## Компоненты Go-сервера
|
||||
|
||||
### PipeReader — [internal/pipe/reader.go](../internal/pipe/reader.go)
|
||||
|
||||
Единственный «производитель» данных. В отдельной горутине: следит за существованием FIFO (проверка каждые 2 с), открывает его, читает блоками до 4096 байт и раздаёт каждый байт потребителям (см. схему). Ведёт машину состояний `unknown → found / not_found / error / disconnected` и пишет события `PIPE_*` **только при смене состояния** — защита от спама в лог при флапающем соединении. При обрыве чтения переподключается через 1 с.
|
||||
|
||||
Дополнительно дедуплицирует human-лог (пишет при изменении `count`/`strength` или раз в `-human-log-interval` с) и алерты (`-alert-cooldown` на одинаковый `count`).
|
||||
|
||||
### RingBuffer — [internal/adapter/buffer.go](../internal/adapter/buffer.go)
|
||||
|
||||
Кольцевой буфер последних N байт (флаг `-buffer-size`, по умолчанию 10240) — источник данных для `/api/latest`, `/api/history`, `/api/health`. Потокобезопасен: данные под `sync.RWMutex`, счётчики (`lastWrite`, `totalBytes`) — атомики.
|
||||
|
||||
Скорость приёма считается **двумя механизмами**, оба пишут в одни и те же атомики `currentBPS`/`currentBPSBits`: периодический пересчёт по дельте счётчика (не чаще раза в 250 мс) и скользящее окно 1 с по временным меткам последних байт. Второй перетирает первый; есть и публичный `GetWindowSpeed()`, который API не использует. Это избыточность — кандидат на упрощение ([tech-debt.md](tech-debt.md)).
|
||||
|
||||
### Логгеры — [internal/logger/](../internal/logger/)
|
||||
|
||||
- **RotatingLogger** (`rotation.go`) — фасад над DataLogger: держит файл текущего часа, по таймеру (`-rotation-check-interval`) проверяет смену часа и переоткрывает файл.
|
||||
- **DataLogger** (`data_logger.go`) — бинарная запись сэмплов (9 байт) с буфером 64 КБ, flush раз в 1 с или при заполнении, `fsync` после каждого сброса.
|
||||
- **HumanLogger** (`human_logger.go`) — построчная запись в `gpio_human.log` с немедленным `Sync`.
|
||||
- **EventLogger** (`event_logger.go`) — системные события, двойная запись: JSON (`events.log`) + текст (`events_human.log`).
|
||||
- **Monitor** (`monitor.go`) — watchdog: PipeReader отмечает каждую запись (`RecordWrite`), фоновый цикл раз в `-watchdog-interval` минут сравнивает тишину с порогами и пишет `ТИШИНА_5МИН`/`ТИШИНА_10МИН`.
|
||||
- **Retention** (`retention.go`) — фоновая FIFO-очистка `.bin`-файлов по возрасту и суммарному размеру, минимум 2 файла всегда сохраняются; устойчива к скачку системных часов (подробности в [data-formats.md](data-formats.md#retention-очистка)).
|
||||
|
||||
### Аудиомонитор — [internal/audio/monitor.go](../internal/audio/monitor.go)
|
||||
|
||||
Запускает `arecord` (ALSA: raw, 1 канал, 16 кГц, S16_LE), автоматически выбирая USB-аудиоустройство по выводу `arecord -l` (fallback — первое устройство захвата, затем `default`). Читает PCM блоками 3200 байт (100 мс), считает RMS и публикует уровень 0–100 (`RMS/32768 × 100 × 4`, с отсечкой). При падении `arecord` перезапускает захват через 5 с. Уровень отдаётся в `/api/health` полем `sound_level`. Недоступность микрофона не мешает запуску сервера.
|
||||
|
||||
### HTTP-слой — [cmd/server/main.go](../cmd/server/main.go)
|
||||
|
||||
Стандартный `net/http` без внешних роутеров: `/api/` обслуживает switch по пути (8 эндпоинтов, все под CORS-обёрткой, кроме `/api/cam`), остальное — `http.FileServer` поверх встроенных веб-файлов. Прокси камеры ретранслирует MJPEG-поток из go2rtc с flush после каждого чанка 32 КБ.
|
||||
|
||||
## Порядок инициализации
|
||||
|
||||
Последовательность в `main()` (важна: каждый следующий компонент получает уже готовые предыдущие):
|
||||
|
||||
1. Разбор CLI-флагов → `logger.Config`;
|
||||
2. **EventLogger** (без него сервер не стартует) → событие `СЕРВЕР_ЗАПУЩЕН`;
|
||||
3. **HumanLogger**;
|
||||
4. **Monitor** (watchdog) — сразу запускает фоновый цикл;
|
||||
5. **RotatingLogger** — открывает файл текущего часа, запускает цикл ротации;
|
||||
6. **Retention** — если `-retention-hours > 0` или `-retention-mb > 0`; первая очистка через 1 мин;
|
||||
7. **RingBuffer**;
|
||||
8. **audio.Monitor** — ошибка запуска не фатальна;
|
||||
9. **PipeReader.Start** — горутина чтения FIFO;
|
||||
10. Регистрация HTTP-хендлеров и `ListenAndServe` (блокируется навсегда).
|
||||
|
||||
Ошибка инициализации любого логгера (шаги 2–5) — `log.Fatal`, сервер не запускается.
|
||||
|
||||
## Конкурентность
|
||||
|
||||
| Горутина | Кто запускает | Что делает |
|
||||
|---|---|---|
|
||||
| Чтение FIFO | `PipeReader.Start` | Цикл открытия/чтения пайпа, раздача байт |
|
||||
| Flush-цикл DataLogger | `NewDataLogger` (пересоздаётся при ротации) | Сброс буфера раз в 1 с |
|
||||
| Цикл ротации | `NewRotatingLogger` | Проверка смены часа |
|
||||
| Watchdog | `NewMonitor` | Проверка тишины |
|
||||
| Цикл retention | `Retention.StartWithInterval` | Периодическая очистка + разовая через 1 мин |
|
||||
| Чтение PCM | `audio.Monitor.startCapture` | RMS-расчёт уровня звука |
|
||||
| `cmd.Wait` arecord | `startCapture` | Сбор зомби-процесса |
|
||||
| HTTP-хендлеры | `net/http` | По горутине на запрос |
|
||||
|
||||
Синхронизация: у каждого компонента свой мьютекс (`RingBuffer.mu`+`speedMu`+`recentMu`, `DataLogger.mu`, `RotatingLogger.mu`, `EventLogger.mu`, `HumanLogger.mu`, `Monitor.mu`, `audio.Monitor.mu`); межкомпонентных блокировок нет — данные передаются вызовами методов, владение файлами не разделяется. Завершение — через `context` (PipeReader, audio) и каналы (`DataLogger.closeCh`, `Retention.stopCh`).
|
||||
|
||||
## Встраивание фронтенда
|
||||
|
||||
```go
|
||||
//go:embed web/dist web/css web/fonts web/*.html web/*.png
|
||||
var webFiles embed.FS
|
||||
```
|
||||
|
||||
Скомпилированный дашборд вшивается в бинарник на этапе `go build` — сервер разворачивается одним файлом. Следствие: **`go build` падает, если `web/dist` не существует**, поэтому фронтенд всегда собирается первым (`make build` = `build-frontend` + `build-backend`). Подробнее — [development.md](development.md) и [frontend.md](frontend.md).
|
||||
|
||||
## Топологии развёртывания
|
||||
|
||||
Режим определяется наличием каталога `/opt/gpio-monitoring` ([internal/logger/paths.go](../internal/logger/paths.go)):
|
||||
|
||||
| | Пакетный режим (deb) | Режим разработки |
|
||||
|---|---|---|
|
||||
| Бинарник | `/usr/bin/gpio-monitor-server` | `./build/gpio-monitor-server` или `go run` |
|
||||
| Запуск | systemd `gpio-monitor-server.service`, пользователь `gpio-monitor` | вручную |
|
||||
| Логи | `/var/log/gpio-monitoring` | `~/.local/share/gpio-monitoring/logs` |
|
||||
| Веб-файлы | вшиты в бинарник (копия в `/opt/gpio-monitoring/web`) | вшиты в бинарник |
|
||||
| Источник данных | реальный `gpio-interrupt` через systemd socket | эмулятор `scripts/emulator.py` + `mkfifo` |
|
||||
|
||||
Внешние сервисы в обоих режимах: **go2rtc** (порт 1984, конфиг [scripts/go2rtc.yaml](../scripts/go2rtc.yaml)) для MJPEG-камеры и **ALSA/arecord** для микрофона. Установка и настройка — в [operations.md](operations.md).
|
||||
|
||||
Подпроект [SDR/](../SDR/) (OFDM-радиолинк на PlutoSDR) — автономный C-проект со своей сборкой и [README](../SDR/README.md); с Go-сервером не связан: канал HackRF/PlutoSDR используется как альтернативный транспорт байтов GPIO-шины (см. [scripts/emulatohackrf.sh](../scripts/emulatohackrf.sh)).
|
||||
169
docs/data-formats.md
Normal file
169
docs/data-formats.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# Форматы данных и логов
|
||||
|
||||
Документ описывает все форматы данных системы: байт GPIO, протокол FIFO-пайпа, бинарные и текстовые логи, пути хранения и правила очистки.
|
||||
|
||||
**Аудитория:** разработчики и интеграторы.
|
||||
|
||||
## Содержание
|
||||
|
||||
- [Байт GPIO](#байт-gpio)
|
||||
- [Протокол FIFO-пайпа](#протокол-fifo-пайпа)
|
||||
- [Бинарные логи (.bin)](#бинарные-логи-bin)
|
||||
- [Человекочитаемый GPIO-лог](#человекочитаемый-gpio-лог)
|
||||
- [События системы](#события-системы)
|
||||
- [Пути хранения](#пути-хранения)
|
||||
- [Ротация](#ротация)
|
||||
- [Retention (очистка)](#retention-очистка)
|
||||
|
||||
## Байт GPIO
|
||||
|
||||
Единица данных системы — один байт, снятый с 8-битной GPIO-шины. Разбор — в [internal/logger/parser.go](../internal/logger/parser.go) (`ParseGPIO`):
|
||||
|
||||
```
|
||||
бит: 7 6 | 5 4 3 2 1 0
|
||||
└─┬─┘ └────┬─────┘
|
||||
strength count
|
||||
(0–3) (0–63)
|
||||
```
|
||||
|
||||
| Поле | Биты | Диапазон | Смысл |
|
||||
|---|---|---|---|
|
||||
| `count` (в API — `signal`) | 0–5 | 0–63 | Количество обнаружений |
|
||||
| `strength` (в API — `amplitude`) | 6–7 | 0–3 | Сила сигнала |
|
||||
|
||||
Текстовые имена уровней силы (`GetStrengthName`):
|
||||
|
||||
| Значение | Имя |
|
||||
|---|---|
|
||||
| 0 | Слабый |
|
||||
| 1 | Средний |
|
||||
| 2 | Сильный |
|
||||
| 3 | Максимальный |
|
||||
|
||||
**Порог тревоги:** при `count > 10` (`IsHighCount`) генерируется событие `ОБНАРУЖЕНО_<count>_ОБЪЕКТОВ_СИЛА_<strength>` и строка «ВНИМАНИЕ» в human-логе (анти-спам: не чаще одного алерта на одинаковый `count` за `-alert-cooldown` секунд).
|
||||
|
||||
Пример: байт `0x8C` = `1000 1100` → strength = 2 («Сильный»), count = 12 → сработает алерт.
|
||||
|
||||
## Протокол FIFO-пайпа
|
||||
|
||||
Связь C-программы захвата с Go-сервером — именованный канал (по умолчанию `/tmp/gpio_pipe`, флаг `-pipe`).
|
||||
|
||||
- Поток **сырых байт без кадрирования**: каждый байт — одно измерение в формате [байта GPIO](#байт-gpio). Никаких заголовков, разделителей и контрольных сумм.
|
||||
- Сервер читает блоками до 4096 байт ([internal/pipe/reader.go](../internal/pipe/reader.go)).
|
||||
- Метка времени присваивается **на стороне сервера** в момент чтения (`time.Now().UnixMicro()`).
|
||||
- При отсутствии/обрыве пайпа сервер переподключается сам: проверка существования каждые 2 с, повторное открытие через 1 с; смены состояния фиксируются событиями `PIPE_*` (см. [словарь событий](#события-системы)).
|
||||
|
||||
## Бинарные логи (.bin)
|
||||
|
||||
Основной архив данных. Запись — [internal/logger/data_logger.go](../internal/logger/data_logger.go), чтение — `HandleLogData` в [cmd/server/main.go](../cmd/server/main.go).
|
||||
|
||||
**Формат файла:** конкатенация записей по 9 байт, без заголовка и футера:
|
||||
|
||||
```
|
||||
┌────────────────────────────────┬───────────┐
|
||||
│ timestamp: uint64 LE, 8 байт │ value: 1б │
|
||||
└────────────────────────────────┴───────────┘
|
||||
```
|
||||
|
||||
- `timestamp` — микросекунды Unix (`UnixMicro`), little-endian;
|
||||
- `value` — сырой [байт GPIO](#байт-gpio).
|
||||
|
||||
**Буферизация записи:** сэмплы копятся в буфере 64 КБ и сбрасываются на диск раз в 1 секунду либо при заполнении буфера; после каждого сброса вызывается `fsync` (durability при отключении питания).
|
||||
|
||||
**Именование:** `gpio-YYYY-MM-DD-HH.bin`, время **локальное**, один файл на час (см. [Ротация](#ротация)). Пример: `gpio-2026-07-17-12.bin` — данные за 12:00–12:59 17 июля 2026.
|
||||
|
||||
**Чтение на другой платформе:** каждая запись — `<Q` + `B` в терминах Python `struct`:
|
||||
|
||||
```python
|
||||
import struct
|
||||
with open("gpio-2026-07-17-12.bin", "rb") as f:
|
||||
while chunk := f.read(9):
|
||||
if len(chunk) < 9:
|
||||
break
|
||||
ts_us, value = struct.unpack("<QB", chunk)
|
||||
strength, count = (value >> 6) & 0x3, value & 0x3F
|
||||
```
|
||||
|
||||
## Человекочитаемый GPIO-лог
|
||||
|
||||
Файл `gpio_human.log` (см. [пути](#пути-хранения)), пишет [internal/pipe/reader.go](../internal/pipe/reader.go) через `HumanLogger`.
|
||||
|
||||
Обычная строка (пишется при изменении `count`/`strength` либо раз в `-human-log-interval` секунд):
|
||||
|
||||
```
|
||||
[2026-07-17 12:34:56.789] Обнаружение: 12 объектов | Сила: Сильный (уровень 2) | Сырое: 0x8C (140)
|
||||
```
|
||||
|
||||
Строка тревоги (при `count > 10`, с учётом анти-спама):
|
||||
|
||||
```
|
||||
[2026-07-17 12:34:56.789] ВНИМАНИЕ: Обнаружено превышение! 12 объектов | Сила: Сильный (уровень 2)
|
||||
```
|
||||
|
||||
## События системы
|
||||
|
||||
События пишутся [internal/logger/event_logger.go](../internal/logger/event_logger.go) **сразу в два файла**:
|
||||
|
||||
1. `events.log` — JSON-строки (одна на событие):
|
||||
|
||||
```json
|
||||
{"ts": 1789034096, "time": "2026-07-17 12:34:56", "event": "ПЛАНОВАЯ_РОТАЦИЯ"}
|
||||
```
|
||||
|
||||
2. `events_human.log` — текст (этот файл читает `/api/log/events`):
|
||||
|
||||
```
|
||||
[2026-07-17 12:34:56.789] EVENT: ПЛАНОВАЯ_РОТАЦИЯ
|
||||
```
|
||||
|
||||
**Словарь событий** (по вызовам `Event(...)` в коде):
|
||||
|
||||
| Событие | Источник | Когда |
|
||||
|---|---|---|
|
||||
| `СЕРВЕР_ЗАПУЩЕН` | main.go | Старт сервера |
|
||||
| `HUMAN_ЛОГЕР_ГОТОВ` | main.go | Инициализация HumanLogger |
|
||||
| `DATA_ЛОГЕР_ГОТОВ` | main.go | Инициализация RotatingLogger |
|
||||
| `ОШИБКА_ИНИЦИАЛИЗАЦИИ_DATA_ЛОГЕРА` | main.go | Сбой инициализации (сервер завершается) |
|
||||
| `RETENTION_ГОТОВ` | main.go | Запуск очистки логов |
|
||||
| `ПЛАНОВАЯ_РОТАЦИЯ` | rotation.go | Открыт новый почасовой файл |
|
||||
| `ПЛАНОВАЯ_РОТАЦИЯ_НЕУДАЧНА` | rotation.go | Не удалось открыть новый файл |
|
||||
| `PIPE_НЕ_НАЙДЕН` | reader.go | FIFO-файл отсутствует |
|
||||
| `ОШИБКА_ОТКРЫТИЯ_PIPE` | reader.go | FIFO существует, но не открывается |
|
||||
| `PIPE_ПОДКЛЮЧЕН` | reader.go | Пайп открыт / связь восстановлена |
|
||||
| `PIPE_ОТКЛЮЧЕН` | reader.go | Ошибка чтения из пайпа |
|
||||
| `ТИШИНА_5МИН` | monitor.go | Нет данных дольше порога `-silence-5min` |
|
||||
| `ТИШИНА_10МИН` | monitor.go | Нет данных дольше порога `-silence-10min` |
|
||||
| `ОБНАРУЖЕНО_<N>_ОБЪЕКТОВ_СИЛА_<M>` | reader.go | Алерт превышения (`count > 10`) |
|
||||
| `ОЧИСТКА_ЛОГОВ_УДАЛЕНО_<N>_ФАЙЛОВ_<M>_MB` | retention.go | Retention удалил файлы |
|
||||
| `ОШИБКА_ПОЛУЧЕНИЯ_ДИРЕКТОРИИ` | retention.go | Retention не смог получить каталог данных |
|
||||
|
||||
События смены состояния пайпа пишутся **только при изменении** состояния (дедупликация в `PipeReader`), тишина — при каждой проверке watchdog (раз в `-watchdog-interval` минут), пока тишина длится.
|
||||
|
||||
## Пути хранения
|
||||
|
||||
Все пути вычисляет [internal/logger/paths.go](../internal/logger/paths.go). Режим определяется по существованию каталога `/opt/gpio-monitoring`: есть — «пакетный» режим (deb-установка), нет — режим разработки (XDG).
|
||||
|
||||
| Что | Пакетный режим | Режим разработки |
|
||||
|---|---|---|
|
||||
| Данные приложения | `/var/lib/gpio-monitoring` | `~/.local/share/gpio-monitoring` |
|
||||
| Каталог логов | `/var/log/gpio-monitoring` | `~/.local/share/gpio-monitoring/logs` |
|
||||
| Бинарные `.bin` | `/var/log/gpio-monitoring/data/` | `~/.local/share/gpio-monitoring/logs/data/` |
|
||||
| События (JSON) | `.../events.log` | `.../logs/events.log` |
|
||||
| События (текст) | `.../events_human.log` | `.../logs/events_human.log` |
|
||||
| GPIO human-лог | `.../gpio_human.log` | `.../logs/gpio_human.log` |
|
||||
|
||||
## Ротация
|
||||
|
||||
[internal/logger/rotation.go](../internal/logger/rotation.go): `RotatingLogger` держит текущий `DataLogger` и раз в `-rotation-check-interval` минут (по умолчанию 1) сверяет текущий час с часом открытого файла. При смене часа старый файл закрывается (с финальным flush) и открывается новый `gpio-YYYY-MM-DD-HH.bin`; пишется событие `ПЛАНОВАЯ_РОТАЦИЯ`.
|
||||
|
||||
## Retention (очистка)
|
||||
|
||||
[internal/logger/retention.go](../internal/logger/retention.go): фоновая FIFO-очистка каталога `data/`. Проверка раз в `-retention-interval` минут (по умолчанию 15), первая — через 1 минуту после старта.
|
||||
|
||||
Алгоритм `Cleanup()`:
|
||||
|
||||
1. **По возрасту** (`-retention-hours`, 0 = отключено): файлы старше порога удаляются с самого старого края. Файл считается устаревшим, только если он старше порога **и по времени из имени, и по ModTime** — защита от скачка системных часов после NTP-синхронизации на устройстве без RTC.
|
||||
2. **По размеру** (`-retention-mb`, 0 = отключено): пока суммарный размер `.bin`-файлов превышает лимит, удаляется самый старый файл.
|
||||
3. В любом случае сохраняются минимум **2 самых свежих файла** (`minKeepFiles`) — защита от полного стирания каталога.
|
||||
|
||||
Если имя файла не соответствует шаблону `gpio-YYYY-MM-DD-HH.bin`, вместо времени из имени используется ModTime. Поведение закреплено тестами [internal/logger/retention_test.go](../internal/logger/retention_test.go).
|
||||
115
docs/development.md
Normal file
115
docs/development.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Руководство разработчика
|
||||
|
||||
Сборка, запуск без железа, тесты и соглашения проекта.
|
||||
|
||||
**Аудитория:** разработчики.
|
||||
|
||||
## Содержание
|
||||
|
||||
- [Окружение](#окружение)
|
||||
- [Порядок сборки](#порядок-сборки)
|
||||
- [Цели Makefile](#цели-makefile)
|
||||
- [Сборка deb-пакета](#сборка-deb-пакета)
|
||||
- [Запуск без железа (эмуляция)](#запуск-без-железа-эмуляция)
|
||||
- [Тесты](#тесты)
|
||||
- [Соглашения](#соглашения)
|
||||
|
||||
## Окружение
|
||||
|
||||
- **Go 1.21+** — модуль `gpio-monitor`, **внешних Go-зависимостей нет** (только стандартная библиотека, см. [go.mod](../go.mod));
|
||||
- **Node.js 20+ и npm** — только для компиляции TypeScript (единственная dev-зависимость — `typescript`);
|
||||
- `make`, `git`; для deb-пакета — `dpkg-deb`; опционально `golangci-lint` для `make lint`.
|
||||
|
||||
Подготовка после клонирования:
|
||||
|
||||
```bash
|
||||
make init # проверит Node.js, скачает Go-модули и npm-пакеты
|
||||
```
|
||||
|
||||
## Порядок сборки
|
||||
|
||||
**Критичный нюанс:** сервер встраивает веб-файлы через `go:embed web/dist ...` ([cmd/server/main.go:28](../cmd/server/main.go)). Каталог `dist/` генерируется компилятором TypeScript и в git не хранится, поэтому **на чистом checkout `go build` упадёт с ошибкой embed**. Всегда собирайте фронтенд первым:
|
||||
|
||||
```bash
|
||||
make build # = build-frontend (tsc) + build-backend (go build)
|
||||
```
|
||||
|
||||
По умолчанию бинарник собирается кросс-компиляцией под **linux/arm64** (Raspberry Pi) в `build/gpio-monitor-server`. Для локальной платформы используйте `make build-amd64` / `build-mac` / `build-windows` или `make dev`.
|
||||
|
||||
Есть также исторический скрипт [build.sh](../build.sh) — делает то же, что `make build`, но с захардкоженным путём проекта `~/temp/golang`; предпочитайте Makefile.
|
||||
|
||||
## Цели Makefile
|
||||
|
||||
Справка встроена: `make help`. Основные цели ([Makefile](../Makefile)):
|
||||
|
||||
| Цель | Что делает |
|
||||
|---|---|
|
||||
| `init` | Проверка Node.js/npm + установка всех зависимостей |
|
||||
| `build` | Полная сборка: фронтенд + бекенд (GOOS/GOARCH переопределяемы) |
|
||||
| `build-frontend` | `npm install` (при отсутствии node_modules) + `npm run build` (tsc) |
|
||||
| `build-backend` | `go build -ldflags="-s -w"` в `build/` |
|
||||
| `run` | Сборка + запуск бинарника |
|
||||
| `dev` | `go run ./cmd/server -port :8080` без оптимизаций (dist уже должен существовать) |
|
||||
| `test` | `go test -v ./...` |
|
||||
| `fmt` | `go fmt ./...` |
|
||||
| `lint` | `golangci-lint run ./...` (если установлен) |
|
||||
| `clean` | Удаляет `build/`, `web/dist`, `web/node_modules`, временные deb-файлы |
|
||||
| `build-arm64` / `build-amd64` / `build-mac` / `build-windows` / `build-all` | Кросс-сборки |
|
||||
| `install` / `uninstall` | Копирование бинарника в `/usr/local/bin` |
|
||||
| `deb` / `deb-info` / `deb-contents` / `deb-clean` / `version` | Работа с deb-пакетом (ниже) |
|
||||
|
||||
> Цели `docker-build`/`docker-run` объявлены, но Dockerfile в репозитории отсутствует — они нерабочие ([tech-debt.md](tech-debt.md#сборка-и-инфраструктура)).
|
||||
|
||||
## Сборка deb-пакета
|
||||
|
||||
```bash
|
||||
make deb # от обычного пользователя, без sudo
|
||||
make version # показать вычисленную версию и имя файла
|
||||
make deb-info # информация о собранном пакете (dpkg-deb -I)
|
||||
make deb-contents # список файлов пакета (dpkg-deb -c)
|
||||
make deb-clean # очистка временных файлов сборки пакета
|
||||
```
|
||||
|
||||
**Версионирование через git:** версия пакета = `<база>-build<N>`, где база берётся из `Version:` в [debian/control](../debian/control) (сейчас `1.0.0`), а `N` = `git rev-list --count HEAD` (число коммитов). Итоговый файл: `build/gpio-monitor-server_1.0.0-build<N>_arm64.deb`. Каждый коммит автоматически увеличивает номер сборки.
|
||||
|
||||
`make deb` выполняет `clean → build-frontend → build-backend → prepare-debian → dpkg-deb --build`. В пакет попадают: бинарник, веб-файлы (копия для справки — сервер использует встроенные), unit systemd, конфиг, утилита `gpio-logs`, maintainer-скрипты из [debian/](../debian/). Что происходит при установке — в [operations.md](operations.md#установка-из-deb-пакета).
|
||||
|
||||
## Запуск без железа (эмуляция)
|
||||
|
||||
FIFO и поток данных можно смоделировать на любой Linux-машине:
|
||||
|
||||
```bash
|
||||
mkfifo /tmp/gpio_pipe
|
||||
python3 scripts/emulator.py scripts/array.txt /tmp/gpio_pipe # генератор данных
|
||||
make dev # сервер (в другом терминале)
|
||||
```
|
||||
|
||||
Вспомогательные скрипты [scripts/](../scripts/):
|
||||
|
||||
| Скрипт | Назначение |
|
||||
|---|---|
|
||||
| `emulator.py` | Эмулятор капчера: читает массив байт из файла (формат `[0xE0, 0x01, ...]`) и пишет их в pipe/stdout с заданной задержкой, с переподключением и повтором |
|
||||
| `array.txt` | Пример массива данных для эмулятора |
|
||||
| `imi_wire.py` | Имитатор параллельной шины на **реальных GPIO** (gpiod, Raspberry Pi 4): выставляет байт на пины данных и дёргает строб WR — для теста настоящего `gpio-interrupt` |
|
||||
| `emulatohackrf.sh` | Связка эмулятора с передатчиком HackRF (`hackrf-frame64-tx`): передача байтов GPIO по радиоканалу |
|
||||
| `vu_meter.py` | Консольный VU-метр микрофона (sounddevice/numpy) — независимая проверка аудиотракта |
|
||||
| `view_logs.sh` | Сводная статистика dev-логов (`~/.local/share/gpio-monitoring/logs`) |
|
||||
|
||||
Пути внутри `emulatohackrf.sh` и systemd-юнитов захардкожены под конкретное устройство (`/home/user/...`) — правьте под своё окружение.
|
||||
|
||||
## Тесты
|
||||
|
||||
```bash
|
||||
make test # go test -v ./...
|
||||
```
|
||||
|
||||
Сейчас тестами покрыт только retention: [internal/logger/retention_test.go](../internal/logger/retention_test.go) (очистка по возрасту/размеру, minKeepFiles, устойчивость к скачку часов и битым именам). Остальные пакеты тестов не имеют — см. [tech-debt.md](tech-debt.md#тесты-и-ci). CI в репозитории нет — прогоняйте `make test` и `go vet ./...` перед коммитом вручную.
|
||||
|
||||
## Соглашения
|
||||
|
||||
- **Язык** — русский: комментарии, логи, сообщения об ошибках, документация.
|
||||
- **Конфигурация** — только CLI-флаги (без env-переменных и конфиг-файлов); новые параметры добавляются флагом в `main()` и полем в `logger.Config`.
|
||||
- **Зависимости** — Go-код держится на стандартной библиотеке; прежде чем добавить стороннюю зависимость, убедитесь, что она действительно необходима.
|
||||
- **Структура**: `cmd/server` — точка входа и HTTP-слой; `internal/adapter` — буфер; `internal/pipe` — приём данных; `internal/logger` — хранение/события; `internal/audio` — микрофон. Фронтенд — [frontend.md](frontend.md).
|
||||
- **Целевая платформа** — linux/arm64; сборка и на других платформах должна оставаться рабочей (кросс-цели Makefile).
|
||||
- Форматы данных и API описаны в [data-formats.md](data-formats.md) и [api.md](api.md) — при изменении кода обновляйте эти документы.
|
||||
79
docs/frontend.md
Normal file
79
docs/frontend.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Фронтенд (веб-дашборд)
|
||||
|
||||
Устройство встроенного веб-интерфейса: страницы, модули TypeScript, сборка, взаимодействие с API.
|
||||
|
||||
**Аудитория:** разработчики.
|
||||
|
||||
## Содержание
|
||||
|
||||
- [Обзор](#обзор)
|
||||
- [Страницы](#страницы)
|
||||
- [Модули TypeScript](#модули-typescript)
|
||||
- [Взаимодействие с API](#взаимодействие-с-api)
|
||||
- [Сборка](#сборка)
|
||||
- [Цикл разработки](#цикл-разработки)
|
||||
|
||||
## Обзор
|
||||
|
||||
Фронтенд — SPA на **ванильном TypeScript без фреймворков и рантайм-зависимостей**: ES-модули, Canvas для графиков, Web Audio API для звуковой индикации. Исходники — [cmd/server/web/js/](../cmd/server/web/js/), компилируются `tsc` в `web/dist/` (ES2020, strict), стили — `web/css/`, разметка — `web/*.html`.
|
||||
|
||||
Готовые файлы **вшиваются в бинарник сервера** (`go:embed web/dist web/css web/fonts web/*.html web/*.png`) и раздаются с корня `/` — отдельного веб-сервера для фронтенда нет.
|
||||
|
||||
## Страницы
|
||||
|
||||
| Страница | Назначение | Точка входа JS |
|
||||
|---|---|---|
|
||||
| `index.html` | Редирект на дашборд | — |
|
||||
| `dashboard.html` | Мониторинг в реальном времени: статус, каналы, график, VU-метр звука, камера | `dist/app.js` |
|
||||
| `logs.html` | Просмотр исторических данных: список `.bin`-файлов, сэмплы с пагинацией, события | `dist/logs.js` |
|
||||
|
||||
## Модули TypeScript
|
||||
|
||||
Карта модулей `js/` (в скобках — размер на момент написания):
|
||||
|
||||
| Модуль | Роль |
|
||||
|---|---|
|
||||
| `app.ts` (434 строки) | Оркестратор дашборда: инициализация всех модулей, цикл опроса сервера (каждые 500 мс), раздача данных в state/ui/chart/audio |
|
||||
| `data.ts` (32) | API-слой дашборда: `fetchHealth()`, `fetchHistory()` + типы ответов |
|
||||
| `state.ts` (222) | Состояние дашборда: история сигнала, peak-holder'ы амплитуды и уровня сигнала с таймерами удержания |
|
||||
| `ui.ts` (414) | Каталог DOM-элементов (`DOM`) и все операции обновления интерфейса: статус, бары, каналы, индикаторы |
|
||||
| `chart.ts` (137) | Отрисовка графика истории на Canvas (сетка, шкала 0–63) |
|
||||
| `audio.ts` (306) | `AudioEngine` — звуковая индикация через Web Audio API: тон зависит от уровня силы (600/1000/1200 Гц), громкость регулируется |
|
||||
| `camera.ts` (164) | Панель камеры: подключение/отключение MJPEG `<img src="/api/cam">`, перекрестие, периодическая проверка потока |
|
||||
| `layout.ts` (235) | Разделитель панелей (drag-resize), открытие/закрытие правой панели |
|
||||
| `accordion.ts` (23) | Сворачиваемый блок «Принятые данные» |
|
||||
| `logs.ts` (747) | Вся страница logs.html: список файлов, таблица сэмплов и событий с пагинацией, график по файлу, автообновление |
|
||||
|
||||
Зависимости между модулями — однонаправленные: `app.ts` импортирует остальные; `logs.ts` автономен (использует только `chart.ts`). Типы ответов API объявлены локально в `data.ts` и `logs.ts` (дублирование — см. [tech-debt.md](tech-debt.md#фронтенд-cmdserverwebjs)).
|
||||
|
||||
## Взаимодействие с API
|
||||
|
||||
Полная спецификация — [api.md](api.md).
|
||||
|
||||
| Модуль | Эндпоинты |
|
||||
|---|---|
|
||||
| `data.ts` (дашборд) | `GET /api/health`, `GET /api/history` — опрос каждые 500 мс из `app.ts` |
|
||||
| `camera.ts` | `GET /api/cam` (MJPEG через `<img>`) |
|
||||
| `logs.ts` | `GET /api/log/files`, `GET /api/log/data`, `GET /api/log/events`, `GET /api/health` (серверное время) |
|
||||
|
||||
## Сборка
|
||||
|
||||
```bash
|
||||
cd cmd/server/web
|
||||
npm install # однократно (единственная dev-зависимость — typescript)
|
||||
npm run build # tsc: js/*.ts → dist/*.js
|
||||
npm run watch # tsc --watch
|
||||
npm run clean # удалить dist/
|
||||
```
|
||||
|
||||
Конфигурация — [tsconfig.json](../cmd/server/web/tsconfig.json): target/module ES2020, `strict: true`, `rootDir: js`, `outDir: dist`. Каталоги `dist/` и `node_modules/` в git не хранятся.
|
||||
|
||||
**Важно:** без собранного `dist/` не соберётся и Go-сервер (`go:embed`) — см. [development.md](development.md#порядок-сборки).
|
||||
|
||||
## Цикл разработки
|
||||
|
||||
1. Терминал 1: `cd cmd/server/web && npm run watch` — пересборка TS при каждом сохранении;
|
||||
2. Терминал 2: `make dev` — Go-сервер;
|
||||
3. После изменения `.ts` — обновить страницу; после изменения `.html`/`.css` при работе через встроенные файлы — перезапустить сервер (файлы вшиваются на этапе сборки, `make dev` через `go run` перечитает их при рестарте).
|
||||
|
||||
Данные без железа — эмулятор: [development.md](development.md#запуск-без-железа-эмуляция).
|
||||
255
docs/operations.md
Normal file
255
docs/operations.md
Normal file
@@ -0,0 +1,255 @@
|
||||
# Руководство администратора
|
||||
|
||||
Установка, настройка и обслуживание GPIO Monitor на Raspberry Pi.
|
||||
|
||||
**Аудитория:** администраторы и операторы.
|
||||
|
||||
## Содержание
|
||||
|
||||
- [Требования](#требования)
|
||||
- [Установка из deb-пакета](#установка-из-deb-пакета)
|
||||
- [Ручная установка](#ручная-установка)
|
||||
- [Параметры командной строки](#параметры-командной-строки)
|
||||
- [Firewall (nftables)](#firewall-nftables)
|
||||
- [Wi-Fi Access Point](#wi-fi-access-point)
|
||||
- [Камера (go2rtc)](#камера-go2rtc)
|
||||
- [Микрофон](#микрофон)
|
||||
- [Логи: где лежат и как смотреть](#логи-где-лежат-и-как-смотреть)
|
||||
- [Диагностика проблем](#диагностика-проблем)
|
||||
|
||||
## Требования
|
||||
|
||||
- Raspberry Pi (целевая платформа — ARM64, Raspberry Pi OS Bookworm);
|
||||
- C-программа захвата `gpio-interrupt` (WiringPi) на устройстве — поставляется отдельно, в репозитории её нет;
|
||||
- `alsa-utils` (`arecord`) — для монитора уровня звука;
|
||||
- go2rtc — для видеопотока камеры (бинарник в [scripts/go2rtc](../scripts/go2rtc));
|
||||
- systemd.
|
||||
|
||||
Назначение GPIO-пинов:
|
||||
|
||||
```
|
||||
WR/STROBE = GPIO27
|
||||
|
||||
DATA BUS:
|
||||
D0 = GPIO21 D4 = GPIO25
|
||||
D1 = GPIO7 D5 = GPIO24
|
||||
D2 = GPIO6 D6 = GPIO23
|
||||
D3 = GPIO5 D7 = GPIO22
|
||||
```
|
||||
|
||||
## Установка из deb-пакета
|
||||
|
||||
Рекомендуемый способ развёртывания. Сборка пакета — см. [development.md](development.md#сборка-deb-пакета); итоговый файл: `build/gpio-monitor-server_<база>-build<N>_arm64.deb`, где `N` — число git-коммитов (например `gpio-monitor-server_1.0.0-build83_arm64.deb`).
|
||||
|
||||
```bash
|
||||
sudo dpkg -i build/gpio-monitor-server_1.0.0-build83_arm64.deb
|
||||
```
|
||||
|
||||
Пакет при установке ([debian/preinst](../debian/preinst), [debian/postinst](../debian/postinst)):
|
||||
|
||||
- создаёт системного пользователя `gpio-monitor` (группы `gpio`, `dialout`) и каталоги `/var/log/gpio-monitoring`, `/var/lib/gpio-monitoring`, `/opt/gpio-monitoring/web`;
|
||||
- ставит бинарник в `/usr/bin/gpio-monitor-server` и утилиту `gpio-logs` в `/usr/bin/gpio-logs`;
|
||||
- кладёт unit `gpio-monitor-server.service` в `/etc/systemd/system/` и справочный конфиг в `/etc/gpio-monitoring/config`;
|
||||
- включает и **сразу запускает** сервис.
|
||||
|
||||
Unit [debian/gpio-monitor-server.service](../debian/gpio-monitor-server.service) запускает сервер с параметрами `-retention-hours=72 -retention-mb=2000 -retention-interval=60 -rotation-check-interval=1 -buffer-size=10240 -human-log-interval=5` (обратите внимание: retention здесь 72 часа, а не встроенные по умолчанию 48). Чтобы изменить параметры, отредактируйте `ExecStart` в unit-файле и выполните `sudo systemctl daemon-reload && sudo systemctl restart gpio-monitor-server`.
|
||||
|
||||
> Файл `/etc/gpio-monitoring/config` — справочный: сервис его **не читает**, параметры берутся только из `ExecStart` (см. [tech-debt.md](tech-debt.md#пакеты-internal)).
|
||||
|
||||
Управление:
|
||||
|
||||
```bash
|
||||
sudo systemctl status gpio-monitor-server
|
||||
sudo systemctl restart gpio-monitor-server
|
||||
gpio-logs -f # журнал сервиса
|
||||
```
|
||||
|
||||
Удаление: `sudo apt remove gpio-monitor-server`; полная очистка с логами и пользователем: `sudo apt purge gpio-monitor-server`.
|
||||
|
||||
## Ручная установка
|
||||
|
||||
### 1. Зависимости и сборка
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y golang git build-essential wiringpi nodejs npm alsa-utils
|
||||
git clone <your-repo> && cd golang
|
||||
make init && make build # подробности — development.md
|
||||
```
|
||||
|
||||
### 2. Канал данных: systemd socket-activation
|
||||
|
||||
FIFO `/tmp/gpio_pipe` и программу захвата запускает systemd ([scripts/monitor-gpio.socket](../scripts/monitor-gpio.socket), [scripts/monitor-gpio.service](../scripts/monitor-gpio.service)): socket-юнит создаёт FIFO, service-юнит направляет stdout `gpio-interrupt` в него.
|
||||
|
||||
```bash
|
||||
sudo cp scripts/monitor-gpio.service scripts/monitor-gpio.socket /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable monitor-gpio.socket monitor-gpio.service
|
||||
sudo systemctl start monitor-gpio.service
|
||||
```
|
||||
|
||||
> Пути в `monitor-gpio.service` (`/home/user/WiringPi/examples/gpio-interrupt`) и владелец FIFO в `monitor-gpio.socket` (`SocketUser=user`) заданы под конкретное устройство — проверьте их перед установкой.
|
||||
|
||||
### 3. Go-сервер как сервис
|
||||
|
||||
Шаблон — [scripts/go2monitor.service](../scripts/go2monitor.service) (запуск собранного бинарника от обычного пользователя). Поправьте `WorkingDirectory` и путь в `ExecStart` под своё расположение проекта, затем:
|
||||
|
||||
```bash
|
||||
sudo cp scripts/go2monitor.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now go2monitor.service
|
||||
```
|
||||
|
||||
Либо запустите вручную:
|
||||
|
||||
```bash
|
||||
./build/gpio-monitor-server -pipe /tmp/gpio_pipe -retention-hours=72 -retention-mb=2000
|
||||
```
|
||||
|
||||
### 4. Проверка
|
||||
|
||||
```bash
|
||||
sudo systemctl status monitor-gpio.service
|
||||
curl http://localhost:8080/api/health
|
||||
```
|
||||
|
||||
Дашборд: `http://<IP>:8080/` (редирект на `dashboard.html`), просмотр истории: `http://<IP>:8080/logs.html`.
|
||||
|
||||
## Параметры командной строки
|
||||
|
||||
Полный список флагов `gpio-monitor-server` (источник — [cmd/server/main.go](../cmd/server/main.go)):
|
||||
|
||||
| Параметр | По умолчанию | Описание |
|
||||
|---|---|---|
|
||||
| `-pipe` | `/tmp/gpio_pipe` | Путь к FIFO pipe |
|
||||
| `-port` | `:8080` | Адрес/порт веб-сервера |
|
||||
| `-retention-hours` | `48` | Часы хранения `.bin`-логов (0 = отключено) |
|
||||
| `-retention-mb` | `5000` | Максимальный суммарный размер `.bin`-логов, МБ (0 = отключено) |
|
||||
| `-retention-interval` | `15` | Интервал проверки retention, мин |
|
||||
| `-rotation-check-interval` | `1` | Интервал проверки ротации, мин |
|
||||
| `-buffer-size` | `10240` | Размер кольцевого буфера, элементов |
|
||||
| `-silence-5min` | `5` | Порог тишины для события `ТИШИНА_5МИН`, мин |
|
||||
| `-silence-10min` | `10` | Порог тишины для события `ТИШИНА_10МИН`, мин |
|
||||
| `-watchdog-interval` | `1` | Интервал проверки watchdog, мин |
|
||||
| `-alert-cooldown` | `10` | Анти-спам между одинаковыми алертами, сек |
|
||||
| `-human-log-interval` | `5` | Интервал записи human-логов, сек |
|
||||
| `-camera-url` | `http://127.0.0.1:1984/api/stream.mjpeg?src=cam_mjpeg` | URL MJPEG-потока для прокси `/api/cam` |
|
||||
|
||||
## Firewall (nftables)
|
||||
|
||||
На Raspberry Pi OS Bookworm по умолчанию активен nftables, блокирующий входящие порты, кроме SSH (22).
|
||||
|
||||
Разрешить порт 8080:
|
||||
|
||||
```bash
|
||||
sudo nft add rule inet filter input tcp dport 8080 accept
|
||||
|
||||
# Проверить и сохранить
|
||||
sudo nft list ruleset
|
||||
sudo nft list ruleset | sudo tee /etc/nftables.conf > /dev/null
|
||||
sudo systemctl enable nftables
|
||||
sudo systemctl restart nftables
|
||||
```
|
||||
|
||||
Ограничить доступ:
|
||||
|
||||
```bash
|
||||
# Только локальная сеть
|
||||
sudo nft add rule inet filter input ip saddr 10.1.1.0/24 tcp dport 8080 accept
|
||||
# Только конкретный IP
|
||||
sudo nft add rule inet filter input ip saddr 192.168.1.100 tcp dport 8080 accept
|
||||
# Только через Wi-Fi AP
|
||||
sudo nft add rule inet filter input iifname "wlan0" tcp dport 8080 accept
|
||||
```
|
||||
|
||||
## Wi-Fi Access Point
|
||||
|
||||
Raspberry Pi может работать точкой доступа:
|
||||
|
||||
- SSID: `fix_me`, IP устройства: `192.168.77.1`;
|
||||
- подключение: `ssh user@192.168.77.1`;
|
||||
- дашборд: `http://192.168.77.1:8080`.
|
||||
|
||||
## Камера (go2rtc)
|
||||
|
||||
Видеопоток отдаёт go2rtc (конфиг — [scripts/go2rtc.yaml](../scripts/go2rtc.yaml)): поток `cam_mjpeg` — MJPEG 640×480 15 fps с `/dev/video0` через ffmpeg, API на порту `1984` (также RTSP `:8554`, WebRTC `:8555`).
|
||||
|
||||
```bash
|
||||
./scripts/go2rtc -config scripts/go2rtc.yaml
|
||||
```
|
||||
|
||||
Проверка: `http://<IP>:1984` (встроенный интерфейс go2rtc), `http://<IP>:1984/api/stream.mjpeg?src=cam_mjpeg` (прямой поток). Go-сервер проксирует этот поток на `/api/cam`.
|
||||
|
||||
## Микрофон
|
||||
|
||||
Уровень звука снимается через `arecord` (пакет `alsa-utils`); USB-микрофон находится автоматически. Проверка:
|
||||
|
||||
```bash
|
||||
arecord -l
|
||||
# **** List of CAPTURE Hardware Devices ****
|
||||
# card 3: Device [USB PnP Sound Device], device 0: USB Audio [USB Audio]
|
||||
```
|
||||
|
||||
Если список пуст — микрофон не определился; сервер при этом работает нормально, но `/api/health` отдаёт `sound_level: 0`. Пользователь, от которого запущен сервис, должен состоять в группе `audio` (`sudo usermod -aG audio <user>`). При физическом отвале микрофона сервер сам пытается восстановить захват каждые 5 секунд.
|
||||
|
||||
## Логи: где лежат и как смотреть
|
||||
|
||||
Пути зависят от режима (подробно — [data-formats.md](data-formats.md#пути-хранения)):
|
||||
|
||||
| Файл | Пакетная установка | Ручной запуск (dev) |
|
||||
|---|---|---|
|
||||
| Бинарные данные `gpio-*.bin` | `/var/log/gpio-monitoring/data/` | `~/.local/share/gpio-monitoring/logs/data/` |
|
||||
| События | `/var/log/gpio-monitoring/events.log`, `events_human.log` | `~/.local/share/gpio-monitoring/logs/...` |
|
||||
| GPIO human-лог | `/var/log/gpio-monitoring/gpio_human.log` | `~/.local/share/gpio-monitoring/logs/gpio_human.log` |
|
||||
| Журнал сервиса | `journalctl -u gpio-monitor-server` | stdout |
|
||||
|
||||
Просмотр:
|
||||
|
||||
```bash
|
||||
# Пакетная установка — утилита gpio-logs:
|
||||
gpio-logs -f # журнал сервиса (journalctl)
|
||||
gpio-logs -e # события
|
||||
gpio-logs -g # GPIO human-лог
|
||||
gpio-logs -d # сырые бинарные данные (xxd)
|
||||
|
||||
# Dev-режим:
|
||||
./scripts/view_logs.sh # сводная статистика логов
|
||||
```
|
||||
|
||||
Место на диске контролируется retention (см. флаги `-retention-*`); при значениях по умолчанию deb-юнита — не более 2000 МБ и 72 часов бинарных данных.
|
||||
|
||||
## Диагностика проблем
|
||||
|
||||
### Dashboard не открывается
|
||||
|
||||
1. Сервер запущен?
|
||||
```bash
|
||||
ps aux | grep gpio-monitor-server
|
||||
sudo systemctl status gpio-monitor-server # или go2monitor / monitor-gpio при ручной установке
|
||||
```
|
||||
2. Firewall: `sudo nft list ruleset | grep 8080`
|
||||
3. Сервер слушает нужный интерфейс: `sudo ss -tlnp | grep 8080` (должно быть `*:8080` или `0.0.0.0:8080`)
|
||||
4. Маршрутизация: `ip route show`
|
||||
5. Логи:
|
||||
```bash
|
||||
sudo journalctl -u gpio-monitor-server -f
|
||||
sudo tail -f /var/log/gpio-monitoring/events_human.log # пакетная установка
|
||||
tail -f ~/.local/share/gpio-monitoring/logs/events_human.log # dev-режим
|
||||
```
|
||||
6. FIFO существует: `ls -la /tmp/gpio_pipe`
|
||||
|
||||
### Статус «Нет связи» на дашборде
|
||||
|
||||
Данные не поступают дольше 15 секунд. Проверьте цепочку: `monitor-gpio.service` запущен → FIFO существует → в `events_human.log` нет свежих `PIPE_НЕ_НАЙДЕН`/`PIPE_ОТКЛЮЧЕН` → события `ТИШИНА_5МИН` укажут, что pipe жив, но данных нет (проблема на стороне захвата/шины).
|
||||
|
||||
### Индикатор «🔊 ЗВУК» не реагирует
|
||||
|
||||
1. `which arecord && arecord -l` — arecord установлен и видит микрофон;
|
||||
2. в журнале сервера при старте нет строки `Аудио-монитор не запущен (микрофон недоступен?)`;
|
||||
3. пользователь сервиса в группе `audio`: `groups <user>`.
|
||||
|
||||
### Камера не показывает
|
||||
|
||||
1. go2rtc запущен и отвечает: `curl http://localhost:1984/api/streams`;
|
||||
2. `/dev/video0` существует;
|
||||
3. `/api/stream` возвращает `available: true`. Учтите: проверка доступности всегда идёт на `localhost:1984` независимо от `-camera-url` (см. [api.md](api.md#известные-особенности)).
|
||||
59
docs/tech-debt.md
Normal file
59
docs/tech-debt.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Реестр технического долга
|
||||
|
||||
Известные проблемы и упрощения, принятые в текущей реализации. Реестр ведётся, чтобы долг был видим и осознан; исправления — отдельные задачи. При закрытии пункта удаляйте его отсюда, при появлении нового — добавляйте с указанием места и влияния.
|
||||
|
||||
**Аудитория:** разработчики.
|
||||
|
||||
Состояние на 2026-07-17. Маркеров `TODO`/`FIXME` в коде нет — этот файл единственный источник.
|
||||
|
||||
## Go-сервер (cmd/server/main.go)
|
||||
|
||||
| # | Проблема | Где | Влияние | Направление исправления |
|
||||
|---|---|---|---|---|
|
||||
| 1 | Монолит: 8 HTTP-хендлеров, разбор флагов, чтение `.bin`, прокси камеры — всё в одном файле на ~750 строк | `main.go` | Затрудняет навигацию и тестирование хендлеров | Вынести HTTP-слой в `internal/api`, чтение `.bin` — в `internal/logger` |
|
||||
| 2 | Мёртвый код: `handleCamProxy` не вызывается (роутер использует `handleCamProxyWithURL`) | `main.go:474` | Путает при чтении, дублирует логику | Удалить после подтверждения |
|
||||
| 3 | `HandleStream` проверяет доступность камеры по захардкоженному `localhost:1984`, игнорируя `-camera-url` | `main.go:460` | При нестандартном URL камеры `available` врёт | Строить URL проверки из `-camera-url` |
|
||||
| 4 | Разбор байта GPIO продублирован: `HandleLogData` сдвигает биты сам вместо `logger.ParseGPIO` | `main.go:297–298` ↔ `internal/logger/parser.go` | Риск рассинхронизации формата | Использовать `ParseGPIO` |
|
||||
| 5 | `/api/log/data` читает весь `.bin`-файл в память до пагинации | `main.go:278–308` | На больших файлах — всплеск памяти на каждый запрос | Читать нужный диапазон по смещению (записи фиксированные, 9 байт) |
|
||||
| 6 | CORS открыт для всех источников (`*`) | `main.go:522–535` | Приемлемо для изолированной сети; риск при выходе наружу | Осознанное решение зафиксировано; при необходимости — allowlist |
|
||||
| 7 | `/api/log/files` возвращает `null` вместо `[]` при отсутствии файлов; `/api/latest` отдаёт 200 с `{"error":"no data"}` | `main.go:67,105`, `main.go:435` | Клиентам нужны доп. проверки | Инициализировать слайс; вернуть 204/404 либо задокументированную схему |
|
||||
|
||||
## Пакеты internal/
|
||||
|
||||
| # | Проблема | Где | Влияние | Направление исправления |
|
||||
|---|---|---|---|---|
|
||||
| 8 | Два параллельных механизма расчёта скорости пишут в одни атомики: периодический (250 мс) и скользящее окно (1 с); публичный `GetWindowSpeed()` не используется | `internal/adapter/buffer.go:77–162` | Значение `bytes_per_sec` зависит от того, кто записал последним; лишний код и память (`recentBytes`) | Оставить один механизм |
|
||||
| 9 | Конфигурация размазана: значения по умолчанию во флагах (`retention-hours=48`), в deb-юните (`72`), в `scripts/go2monitor.service` (`72`) и в справочном `/etc/gpio-monitoring/config`, который сервис **не читает** | `main.go`, `debian/gpio-monitor-server.service`, `debian/gpio-monitor-server.conf` | Непонятно, что «истина»; правка конфига не влияет на сервис | Либо читать `EnvironmentFile=/etc/gpio-monitoring/config` в юните, либо удалить конфиг-файл |
|
||||
| 10 | `DataLogger.flush` молча глотает ошибку записи (комментарий «тут должен быть event» в коде) | `internal/logger/data_logger.go:66–70` | Потеря данных при полном диске останется незамеченной | Прокинуть EventLogger и писать событие |
|
||||
|
||||
## Тесты и CI
|
||||
|
||||
| # | Проблема | Где | Влияние | Направление исправления |
|
||||
|---|---|---|---|---|
|
||||
| 11 | Единственный тест-файл на проект — retention; без тестов `parser.go`, `buffer.go` (конкурентность), `rotation.go`, `data_logger.go` (бинарный формат), `reader.go` | `internal/logger/retention_test.go` | Регрессии форматов/конкурентности не ловятся | Начать с parser (тривиально) и data_logger (формат 9 байт) |
|
||||
| 12 | CI отсутствует (нет `.gitea/workflows/`) | — | Сборка и тесты не проверяются автоматически | Gitea Actions: `make build-frontend`, `go vet`, `go test` |
|
||||
|
||||
## Сборка и инфраструктура
|
||||
|
||||
| # | Проблема | Где | Влияние | Направление исправления |
|
||||
|---|---|---|---|---|
|
||||
| 13 | Цели `docker-build`/`docker-run` есть, Dockerfile — нет | `Makefile:249–256` | Цели заведомо падают | Удалить цели или добавить Dockerfile |
|
||||
| 14 | C-программа захвата `gpio-interrupt` — ключевой компонент системы — не версионируется в репозитории (живёт на устройстве в `/home/user/WiringPi/examples`) | `scripts/monitor-gpio.service` | Невоспроизводимость: систему нельзя собрать целиком из репозитория | Добавить исходник в репозиторий (например `capture/gpio-interrupt.c`) |
|
||||
| 15 | `go build` требует предварительно собранного `web/dist` (`go:embed`) — чистый checkout не собирается командой `go build ./...` | `cmd/server/main.go:28` | Неочевидная ошибка для новичка; ломает go-инструментарий на чистом дереве | Задокументировано ([development.md](development.md#порядок-сборки)); вариант — коммитить заглушку `dist/.keep` с `embed` через `all:` |
|
||||
| 16 | Захардкоженные пути под конкретное устройство: `build.sh` и `start-server.sh` (`~/temp/golang`), `emulatohackrf.sh`, systemd-юниты в `scripts/` (`/home/user/...`) | `build.sh`, `start-server.sh`, `scripts/*` | Скрипты не переносимы | Параметризовать или пометить как шаблоны |
|
||||
|
||||
## Фронтенд (cmd/server/web/js/)
|
||||
|
||||
| # | Проблема | Где | Влияние | Направление исправления |
|
||||
|---|---|---|---|---|
|
||||
| 17 | Крупные модули без разбивки: `logs.ts` (747 строк), `app.ts` (434), `ui.ts` (414) | `js/` | Сложно поддерживать | Разбить `logs.ts` на api/таблицы/пагинацию |
|
||||
| 18 | Типы ответов API объявлены дважды: в `data.ts` и `logs.ts` | `js/data.ts`, `js/logs.ts` | Рассинхронизация с сервером ловится только вручную | Общий `js/api-types.ts` |
|
||||
| 19 | Нет линтера/форматтера (eslint/prettier) и ни одного теста фронтенда; `make fmt` вызывает несуществующий `npm run format` | `package.json`, `Makefile:112` | Стиль и регрессии не контролируются | Добавить prettier + script `format` |
|
||||
| 20 | `logs.ts:284` передаёт в `/api/log/events` параметр `order=newest_first`, который сервер не читает | `js/logs.ts:284` | Мусорный параметр, вводит в заблуждение | Убрать параметр |
|
||||
|
||||
## SDR/
|
||||
|
||||
| # | Проблема | Где | Влияние | Направление исправления |
|
||||
|---|---|---|---|---|
|
||||
| 21 | makefile ожидает `receiver.c`, реальный файл — `reciever.c` (опечатка): цель сборки приёмника не срабатывает | `SDR/makefile`, `SDR/reciever.c` | `make` собирает только передатчик; приёмник — только вручную (обходная команда в [SDR/README.md](../SDR/README.md)) | Переименовать файл в `receiver.c` (или поправить makefile) |
|
||||
| 22 | Статус подпроекта не определён: SDR не связан с основной системой ни сборкой, ни CI | `SDR/` | Непонятно, поддерживается ли код | Зафиксировать статус в SDR/README (актуален/эксперимент/заморожен) |
|
||||
Reference in New Issue
Block a user