diff --git a/CLAUDE.md b/AGENTS.md similarity index 99% rename from CLAUDE.md rename to AGENTS.md index 7561f4f..833716d 100644 --- a/CLAUDE.md +++ b/AGENTS.md @@ -17,8 +17,8 @@ dotnet format SemiPlot.slnx # pre-commit hook enforces this `dotnet run` on the viewer needs all three launch keys — `--config-dir`, `--log-file` and `--logging-level` — and none has a default; a missing or bad one opens the failure window and exits -1. The run reads a copy of the tracked set, never the tracked set itself: `readme.md` holds the -copy recipe that produces `SemiPlot/Artifacts/dev-config`, which is also where the `.zed/` and `.run/` +1. The run reads a copy of the tracked set, never the tracked set itself: the first two lines of the +`converge` example below produce `SemiPlot/Artifacts/dev-config`, which is also where the `.zed/` and `.run/` launchers point `--config-dir`, and the password is then filled in the viewer's `Edit` -> `Settings`. Configuration is a tree of section folders under `--config-dir`: `app/` and `connection/`. Every diff --git a/SemiPlot/SemiPlot.Tests.Integration/RealtimeSubscriptionTests.cs b/SemiPlot/SemiPlot.Tests.Integration/RealtimeSubscriptionTests.cs index 5d46207..b3b94b4 100644 --- a/SemiPlot/SemiPlot.Tests.Integration/RealtimeSubscriptionTests.cs +++ b/SemiPlot/SemiPlot.Tests.Integration/RealtimeSubscriptionTests.cs @@ -200,7 +200,7 @@ private static IReadOnlyList SeededRows() // The nth Connected is the nth subscription's armed point, and NoFaultWasSeen is what makes that an // asserted precondition instead of an assumption. The waiters run their continuations asynchronously - // (CLAUDE.md, "An xunit v3 test project is an executable"). + // (AGENTS.md, "An xunit v3 test project is an executable"). private sealed class ArmedGate : IDisposable { private readonly List _states = []; diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 1e71e55..8172d54 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -1,7 +1,7 @@ # SemiPlot Architecture Machine-readable architecture docs (English). These describe the stable design, not the -decision process. For build/test/style conventions see the root `CLAUDE.md`; for +decision process. For build/test/style conventions see the root `AGENTS.md`; for implementation plans see `docs/plans/`. ## Documents diff --git a/docs/architecture/bench.md b/docs/architecture/bench.md index 28e7c6a..6a3c4ab 100644 --- a/docs/architecture/bench.md +++ b/docs/architecture/bench.md @@ -321,7 +321,7 @@ The launchers that start the viewer alone - `.run/Debug.run.xml` and the `.zed/` entries - point `--config-dir` at `SemiPlot/Artifacts/dev-config` through `$PROJECT_DIR$` and `$ZED_WORKTREE_ROOT`, never at the stand's copy: `%TEMP%\SemiPlot\ConfigFiles` exists only while the stand runs, and the stand has already started its own viewer against it. The developer creates that -directory once from `ConfigFiles/`, as `readme.md` and `CLAUDE.md` spell out; `SemiPlot/Artifacts/` +directory once from `ConfigFiles/`, as `AGENTS.md` spells out; `SemiPlot/Artifacts/` is gitignored, so the password filled in there stays out of the repository. ## The headless render and input guards diff --git a/docs/architecture/charting.md b/docs/architecture/charting.md index 02e295d..f64bfa9 100644 --- a/docs/architecture/charting.md +++ b/docs/architecture/charting.md @@ -303,7 +303,7 @@ models, backed by renderer-agnostic models in `SemiPlot.Core`. Responsibilities: and `RestoreInitialScale` on the chart, and the submenu header that names the drawn pen; the window's code-behind owns the three view-side requests (close, About dialog, settings dialog). - `MainWindow/AppMenuBar` — the File / Edit / View / Help menu. Each checkable item reads its flag - `Mode=OneWay` and writes it only through the command it invokes (`CLAUDE.md`, UI). + `Mode=OneWay` and writes it only through the command it invokes (`AGENTS.md`, UI). - `MainWindow/AppStatusBar` + `AppStatusBarViewModel` — current connection state and the active aggregation layer, named from resx. Built by `TrendWindow` over the coordinator's connection stream and the chart's navigation, and writes the fault and recovery entries (`data-integration.md`). diff --git a/docs/architecture/data-integration.md b/docs/architecture/data-integration.md index 8aedbda..aef83b8 100644 --- a/docs/architecture/data-integration.md +++ b/docs/architecture/data-integration.md @@ -110,7 +110,7 @@ settings columns and no `INSERT` or `DELETE` on `semiplot_tags` (`postgres-insta The read interface stays read-only because the chart consumes it. A write member on `IDataProvider` would put a write in reach of every chart consumer; with the write path apart, `Chart/` and `Legend/` never name `IPenCatalogueEditor`, `IDataProvider` carries no `Insert`, `Update`, `Write` or `Save`, and -two greps in `CLAUDE.md` hold both. An axis the operator rescales stays a session edit (`charting.md`). +two greps in `AGENTS.md` hold both. An axis the operator rescales stays a session edit (`charting.md`). `PostgresPenCatalogueEditor` in `SemiPlot.DataSource.Postgres` is the only implementation. `AddPostgresData()` registers it by factory beside `IDataProvider`, over the same `NpgsqlDataSource` diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index d257e36..7f39f89 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -301,7 +301,7 @@ and the handler on `RxState`. The same ordering forces the one static read this tree allows. The handler is taken before any window exists, so it cannot be given a panel by constructor injection; `App.ResolveMessagePanel` reads that field of `Application.Current`. -That is the declared exception to the constructor-injection rule in `CLAUDE.md`, and it covers this +That is the declared exception to the constructor-injection rule in `AGENTS.md`, and it covers this one method: nothing else may resolve a service through a static. ## Data flow diff --git a/docs/architecture/testing-strategy.md b/docs/architecture/testing-strategy.md index 7490dbc..845676a 100644 --- a/docs/architecture/testing-strategy.md +++ b/docs/architecture/testing-strategy.md @@ -196,7 +196,7 @@ scheduler. The view subscribes to `RedrawRequested`, and each redraw request sch 33 ms ahead. `ImmediateScheduler` runs it inline after sleeping 33 ms on the calling thread, so every redraw blocks the dispatcher and paints inside the call that asked for it, which production never does. The schedule is one-shot, so nothing hangs; the hangs belong to `PenCatalogueSync`'s wait loop and the -minimap band's next-read schedule (`CLAUDE.md`, Test). Two schedulers work in its place. +minimap band's next-read schedule (`AGENTS.md`, Test). Two schedulers work in its place. | Scheduler | When | Cost | | --- | --- | --- | diff --git a/docs/img/main-window.png b/docs/img/main-window.png new file mode 100644 index 0000000..9a10dfd Binary files /dev/null and b/docs/img/main-window.png differ diff --git a/docs/img/pens-and-groups.png b/docs/img/pens-and-groups.png new file mode 100644 index 0000000..77bcea6 Binary files /dev/null and b/docs/img/pens-and-groups.png differ diff --git a/docs/img/settings.png b/docs/img/settings.png new file mode 100644 index 0000000..9446300 Binary files /dev/null and b/docs/img/settings.png differ diff --git a/docs/img/two-windows.png b/docs/img/two-windows.png new file mode 100644 index 0000000..6577353 Binary files /dev/null and b/docs/img/two-windows.png differ diff --git a/docs/plans/backlog.md b/docs/plans/backlog.md index fe105ee..ba4860d 100644 --- a/docs/plans/backlog.md +++ b/docs/plans/backlog.md @@ -125,7 +125,7 @@ Paths are under `SemiPlot/SemiPlot.UI/` unless they name another project. - **~~Test unification (with the Avalonia 12 bump).~~ Resolved.** Avalonia is on 12.0.5 and all three test projects target xunit v3. `SemiPlot.Core.Tests` no longer exists; the suites are now split by dependency - graph and skip policy rather than by framework, which `CLAUDE.md` states. + graph and skip policy rather than by framework, which `AGENTS.md` states. ## Test harness and the demo bench diff --git a/readme.md b/readme.md index cc40017..9196811 100644 --- a/readme.md +++ b/readme.md @@ -3,166 +3,79 @@ [![CI](https://github.com/Semiteq/SemiPlot/actions/workflows/ci.yml/badge.svg)](https://github.com/Semiteq/SemiPlot/actions/workflows/ci.yml) ![C#](https://img.shields.io/badge/C%23-.NET-blue) ![.NET](https://img.shields.io/badge/.NET-10%2B-512BD4) +![Platform](https://img.shields.io/badge/platform-Windows-informational) -SemiPlot — приложение для просмотра графиков и трендов технологических параметров промышленных установок. Использует SimpleScada как источник данных. +
+ +
+ +SemiPlot — программа просмотра трендов технологических параметров промышленных установок. Она читает +архив Simple-Scada 2 в СУБД PostgreSQL и отображает архивные данные и значения в реальном времени. [Документация](./docs/architecture/README.md)
- + +

