# mocl — архитектура и журнал решений

> Этот файл — **единственный источник правды** по архитектуре нового стека.
> Создан как перенос решений из исходной сессии в workspace `/var/www/robostar/`.
> При первом открытии `/var/www/mocl/` как отдельного workspace — попросить агента
> засеять содержимое этого файла в `/memories/repo/architecture.md` нового workspace,
> чтобы оно автоматически попадало в контекст следующих сессий.

---

## 1. Контекст и причина рестарта

Был стек `/var/www/robostar/` (MySQL `mocl` + `node_mysql` + `ui/`) с проблемами:
- `RunSavedQueryUniversalSafe` — небезопасная REPLACE-подстановка параметров в SQL
  (нет word-boundary, теряются типы, NULL ломается).
- `savedproc` без поля `opertype` — нельзя автоматически роутить reader/writer.
- `savedquery` (SQL-шаблоны в строках) — анти-паттерн.
- DEFINER-неконсистентность: `robostar@localhost` vs `robostarDB@%`.
- MVCC race: writer ещё не закоммитил, а cache-invalidate уже пошёл.
- Доменная привязка (медицина) проникла в инфраструктуру.

Решено: **полный greenfield на PostgreSQL** с переписыванием и сервера, и клиента.
Старый стек остаётся работать параллельно до cutover; данные переедут позже через pgloader/ETL.

---

## 2. Зафиксированные решения (плёнка опроса)