Главное окно: график, условные обозначения по группам, миникарта архива

---- +
+ +

Два окна: у каждого свой набор перьев и свой интервал времени

+
## Возможности -**Тренд-вьювер** - -- Перья (источники данных): добавление/удаление на лету, включение/выключение, логическая группировка -- Мультиоси: у каждого пера своя шкала min/max; на графике стоит ось активного пера, всегда слева. Щелчок - по оси открывает панель с полями «Максимум» и «Минимум» и кнопками «Применить», «Автомасштаб» и - «Вернуть начальную» -- Курсор/перекрестье с чтением значения **всех** видимых перьев в точке -- Минилегенда: чекбокс / цветная точка / имя / текущее значение в маске пера / единица измерения; - перо может стоять в нескольких группах и показывается под каждой, панель сворачивается до - чекбокса, точки и имени -- Переключатель в заголовке каждой группы включает или выключает все её перья разом. У него три - состояния: все включены, все выключены и смешанное, когда включена только часть -- Ширина панели легенды меняется перетаскиванием ручки между графиком и панелью, в развёрнутом и - свёрнутом виде. Каждый вид помнит свою ширину до конца сеанса, после перезапуска ширина сбрасывается -- Стартовый набор: при открытии рисуются только перья, помеченные в каталоге как включённые -- Границы шкалы из каталога — начальная шкала пера: перо открывается ровно на них, без полей. Правки - оси живут до конца сеанса и в базу не уходят. Новые границы, сохранённые в каталоге, не меняют ось - пера на работающем графике: они действуют, когда перо входит в график, и по команде «Вид → Шкала пера → Вернуть - начальную шкалу» -- Слои агрегации архива: raw / минута / час / сутки — слой выбирается по ширине окна и ширине холста - вместе (самый грубый слой, шаг которого укладывается в пиксельную колонку) -- Навигация по времени: зум/пан, переход к началу/концу, выбор диапазона -- Реалтайм и история через единый интерфейс `IDataProvider` - -**Окно** - -- Меню «Файл / Правка / Вид / Справка»: новое окно (второй процесс с теми же ключами запуска), выход, окно настроек, окно перьев и групп, переключатели - панели навигации, легенды, миникарты и панели сообщений, подменю «Шкала пера» с командами - «Автомасштаб» и «Вернуть начальную шкалу» для оси активного пера, окно «О программе» с версией сборки и - каталогом конфигурации -- Окно настроек («Правка → Настройки»): язык, тема и подключение к архиву, включая пароль. Окно - пишет в файлы конфигурации только изменённые ключи, и каждый попадает в тот файл, где уже лежит. - Перед записью значения проверяются теми же загрузчиками, что и на старте; негодное значение до - файла не доходит, а отказ появляется в панели сообщений. Новая тема сразу применяется во всех открытых - окнах вьювера. Остальные изменения вступают в силу при следующем запуске: окно показывает об этом строку - и кнопку «Перезапустить», которая открывает новое окно с теми же ключами запуска и закрывает текущее. Ключ, которого окно не показывает (например, `schema`), остаётся в своём файле и - переживает любую перезапись. Комментарии в переписанном файле не сохраняются -- Настройки открываются и из окна ошибки старта: там их открывают чаще всего, потому что поставляемый - набор приходит с пустым паролем. Вместо меню в этом окне кнопки «Настройки», «Перезапустить», «О программе» и «Выход»; «Перезапустить» - запускает вьювер заново с теми же ключами -- Окно «Правка → Перья и группы», вкладка «Перья»: таблица всех перьев, которая сортируется щелчком по - заголовку столбца, и форма выбранного пера. В форме правят имя, единицу измерения, маску с примером, - цвет, тип линии, включение при запуске и начальную шкалу. Значение поля уходит в базу, когда правка в - нём закончена; кнопки сохранения нет. Негодное значение в базу не уходит: поле возвращает прежнее - значение и подсвечивается, а строка под формой называет причину -- Тип линии почти не меняет вид пера, которое SCADA архивирует по изменению: на каждое изменение архив - пишет две строки, прежнее значение в последний такт опроса и новое примерно через 100 мс, поэтому и - интерполированная линия рисует ступеньку. Ступенчатая линия отличается от неё только на этих 100 мс; - разница видна на переменной, которую архив пишет без такой пары -- Вкладка «Группы» того же окна: группы создают, переименовывают и удаляют, а состав выбранной группы - задают чекбоксами. Перед удалением окно один раз спрашивает подтверждение и называет, сколько перьев - в группе; сами перья остаются -- Изменения из окна перьев доходят до графика и легенды каждого запущенного вьювера без перезапуска: - вьювер перечитывает каталог перьев раз в 5 секунд, а после записи из своего окна — сразу. Исключение - — границы шкалы: у пера на графике ось остаётся прежней, а новые границы применяются командой - «Вид → Шкала пера → Вернуть начальную шкалу» или когда перо входит в график. Номер пера - совпадает с номером переменной SCADA, поэтому окно его не меняет, не добавляет перья вручную и не - удаляет их. Смена «включения при запуске» не включает и не выключает перо на работающем графике: - флаг задаёт видимость пера на следующем старте. Открытое окно перьев каталог само не перечитывает: - правки других вьюверов появляются в нём после кнопки «Обновить список перьев», а из двух записей - одного поля остаётся последняя -- Новые переменные SCADA появляются в таблице скрытыми перьями после кнопки «Обновить список перьев» в - окне перьев: имя пера совпадает с номером переменной, при запуске перо выключено. Рядом с кнопкой - окно показывает, сколько перьев добавлено. Сам вьювер на старте перья не регистрирует -- Панель навигации: переход к живому краю, режим слежения, режим дельта-курсоров -- Строка состояния: связь с архивом и активный слой агрегации, оба на языке окна. Индикатор связи — - кнопка, она же открывает и закрывает панель сообщений -- Панель сообщений: список отказов с отметкой времени и счётчиком повторов, не длиннее 200 записей. - Туда приходит каждый отказ — чтение архива, запись из окна перьев, потеря связи и необработанное - исключение ReactiveUI - ---- - -## Архитектура - -- **Платформа:** .NET 10, C# 14, Windows -- **Оболочка:** Avalonia 12.0.5 (Win32 + Skia + HarfBuzz) + ReactiveUI -- **Графика:** ScottPlot.Avalonia 5.1.59 (нативный рендеринг, SkiaSharp) -- **Данные:** абстракция `IDataProvider`; единственная реализация — `PostgresDataProvider`, - который читает архив PostgreSQL от Simple-Scada 2: каталог переменных, границы архива, - окно истории и опрос живого края (см. документацию) - -Подробно — в `docs/architecture/` (overview, charting, trend-interaction, data-integration). - ---- +**Тренды** -## Требования +- Отдельная шкала у каждого пера. На графике отображается ось активного пера; щелчок по оси открывает + панель настройки шкалы. +- Курсор показывает значения всех видимых перьев в выбранной точке. Работа с маркерами. +- Изменение масштаба и сдвиг по времени, переход к текущему времени, режим «Следовать за текущим + временем». +- Слой архива (исходные данные, минутный, часовой, суточный) выбирается автоматически для установленного масштаба, точки группируются и соединяются с учетом экстремумов. +- Миникарта показывает весь архив с графиком активного пера и время в точке под указателем мыши. + +**Перья и группы** + +- Условные обозначения сгруппированы; у каждого пера отображается текущее значение по его маске, у + каждой группы — переключатель видимости всех её перьев. +- Окно «Правка → Перья и группы» задаёт имя, единицу измерения, маску, цвет, тип линии, начальную + шкалу пера и состав групп. +- Изменения каталога перьев применяются во всех открытых окнах без перезапуска. +- Новые переменные Simple-Scada добавляются в каталог кнопкой «Обновить список перьев». + +**Окна и настройки** -| Компонент | Требование | -| --------------- | ------------------------------------------------------------- | -| ОС | Windows 10 или Windows 11 (64-bit) | -| Среда сборки | .NET 10 SDK | -| Источник данных | Архив PostgreSQL от Simple-Scada 2, подготовленный SemiBase `v0.4.0` или новее: нужна роль `semiplot` и таблицы каталога `semiplot_tags`, `semiplot_groups`, `semiplot_pen_groups`. Версию разметки программа не проверяет: на более ранней разметке падает чтение каталога перьев, и окно ошибки старта называет недостающий столбец или таблицу. Кнопка «Обновить список перьев» в окне перьев вызывает функцию `semiplot_register_new_pens()` из той же разметки; без неё не работает только эта кнопка. Без архива приложение открывает окно ошибки старта вместо главного окна | -| Конфигурация | Каталог `--config-dir` с папками-секциями `app/` и `connection/`. Папка секции читается целиком: каждый `*.yaml` в ней разбирается отдельно, а ключи сливаются в одно отображение; ключ, который лежит сразу в двух файлах одной папки, роняет старт и называет оба. Поставляемый набор лежит в репозитории в `ConfigFiles/`; запускать надо по его копии (`SemiPlot/Artifacts/dev-config`, туда же смотрят конфигурации запуска из `.zed/` и `.run/`), в отслеживаемые файлы ничего не вписывают. `app/app.yaml` держит `locale` (`ru` \| `en`) и `theme` (`light` \| `dark`), оба обязательны и без значений по умолчанию. `theme` применяется на лету во всех запущенных окнах, кто бы ни изменил файл, а `locale` вступает в силу при следующем запуске. `connection/connection.yaml` — подключение к архиву, и `password` в нём пустой. `host` принимает только IPv4-адрес вида `127.0.0.1`, а имя сервера, `localhost` в том числе, роняет старт. Часовой пояс архива не настраивается: вьювер читает архив в часовом поясе своей машины, поэтому его запускают на той же машине, что и SimpleScada. Пароль заполняют в окне «Правка → Настройки», которое открывается и из окна ошибки старта. Любая из этих ошибок открывает окно ошибки старта вместо графика | -| Ключи запуска | `--config-dir`, `--log-file` и `--logging-level` обязательны, значений по умолчанию нет ни у одного. `--logging-level` принимает `verbose`, `debug`, `info` (или `information`), `warning`, `error`, `fatal`. Пропущенный ключ, незнакомый ключ, ключ с пустым значением и негодный уровень логирования открывают окно ошибки старта и дают код возврата 1. Окна, открытые через «Файл → Новое окно», пишут в тот же `--log-file`: каждое событие после времени несёт в скобках идентификатор процесса, который его записал, а при 5 МБ запись переходит в следующий файл (`semiplot.log` → `semiplot_001.log`) | -| Тестовый стенд | Только для интеграционных тестов: Docker (или иная среда контейнеров). `semibase` приходит слоем образа из `ghcr.io/semiteq/semibase`, ставить его на машину не нужно; без среды контейнеров эти тесты падают, а не пропускаются | +- Команда «Файл → Новое окно» запускает независимый экземпляр программы. +- Окно «Правка → Настройки» задаёт язык интерфейса, тему оформления и параметры подключения к архиву. + Смена темы применяется во всех открытых окнах сразу. +- Панель сообщений собирает все ошибки чтения и записи. +- При ошибке запуска программа открывает окно ошибки, из которого доступны настройки и перезапуск. + +
+ +