| Вопрос | Ответ |
|---|---|
| Scope | greenfield + параллельная работа старого стека; миграция данных позже |
| Domain coupling | generic-ядро без домена; домен подключается отдельным модулем поверх |
| Tenancy | single-tenant (одна организация на инсталляцию) |
| Где бизнес-логика | в PL/pgSQL процедурах PostgreSQL (savedproc continuation) |
| Источник changelog | триггеры в таблицу `core.changelog` + `pg_notify` |
| Стек | переписываем всё, включая клиент |
| Что делать сейчас | (1) зафиксировать архитектуру, (2) создать каркас |
| Клиентский стек | Vite + Lit + OPFS + `@sqlite.org/sqlite-wasm` (перенос проверенной local-first схемы из `ui/`) |
| Расположение | новый workspace `/var/www/mocl/` |
| Транспорт | WebSocket (один канал для RPC и push'а changelog) |
| Auth | заглушка dev-user; реальный auth — позже |
| Deploy | Docker Compose (postgres + redis + adminer) |
| Redis | оставляем как qmanager (кэш + pub/sub) — модель из старого `node_mysql/queuemanager` |
| PG версия | PostgreSQL 16 |

---

## 3. Архитектура

### 3.1 Стек
- **БД**: PostgreSQL 16. Вся бизнес-логика — в PL/pgSQL процедурах. Никаких "универсальных query".
- **Server**: Node + TypeScript + Fastify + `@fastify/websocket` + `pg` (node-postgres) + ioredis.
- **Cache/pubsub**: Redis 7 (qmanager-стиль из legacy `node_mysql/queuemanager/`).
- **Client**: Vite + Lit + OPFS + `@sqlite.org/sqlite-wasm` (SAH-Pool VFS).
- **Auth**: dev-stub (заголовок `X-Dev-User`); реальный — позже (вариант: opaque session tokens в PG, bcrypt).
- **Deploy**: Docker Compose локально и в проде. PG через `postgres:16-alpine`.

### 3.2 Generic-ядро (схема `core`, БЕЗ домена)

В ядре только инфраструктурные таблицы. Никаких patients/visits/employees.

| Таблица | Назначение |
|---|---|
| `core.saved` | Реестр PL/pgSQL процедур: `name`, `schema_name`, `proc_name`, `opertype` (0=reader, 1=delete, 2=update, 3=insert), `trackedtable JSONB`, `property JSONB`, `coltemplate_id FK`. **`opertype` ОБЯЗАТЕЛЕН** — исправляем дыру старой схемы. |
| `core.changelog` | Append-only лог изменений. Одна BIGSERIAL последовательность на весь домен. Поля: `id, ts, table_name, op (I/U/D), row_id, row JSONB, actor, txid`. |
| `core.synctable` | Описание таблиц для клиента. `mode IN ('cached-full','cached-rolling','cached-query','local-only')`, `scope IN ('shared','user')`, опционально `rolling JSONB {limit, orderBy, orderDir}`. |
| `core.accesstable` | ACL: `(role_name, table_name, can_read, can_write)`. Заполняет домен. |
| `core.coltemplate` | Шаблоны колонок для UI. |
| `core.schema_version` | Версия структуры (для invalidate клиентского OPFS-кэша). |

Доменные таблицы и процедуры подключаются отдельным модулем поверх ядра.

### 3.3 Changelog: триггеры + LISTEN/NOTIFY

- Универсальная trigger-функция `core.log_change()` пишет в `core.changelog` и делает
  `pg_notify('core_changelog', json_payload)`. Payload компактный: `{id, t, op, rid}`.
- Триггер навешивается per-table через хелпер `core.track_table('schema.table', mode)`.
- Доменный код пишет: `SELECT core.track_table('app.patients')` — и таблица отслеживается.
- Снять отслеживание: `core.untrack_table('schema.table')`.
- Node-сервер держит ОДИН выделенный `pg.Client` с `LISTEN core_changelog`,
  раздаёт нотификации в WebSocket'ы заинтересованных клиентов (per-session подписки).
- **Actor**: trigger читает `current_setting('mocl.actor', true)` — сервер выставляет
  через `SELECT set_config('mocl.actor', $1, false)` перед каждым `CALL`.
- **MVCC race-condition решён by-design**: trigger выполняется в той же транзакции, что и DML.
  `NOTIFY` доставляется **только после COMMIT**. Клиенты, открывающие snapshot после
  получения `NOTIFY` с `id=N`, гарантированно увидят все changes ≤ N.

### 3.4 Reader/Writer split

- Два `pg.Pool` в сервере: `readerPool` (для `opertype=0`), `writerPool` (для `opertype>0`).
- Маршрутизация — в `server/src/rpc/dispatcher.ts` по результату lookup в `core.saved`.
- Reader pool позже можно направить на read-replica без изменений в коде доменных процедур.

### 3.5 RPC протокол (WebSocket)

Файл [server/src/rpc/types.ts](server/src/rpc/types.ts) — единственный источник правды.

Запрос клиент → сервер:
```ts
{ id: string, method: 'call', name: string, params?: unknown }
```

Ответ сервер → клиент:
```ts
{ id: string, ok: true, result: unknown }
| { id: string, ok: false, error: { code: string, message: string } }
```

Push сервер → клиент:
```ts
{ type: 'changelog', id: number, table: string, op: 'I'|'U'|'D', rowId: string }
```

Корреляция запрос↔ответ — по `id` (UUID на клиенте).
Клиент: [client/src/platform/rpc.ts](client/src/platform/rpc.ts) — auto-reconnect с backoff.

### 3.6 Клиентский local-first (из legacy `ui/`)

Сохраняется проверенная модель из `/var/www/robostar/ui/src/storage/`:

- `opfs.ts` — обёртка над OPFS (read/write/list/stat/quota).
- `@sqlite.org/sqlite-wasm` 3.53, OPFS SAH-Pool VFS (`vfsName: 'opfs-sahpool-robostar'`).
- `sqlite-worker.ts` — dedicated Worker, держит один Database, `PRAGMA foreign_keys = ON`.
- `sqlite-protocol.ts` — типы RPC между основным потоком и Worker'ом.
- `sqlite.ts` — клиент: `openDatabase()` → `SqliteClient` (exec/run/select/selectOne/selectValue/batch/close/wipe).
- Интерактивных транзакций НЕТ — только `batch` (auto BEGIN/COMMIT/ROLLBACK).
- `schema.ts` — декларативная схема: `TableDef`/`ColumnDef`/`IndexDef`, `createTables`/`dropTable`/`tableExists`.

**ВАЖНО для Vite-конфига** (без этих двух флагов ничего не работает):
- `optimizeDeps.exclude: ['@sqlite.org/sqlite-wasm']` — иначе prebundler ломает путь к `.wasm`,
  worker получает HTML-fallback и падает с `CompileError "expected magic word"`.
- `worker: { format: 'es' }` — иначе SQLite worker не запустится.
- После правки конфига: `rm -rf node_modules/.vite` и hard-reload.

Worker создаётся как: `new Worker(new URL('./sqlite-worker.ts', import.meta.url), { type: 'module', name: 'sqlite-worker' })`. `name` ОБЯЗАН быть статическим литералом (Vite не парсит template strings в worker options).

### 3.7 Физическое размещение в OPFS (план)

- `shared/refdata.sqlite` — общие данные для всех юзеров на ПК: справочники, кэш query общего скоупа.
- `user/<userId>/workspace.sqlite` — личные данные юзера: outbox, UI-state, кэш query, привязанный к юзеру.
- При logout — удаляется только `user/<userId>/...`. Shared не трогаем.
- Один логин на origin: новая вкладка → старая логаут (BroadcastChannel + ротация токена).
- Fallback без OPFS — режим "online-only" через `Repo`-абстракцию (`LocalFirstRepo`/`OnlineRepo`).

### 3.8 Типы синхронизируемых таблиц (`synctable.mode`)

1. `cached-full` — полная копия серверной таблицы; sync дельтой, snapshot при разрыве.
2. `cached-rolling` — последние N записей (`limit, orderBy, orderDir`); sync + локальный prune.
3. `cached-query` — кэш ответа на конкретный запрос (ключ = нормализованный URL+params).
   Конверт ответа: `{ rows, meta: { etag, version, tablesUsed: string[] } }`.
   Инвалидация — по пересечению `tablesUsed` с изменившимися таблицами + `If-None-Match`.
4. `local-only` — только клиент (outbox, UI-state, черновики).

### 3.9 Версионирование (клиент)

- `schemaVersion` (структура локальной БД) → mismatch → drop+create.
- `dataVersion = MAX(core.changelog.id)` → mismatch → дельта;
  `since < horizon` → full snapshot нужных таблиц.

### 3.10 Черновики (документы)

- Источник правды — на сервере. Клиент работает через outbox при offline.
- В рамках одного юзера между его устройствами — last-write-wins по `version` + предупреждение.
- ID документа генерирует клиент (UUID/ULID), чтобы можно было сохранять offline.
- Аудит-лог — отдельная тема, в эту модель не входит.

---

## 4. Структура репозитория

```
/var/www/mocl/
├── ARCHITECTURE.md             ← этот файл (источник правды)
├── README.md                   ← короткий quickstart
├── docker-compose.yml          ← postgres:16 + redis:7 + adminer
├── .env / .env.example
├── .gitignore
├── db/
│   ├── migrations/
│   │   ├── 0001_core.sql       ← registry-схема
│   │   ├── 0002_triggers.sql   ← changelog trigger factory + pg_notify
│   │   └── README.md
│   └── procedures/             ← PL/pgSQL процедуры (заполняется доменом)
│       └── README.md
├── server/
│   ├── package.json            ← Fastify 4 + @fastify/websocket 10 + pg 8 + ioredis 5
│   ├── tsconfig.json
│   ├── scripts/
│   │   └── migrate.ts          ← runner миграций (читает db/migrations/0*.sql)
│   └── src/
│       ├── index.ts            ← entry: Fastify + WS + LISTEN + invalidator
│       ├── config.ts
│       ├── db/
│       │   ├── pool.ts         ← readerPool + writerPool
│       │   └── notify.ts       ← LISTEN core_changelog + onChangelog подписка
│       ├── ws/
│       │   ├── server.ts       ← регистрирует /ws, раздаёт push сессиям
│       │   └── session.ts      ← per-client subscription state
│       ├── rpc/
│       │   ├── types.ts        ← wire-типы (см. §3.5)
│       │   └── dispatcher.ts   ← lookup в core.saved, маршрутизация opertype
│       ├── auth/
│       │   └── dev-stub.ts     ← X-Dev-User заглушка
│       └── qmanager/
│           ├── cache.ts        ← Redis-кэш (q:*)
│           └── invalidator.ts  ← LISTEN-driven invalidate
└── client/
    ├── package.json            ← Vite 5 + Lit 3 + @sqlite.org/sqlite-wasm 3.53.0-build1
    ├── tsconfig.json
    ├── vite.config.ts          ← с флагами для sqlite-wasm
    ├── index.html
    └── src/
        ├── main.ts
        ├── platform/
        │   ├── rpc.ts          ← WS-RPC клиент с auto-reconnect
        │   ├── sync.ts         ← changelog applier + gap detection
        │   └── repo.ts         ← Repo интерфейс + OnlineRepo (LocalFirstRepo — TODO)
        ├── storage/            ← переносим из ui/src/storage (см. README внутри)
        └── components/         ← переносим из ui/src/components (см. README внутри)
```

---

## 5. Текущее состояние (на момент создания файла)

Что сделано:
- [x] PostgreSQL 16 поднят в Docker (`mocl-pg`, volume `mocl_mocl-pgdata`, порт 5432)
- [x] Миграции `0001_core.sql` + `0002_triggers.sql` применены через `psql`
- [x] Схема `core` содержит 6 таблиц + 3 функции (`log_change`, `track_table`, `untrack_table`)
- [x] `core.schema_version.version = 1`
- [x] Канал `core_changelog` готов (PG `pg_notify`)
- [x] Server-скелет на TypeScript создан, но `npm install` ещё НЕ запускался
- [x] Client-скелет на Vite+Lit создан, но `npm install` ещё НЕ запускался

Что НЕ сделано:
- [ ] Redis и Adminer не подняты (поднять: `sudo docker compose up -d redis adminer`)
- [ ] `cd server && npm install && npm run dev` — не запускался
- [ ] `cd client && npm install && npm run dev` — не запускался
- [ ] Smoke-тест end-to-end: `track_table` на тестовой таблице → INSERT → проверка `changelog` и WS push
- [ ] Перенос `ui/src/storage/` (opfs.ts, sqlite*.ts, schema.ts) в `client/src/storage/`
- [ ] Перенос `ui/src/components/` (RSButton, RSPanel, RSTable, RSTabList, RSTabPage, RSDockContainer, SView.ts) в `client/src/components/`
- [ ] Перенос данных из старой MySQL `mocl` через pgloader/ETL (отложено до stable домена)
- [ ] Реальный auth (вместо `X-Dev-User`)

---

## 6. Доступ к БД

- **Хост**: `localhost`
- **Порт**: `5432`
- **БД**: `mocl`
- **Пользователь**: `mocl`
- **Пароль**: `change-me` (дефолт из шаблона `.env.example`)
- **Контейнер**: `mocl-pg`
- **Volume**: `mocl_mocl-pgdata`

Подключение:
```bash
# из контейнера
sudo docker exec -it mocl-pg psql -U mocl -d mocl

# с хоста (если стоит postgresql-client)
PGPASSWORD=change-me psql -h localhost -U mocl -d mocl
```

Смена пароля:
```sql
ALTER USER mocl WITH PASSWORD 'новый-пароль';
-- затем обновить mocl/.env: PGPASSWORD=новый-пароль
```

Полный сброс (УДАЛИТ ДАННЫЕ):
```bash
cd /var/www/mocl && sudo docker compose down -v
```

`sudo` нужен потому, что текущая shell-сессия ещё не подхватила группу `docker`
(пользователь добавлен в неё через `usermod -aG docker`, но требуется новый login
или `newgrp docker` для активации).

---

## 7. Что НЕ переносится из legacy

- `RunSavedQueryUniversalSafe` — unsafe REPLACE-substitution.
- `savedquery` (SQL-шаблоны) — заменяется PL/pgSQL процедурами в `core.saved`.
- DEFINER-неконсистентность.
- Доменные таблицы переносятся pgloader'ом, но schema будет переработана:
  - везде BIGSERIAL/UUID PK
  - `timestamptz` вместо `DATETIME`
  - `JSONB` вместо TEXT-json
  - нормальные FK с `ON DELETE`/`ON UPDATE`
- Текущая `ui/main.ts` (демо-страница) — переедет переработанной, не как есть.

---

## 8. Известные риски и слабые места (без прикрас)

1. **Inline-кэш реестра в `dispatcher.ts`** — per-process `Map<string, SavedRow>`.
   При нескольких node-инстансах invalidate через `LISTEN core_changelog` придёт всем,
   но это работает ТОЛЬКО потому что все инстансы подписаны. Если решим масштабироваться
   через PM2-кластер с одним shared LISTEN-процессом — нужно будет переделать.

2. **`invalidator.ts` сейчас сносит ВСЁ (`q:*`)** при любом изменении.
   Точечная инвалидация по `core.saved.trackedtable` появится, когда зарегистрируешь
   первую процедуру с реальным списком таблиц.

3. **`@sqlite.org/sqlite-wasm` пиннут на `3.53.0-build1`** (как в текущем `ui/`).
   При апгрейде проверять, что SAH-Pool API не сломался.

4. **`@fastify/websocket@10` + Fastify 4** — стабильная связка. При апгрейде до Fastify 5
   потребуется `@fastify/websocket@11`.

5. **`migrate.ts` пока не ведёт учёт применённых миграций** — он просто прогоняет файлы
   в одной транзакции каждый. Идемпотентность держится на `CREATE … IF NOT EXISTS`
   в самих миграциях. Для прода нужно добавить таблицу `core.applied_migration`
   с (filename, sha256, applied_at, applied_by).

6. **`devAuth` — заглушка**, принимает любого пользователя из заголовка `X-Dev-User`.
   До выкладки наружу — заменить.

7. **Дефолтный пароль `change-me`** в `.env.example` и `docker-compose.yml`.
   Для дева на localhost ок, но `.env` не должен попасть в git (он в `.gitignore`).
   Перед любым выставлением PG порта наружу — поменять.

8. **WAL CDC vs триггеры**: выбрали триггеры (см. §3.3). Альтернатива — `wal2json` +
   logical replication — даёт меньше нагрузки на write, но сложнее в эксплуатации
   и требует `wal_level=logical`. Если когда-нибудь упрёмся в throughput триггеров —
   переключение возможно без слома клиента (формат `pg_notify` payload остаётся).

---

## 9. Следующие шаги (рекомендуемый порядок)

1. **Засеять repo-memory нового workspace** (в открытой mocl-сессии):
   попросить агента создать `/memories/repo/architecture.md` с кратким указателем
   на этот файл и ключевыми фактами из §3 и §6.
2. **Поднять оставшиеся сервисы**: `sudo docker compose up -d redis adminer`.
3. **Server**: `cd server && npm install && npm run dev`. Проверить `/health`.
4. **Smoke-тест триггера**:
   ```sql
   CREATE TABLE app.demo (id BIGSERIAL PRIMARY KEY, name TEXT);
   SELECT core.track_table('app.demo');
   INSERT INTO app.demo (name) VALUES ('test');
   SELECT * FROM core.changelog;       -- должна быть запись
   ```
   Параллельно в `psql`: `LISTEN core_changelog;` → видеть нотификацию после INSERT.
5. **Первая процедура**: написать `app.example_proc(IN p_in JSONB, OUT p_out JSONB)`,
   зарегистрировать в `core.saved` с правильным `opertype`. Проверить вызов через
   `dispatcher.ts` → reader pool.
6. **Client**: `cd client && npm install && npm run dev`. Проверить WS-коннект к `/ws`.
7. **Перенос UI-storage** из `/var/www/robostar/ui/src/storage/` в `client/src/storage/`.
8. **Перенос UI-компонентов** из `/var/www/robostar/ui/src/components/` в `client/src/components/`.
9. **Доменный модуль**: отдельная папка `db/procedures/app/` + миграции `1NNN_*.sql`
   (зарезервировано: `0NNN_*` под core).
10. **Реальный auth** + полноценный `migrate.ts` с учётом применённых миграций.

---

## 10. Инструкция для агента в новой сессии mocl/

Когда откроешь `/var/www/mocl/` как workspace:

1. Прочитай этот файл целиком.
2. Создай `/memories/repo/architecture.md` (он будет привязан к mocl workspace) с кратким
   summary: ссылка на `/var/www/mocl/ARCHITECTURE.md` + ключевые decisions из §2 +
   риски из §8 + credentials из §6 (без пароля — там стоит указать "см. ARCHITECTURE.md §6").
3. Не повторяй обсуждение, которое уже зафиксировано здесь. Если что-то противоречит этому
   файлу — обнови файл и memory синхронно.
4. Соблюдай user-rule: давать объективную оценку, не хвалить, прямо сообщать о рисках.