Окно перьев и групп

+
+ +
+ +

Окно настроек

+
+ +## Требования ---- +| Компонент | Требование | +| -------------------- | ------------------------------------------------------------------------------------------------ | +| Операционная система | Windows 10 или Windows 11, 64-разрядная, .NET 10 | +| Источник данных | Архив Simple-Scada 2 в PostgreSQL, установленный [SemiBase](https://github.com/Semiteq/SemiBase) | +| Конфигурация | Каталог с разделами `app/` и `connection/`; образец — `ConfigFiles/` | -## Сборка и запуск +## Запуск ```powershell -# Сборка -dotnet build SemiPlot/SemiPlot.UI/SemiPlot.UI.csproj - -# Запуск. Все три ключа обязательны, см. "Требования". -# Запускаем по копии набора, а не по отслеживаемому. SemiPlot\Artifacts\ лежит в .gitignore, -# а %TEMP%\SemiPlot принадлежит демо-стенду, который чистит его на старте. -New-Item -ItemType Directory -Force SemiPlot\Artifacts\dev-config | Out-Null -Copy-Item ConfigFiles\* SemiPlot\Artifacts\dev-config -Recurse -Force - -# В connection.yaml пароль пустой, поэтому первый запуск открывает окно ошибки старта. -# Там кнопка «Настройки»: вписать пароль, сохранить и нажать «Перезапустить». -dotnet run --project SemiPlot/SemiPlot.UI/SemiPlot.UI.csproj -- ` - --config-dir SemiPlot\Artifacts\dev-config ` - --log-file SemiPlot\Artifacts\dev-logs\semiplot.log --logging-level information - -# Тесты -dotnet test SemiPlot.slnx +.\SemiPlot.UI.exe --config-dir <каталог конфигурации> --log-file <каталог журнала>\semiplot.log --logging-level info ``` -Хуки живут в `.githooks/` и подключаются один раз на клон: `git config core.hooksPath .githooks`. Хук -запрещает коммит прямо в `master` или `main`. Дальше он проверяет только проиндексированные `.cs`: -`dotnet format --verify-no-changes` и линтер комментариев `terse` (закреплён в -`.config/dotnet-tools.json`, хук сам делает `dotnet tool restore`). -Исходники ASCII, без BOM; файл из `dotnet new` проходит хук только после -`dotnet format SemiPlot.slnx --include `. - -Демо-стенд с одноразовым архивом и живой записью поднимается одной командой: -`dotnet run --project SemiPlot/SemiPlot.AppHost` — контейнер PostgreSQL, `converge`, писатель и -вьювер стартуют в порядке зависимостей и останавливаются вместе (см. `docs/architecture/bench.md`). -Стенд работает с копией: он кладёт `ConfigFiles/` в `%TEMP%\SemiPlot\ConfigFiles`, пишет лог в -`%TEMP%\SemiPlot\Logs` и убирает копию конфигурации на остановке. Логи чистятся при следующем -старте, потому что вьювер ещё может держать `semiplot.log` открытым. В отслеживаемый набор ничего -не пишется. - -Весь проект `SemiPlot.Tests.Integration` — тесты стенда и сквозные сценарии — поднимает PostgreSQL -в контейнере. Образ стенда собирается из `SemiPlot/bench/Dockerfile`: -он забирает `/semibase` из `ghcr.io/semiteq/semibase:latest` и выполняет `semibase bench` из -`/docker-entrypoint-initdb.d/` — до того, как откроется опубликованный порт. Отдельный бинарник -`semibase` на машине не нужен: `dotnet test SemiPlot.slnx` запускает эти тесты сам. - -Прогон ничего после себя не оставляет: собранный образ стенда, контейнер, шаблонная база и все её -клоны удаляются вместе с прогоном. Скачанные из реестра образы (`postgres:17-alpine`, -`ghcr.io/semiteq/semibase:latest`) остаются — намеренно; тег `latest` фикстура подтягивает перед -сборкой образа. - -Если контейнерной среды нет, тесты **падают**, а не пропускаются: фикстура пробрасывает ошибку, и -xunit заваливает всю коллекцию с `TestPipelineException`. `dotnet test SemiPlot.slnx` на такой -машине красный, а зелёным остаётся только `SemiPlot.Tests.Unit`. +Пароль к архиву задаётся в окне «Правка → Настройки». Без пароля программа открывает окно ошибки +запуска, из которого доступны те же настройки.