diff --git a/.cursor/plans/iotmanagerweb_refactor_ceb2e1c6.plan.md b/.cursor/plans/iotmanagerweb_refactor_ceb2e1c6.plan.md new file mode 100644 index 0000000..fbb3a10 --- /dev/null +++ b/.cursor/plans/iotmanagerweb_refactor_ceb2e1c6.plan.md @@ -0,0 +1,832 @@ +--- +name: IoTManagerWeb refactor +overview: "" +todos: + - id: backup-files + content: "" + status: pending + - id: inventory-external + content: Инвентаризировать все точки внешнего взаимодействия (HTTP endpoints, WS, Cookies) и зафиксировать список функций/мест замены. + status: pending + - id: add-http-wrapper + content: Добавить `src/api/http.js` (fetch JSON wrapper), не меняя поведение обработки ok/ошибок. + status: pending + - id: portal-api + content: Добавить `src/api/portal.js` и перенести туда все `fetch("https://portal.iotmanager.org/...`) из `App.svelte`, `pages/Login.svelte`, `pages/Profile.svelte`, `pages/Config.svelte`. + status: pending + - id: firmware-api + content: Добавить `src/api/firmware.js` и перенести `getVersionsList` из `App.svelte`. + status: pending + - id: device-ws-api + content: Добавить `src/api/deviceSocket.js` и перенести туда создание WebSocket, `addEventListener` wiring, `send`/`readyState` проверки; в `App.svelte` оставить протокол/обновление state. + status: pending + - id: ui-components + content: Вынести header/nav/footer `App.svelte` в новые Svelte-компоненты (`src/components/layout/*`), сохранив классы и события (on:change dropdown и т.п.). + status: pending + - id: lib-blob-protocol + content: Вынести парсинг blob в `src/lib/blobProtocol.js` (getPayloadAsJson/Txt, getJsonAsJson, parseBlob, parseAllBlob с колбэками для записи в state). App вызывает модуль и передаёт колбэки/сеттеры. + status: pending + - id: lib-device-list + content: Вынести логику списка устройств в `src/lib/deviceListManager.js` (initDevList, devListOverride, devListCombine, sortList, combineArrays). Без поломки цепочки firstDevListRequest / devlist. + status: pending + - id: lib-device-connection + content: "Вынести всю логику подключения в `src/lib/deviceConnection.js`: getIP, connectToAllDevices, createOpenHandler (onOpen: markDeviceStatus, /devlist| при firstDevListRequest && ws===0, отправка страницы), handleDevListReceived (initDevList + connectToAllDevices). Цепочка «первое устройство → список → подключение ко всем» в одном модуле." + status: pending + - id: lib-ws-reconnect + content: Вынести реконнект и heartbeat в `src/lib/wsReconnect.js` (wsTestMsgTask, ack). App передаёт deviceList, sendFn, markDeviceStatus и опции. + status: pending + - id: cleanup-and-verify + content: Убрать дублирующийся код/магические строки (base URLs), прогнать smoke test по маршрутам и исправить регрессии. + status: pending +isProject: false +--- + +## Бэкенд (референс, не менять) + +Интерфейс **IoTManagerWeb** работает в паре с прошивкой устройств — проект **IoTManager** (backend). При непонятностях по протоколу или форматам данных смотреть бэкенд как референс. **Бэкенд в рамках этого рефакторинга не меняем.** + +- **Путь к проекту**: `../IoTManager` относительно IoTManagerWeb (или абсолютно: `/Users/dmitry/Documents/Database/IoTManagerProject/personal/IoTManager`). +- **Главный файл протокола WebSocket**: `IoTManager/src/WsServer.cpp` + - Обработка входящих текстовых команд (`headerStr` до `|`): страницы `/|`, `/config|`, `/connection|`, `/list|`, `/system|`, `/profile|`; команды `/params|`, `/charts|`, `/gifnoc|`, `/tuoyal|`, `/oiranecs|`, `/sgnittes|`, `/mqtt|`, `/scan|`, `/devlist|`, `/tsil|`, `/control|`, `/tst|`, `/order|` и др. + - Ответ на **/tst|**: отправка строки **/tstr|** (WStype_TEXT) — на фронте обрабатывается как единственное входящее string-сообщение. + - Отправка данных клиенту: **sendFileToWsByFrames(filename, header, ...)** — бинарные фреймы с 6-символьным заголовком (itemsj, widget, config, scenar, settin, layout, params, devlis, prfile, otaupd, charta и т.д.); **sendStringToWs(header, payload, num)** — тот же формат «header|0012|payload» отправляется как **binary** (sendBIN), не текст. На фронте всё приходит как Blob, кроме явного ответа "/tstr|". +- **Модули логирования (графики)**: в `IoTManager/src/modules/virtual/Loging/`, `Loging2/`, `Loging3/`, `LogingDaily/`, `LogingHourly/` — формирование и отправка **charta** (sendFileToWsByFrames) и **chartb** (sendStringToWs). Формат и порядок полей на фронте должны совпадать с бэкендом. +- **Формат экспорта конфигурации** (mark "iotm", разделитель "scenario=>") используется и в примерах конфигов бэкенда: например `IoTManager/src/modules/exec/Buzzer/export.json`, примеры в modules с `scenario=>` в JSON. +- **Данные для тестов/сверки**: `IoTManager/data_svelte/` — config.json, items.json, widgets.json, layout.json, scenario.txt, settings.json и т.д. + +При рефакторинге веб-интерфейса сохранять совместимость с тем, что отправляет и ожидает этот бэкенд; при сомнениях сверяться с `WsServer.cpp` и модулями Loging*. + +--- + +## Контекст (что сейчас важно не сломать) + +- **HTTP-запросы** разбросаны по `App.svelte` и страницам: + - `App.svelte`: `getUser`, `getModInfo`, `getProfile` (portal/compiler) и `getVersionsList` (ver.json). + - `pages/Login.svelte`, `pages/Profile.svelte`, `pages/Config.svelte`. +- **WebSocket к устройствам** — многослойная логика в `App.svelte` (см. раздел «Слои WebSocket» ниже). +- **Архитектурный каркас**: `App.svelte` — shell + роутинг (tinro) + прокидывание props в страницы. + +--- + +## Бэкапы перед рефакторингом (первый шаг) + +Перед любыми изменениями создать копии файлов, чтобы при потере контекста можно было сверяться с оригиналом. + +- **Папка**: `_backup_refactor/` в корне проекта (или `src/_backup_refactor/`), добавить в `.gitignore` при желании не коммитить бэкапы. +- **Файлы для копирования**: + - `src/App.svelte` (главный файл с протоколом и состоянием) + - `src/main.js` + - `src/pages/Config.svelte`, `Connection.svelte`, `Dashboard.svelte`, `List.svelte`, `Login.svelte`, `Profile.svelte`, `System.svelte` (страницы с логикой и/или fetch) + - `src/widgets/Chart.svelte`, `Input.svelte`, `Range.svelte`, `Toggle.svelte`, `Anydata.svelte` + - `src/components/Alarm.svelte`, `Card.svelte`, `Modal.svelte`, `ModalPass.svelte`, `Progress.svelte` +- **Цель**: не потерять контекст; при сомнениях сверять логику/форматы с бэкапом. + +--- + +## Критичная цепочка: первое устройство и список устройств (не ломать) + +Ключевой сценарий: приложение открыто с текущего устройства (один известный IP); по первому подключению запрашивается список всех устройств, затем подключаемся к каждому из них. Порядок и условия менять нельзя. + +### Исходное состояние + +- **deviceList** при инициализации содержит один элемент: `{ name: "--", id: "--", ip: myip, ws: 0, status: false }` (myip = document.location.hostname или при devMode фиксированный IP). [App.svelte L99–108] +- **firstDevListRequest = true** выставляется в **onMount** перед вызовом **connectToAllDevices()**. [L196, L198] + +### Шаг 1: Подключение только к первому устройству + +- **connectToAllDevices()** обходит deviceList, для каждого устройства с `status === false || undefined` вызывает **wsConnect(i)** и **wsEventAdd(i)**. Изначально в списке только индекс 0 — подключается один WebSocket к myip:81. [L228–236] +- В обработчике **open** сокета (в wsEventAdd): если **firstDevListRequest && ws === 0**, отправляется **wsSendMsg(ws, "/devlist|")**. Запрос списка устройств уходит только один раз и только с первого подключённого сокета (ws === 0). [L297–298] + +### Шаг 2: Приём списка и обновление deviceList + +- Устройство отвечает blob с заголовком **"devlis"**, в payload — JSON-массив устройств (name, id, ip, ws, status, …). [L429–434] +- В **parseBlob** при `header === "devlis"`: **incDeviceList = out.json**, затем вызывается **initDevList()**. [L432–436] +- **initDevList()**: [L695–711] + - Если **firstDevListRequest** — вызывается **devListOverride()**: deviceList полностью заменяется на incDeviceList, **sortList(deviceList)**, затем **deviceList[0].status = true** (чтобы не переподключаться к уже подключённому первому устройству). + - Иначе — **devListCombine()**: слияние deviceList и incDeviceList по IP (combineArrays), sortList. + - **firstDevListRequest = false**, затем снова **connectToAllDevices()**. + +### Шаг 3: Подключение ко всем устройствам из списка + +- После devListOverride/devListCombine в **deviceList** уже все IP. **connectToAllDevices()** вызывается снова: для устройств с `status === false` создаются новые сокеты (индексы 1, 2, …); устройство с индексом 0 пропускается, т.к. **deviceList[0].status** уже true. [L714–718, L228–236] + +### Что при рефакторинге сохранить + +- Условие отправки `/devlist|` только при **firstDevListRequest && ws === 0** в обработчике **open**. +- Обработку blob **"devlis"** только в parseBlob (для выбранного устройства), вызов **initDevList()** и внутри него **devListOverride** при первом запросе, **devListCombine** при последующих. +- Присвоение **deviceList[0].status = true** в devListOverride после подстановки списка — без этого произойдёт повторное подключение к первому устройству. +- Последовательность: onMount → firstDevListRequest=true → connectToAllDevices() → open(ws=0) → /devlist| → devlis → initDevList → devListOverride/devListCombine → connectToAllDevices() снова. Не разрывать цепочку и не менять порядок. + +**После рефакторинга** эта логика должна жить в **lib/deviceConnection.js** (connectToAllDevices, createOpenHandler, handleDevListReceived) и **lib/deviceListManager.js** (initDevList, devListOverride, devListCombine); App только передаёт зависимости и вызывает экспорты этих модулей. + +--- + +## Страница списка устройств (List) — не ломать + +Страница [src/pages/List.svelte](src/pages/List.svelte) отображает **deviceList**, управляет добавлением/удалением устройств и сохранением списка на устройство. При рефакторинге сохранять поведение и контракт с App. + +### Данные и пропсы из App + +- **deviceList** — массив устройств (name, id, ip, ws, status, ping, fv и т.д.); таблица строится по нему, при удалении строки вызывается **deviceList.splice** и **deviceList = deviceList**. +- **settingsJson** — для режима (udps: авто/ручной), заголовок карточки и кнопка «Добавить устройство» зависят от **settingsJson.udps**. +- **showInput**, **newDevice** — форма добавления устройства (name, ip, id); при сохранении вызывается **addDevInList()** (в App добавляет newDevice в incDeviceList, devListCombine, connectToAllDevices), затем **saveList()** (отправка **/tsil|** с deviceList). +- **saveList**, **saveSett**, **sendToAllDevices**, **applicationReboot** — передаются с App; **devListOverride** передаётся (резерв/будущее использование). +- **percent** — для прогресс-бара (таймер реконнекта). + +### Критичная логика на странице + +- **deleteLineFromDevlist(i)** — удаление устройства из списка по индексу (splice), не удалять первую строку (i > 0 в шаблоне для кнопки удаления). [L23–32, L95–96] +- **onModeChange()** — при переключении udps (авто/ручной): saveSett(), applicationReboot(). [L34–38, L132] +- **onSaveList()** — в ручном режиме: при showInput вызов addDevInList() и при успехе saveList() и applicationReboot(); без showInput — только saveList() и applicationReboot(). В авторежиме — saveList() и alert о переходе в ручной режим. [L41–64] +- **saveList()** в App отправляет **/tsil|** с JSON deviceList (со статусами, сброшенными в false). [App.svelte L898–906] + +### Что при рефакторинге сохранить + +- Отображение deviceList в таблице (ws+1 как №, name, ip, id, fv, status, ping) и реактивность при изменении deviceList. +- Не ломать вызовы addDevInList, saveList, saveSett, sendToAllDevices, applicationReboot и передачу devListOverride. Сохранение списка на устройство (/tsil|) и логика ручной/авто режима (udps) должны работать как сейчас. + +--- + +## Данные не в blob и системные команды + +Часть обмена с устройством идёт **текстовыми (string) сообщениями**, а не бинарным blob. При рефакторинге разделение string/blob и набор команд менять нельзя. + +### Входящие данные не в blob (WebSocket message, string) + +Обработка в `socket[ws].addEventListener("message", ...)` (App.svelte L312–330): + +- **Только один тип строки обрабатывается**: `event.data === "/tstr|"`. + - Условие: `typeof event.data === "string"` и точное совпадение `data === "/tstr|"`. + - Действие: вызов **ack(ws, true)** — снятие таймаута ожидания ответа, запись ping. Это ответ устройства на команду **/tst|** (heartbeat). +- Все остальные входящие данные обрабатываются только если `event.data instanceof Blob` (parseBlob/parseAllBlob). Строковые сообщения, отличные от "/tstr|", сейчас **игнорируются** (не ломать: не добавлять лишнюю обработку и не удалять проверку на "/tstr|"). + +### Исходящие системные команды (что отправляется по WebSocket) + +Все отправки через **wsSendMsg(ws, msg)** или **sendToAllDevices(msg)**. Формат сообщения — строка; для команд с телом после `|` идёт JSON или текст без изменения формата. + +| Команда | Кто вызывает | Назначение | + +|---------|----------------|------------| + +| **currentPageName** (напр. `/\|`, `/config|`) | handleNavigation, open handler | Запрос данных страницы | + +| `/devlist|` | open (только ws===0, firstDevListRequest) | Запрос списка устройств | + +| `/tst|` | wsTestMsgTask (heartbeat) | Проверка живости; ответ — "/tstr" | + +| `/params|` | sortingLayout(ws) | После layout: запрос params | + +| `/charts|` | updateAllStatuses(ws) | После params: запрос данных графиков | + +| `/tuoyal|` + JSON | saveConfig, saveMqtt | Layout виджетов (reversed "layout") | + +| `/gifnoc|` + JSON | saveConfig | Конфиг (reversed "config") | + +| `/oiranecs|` + scenarioTxt | saveConfig | Сценарий (reversed "scenario") | + +| `/sgnittes|` + JSON | saveSett, saveMqtt | Настройки (reversed "settings") | + +| `/tsil|` + JSON | saveList | Список устройств (reversed "list") | + +| `/clean|` | cleanLogs | Очистка логов на устройстве | + +| `/mqtt|` | saveMqtt | Применение MQTT/настроек | + +| `/control|` + key + "/" + status | wsPush (dashboard) | Управление виджетом по topic key | + +| `/scan|` | ssidClick | Сканирование Wi‑Fi | + +| `/reboot|` | rebootEsp | Перезагрузка устройства | + +| `/update|` + path | updateBuild | OTA-обновление по path | + +| `/rorre|` + JSON | cancelAlarm | Сброс ошибки (reversed "error") | + +| `/order|` + JSON | moduleOrder (Config) | Порядок/параметры модуля | + +При выносе транспорта в api контракт «строка msg без изменения» сохранить; не менять разделитель `|` и имена команд. + +--- + +## Экспорт конфигураций (детали формата) + +Чтобы при рефакторинге не сломать совместимость файлов и портала. + +### Экспорт (createExportFile, Config.svelte) + +- **Объект**: `exportJson = { mark: "iotm", config: configJson }`. Поле **config** — текущий массив конфигурационных элементов (как в таблице). +- **Строка для файла**: сначала **syntaxHighlight(JSON.stringify(exportJson))** — т.е. JSON с отступами (pretty-print), затем конкатенация **"\n\nscenario=>"** и **scenarioTxt** (сырой текст сценария, может содержать переносы строк и любые символы). +- **Файл**: сохраняется как `export.json`, MIME при сохранении — `application/json` (фактически содержимое — JSON часть + литерал `\n\nscenario=>` + текст). Имя файла и разделитель "scenario=>" не менять. + +**syntaxHighlight**: парсит JSON и переформатирует с отступами (4 пробела), экранирует HTML в строках для безопасного вывода; при экспорте используется только переформатирование. Импорт читает файл как текст и парсит только часть до "scenario=>". + +### Импорт (реакция на выбор files) + +- Прочитать файл как текст (files[0].text()). +- Проверки по порядку: (1) в тексте есть подстрока **"scenario=>"**; (2) часть **до** "scenario=>" (jsonPart) парсится как JSON (IsJsonParse); (3) в объекте есть **mark === "iotm"**. Иначе — alert и выход. +- При подтверждении пользователем (window.confirm): **configJson = json.config**, **scenarioTxt =** часть текста **после** "scenario=>" (deleteBeforeDelimiter). Очистка полей перед присвоением (configJson = [], scenarioTxt = "") для реактивности. +- **selectToMarker(str, "scenario=>")** — строка до первого вхождения "scenario=>"; **deleteBeforeDelimiter(str, "scenario=>")** — строка после "scenario=>". Один и тот же разделитель при экспорте и импорте. + +### Публикация на портал (makePost) + +- Тело: объект с полями category, topic (ru/en), text (ru/en), **config**, **scenario**, gallery, type "iotmpost", username. Конфиг и сценарий те же, что в экспорте. Ответ портала: при успехе открывается ссылка с id и token в query. Отдельно от файлового экспорта, но те же config/scenario — не менять структуру полей. + +--- + +## Слои WebSocket (полная инвентаризация) + +Перед любым выносом WS в api нужно учитывать все слои и зависимости. + +### Слой 1 — Транспорт (создание, события, send) + +- **Хранение**: массив `socket[]`, индекс = `device.ws` (то же, что индекс в `deviceList`). +- **wsConnect(ws)** (L269–277): `getIP(ws)` по `deviceList` → `socket[ws] = new WebSocket("ws://" + ip + ":81")`, затем `socket.binaryType = "blob"` (сейчас вешается на массив; корректнее `socket[ws].binaryType = "blob"`). +- **getIP(ws)** (L280–287): обход `deviceList`, возврат `device.ip` при `device.ws === ws`. +- **wsEventAdd(ws)** (L290–345): вешает на `socket[ws]`: + - **open**: `markDeviceStatus(ws, true)`; при `firstDevListRequest && ws === 0` → `wsSendMsg(ws, "/devlist|")`; при `currentPageName === "/|"` → `wsSendMsg(ws, currentPageName)`; иначе при `ws === selectedWs` → `sendCurrentPageNameToSelectedWs()`. + - **message**: строка `"/tstr|"` → `ack(ws, true)`; `Blob` → если `ws === selectedWs` то `parseBlob(data, ws)`, если `currentPageName === "/|"` то `parseAllBlob(data, ws)`. + - **close** / **error**: `markDeviceStatus(ws, false)`. +- **wsSendMsg(ws, msg)** (L1085–1092): если `socket[ws] && socket[ws].readyState === 1` → `socket[ws].send(msg)`. + +Зависимости слоя 1 от состояния App: `deviceList`, `selectedWs`, `currentPageName`, `firstDevListRequest`; вызовы в App: `markDeviceStatus`, `wsSendMsg`, `sendCurrentPageNameToSelectedWs`, `parseBlob`, `parseAllBlob`, `ack`. + +### Слой 2 — Реконнект и heartbeat + +- **wsTestMsgTask()** (L1037–1063): каждую секунду (`setTimeout(wsTestMsgTask, 1000)`); уменьшает `remainingTimeout`; при 0 обходит `deviceList`: если устройство offline → `wsConnect(ws)`, `wsEventAdd(ws)`; иначе → `wsSendMsg(ws, "/tst|")`, `ack(ws, false)`. +- **ack(ws, st)** (L1065–1083): при `st === false` — ставит таймаут `waitingAckTimeout` (18 с), по срабатыванию вызывает `markDeviceStatus(ws, false)`; при `st === true` — снимает таймаут, считает `ping[ws]`, пишет `deviceList[i].ping`. + +Зависимости: `deviceList`, `socket`, `reconnectTimeout`, `remainingTimeout`, `preventReconnect`, `rebootOrUpdateProcess`, `socketConnected`, `percent`, `showAwaitingCircle`; массивы `ackTimeoutsArr`, `startMillis`, `ping`. + +### Слой 3 — Бинарный протокол (парсинг Blob) + +Формат: 6 байт заголовок (текст), 4 байта размер (текст), затем payload. Вспомогательные функции: **getPayloadAsJson(blob, size, out)**, **getPayloadAsTxt(blob, size)**, **getJsonAsJson(blob, size, out)** (L565–595). + +- **parseBlob(blob, ws)** (L347–474): по заголовку пишет в состояние App и в `parsed.*`; в конце вызывает **onParced()**. + - Заголовки: `itemsj` → itemsJson; `widget` → widgetsJson; `config` → configJson; `scenar` → scenarioTxt; `settin` → settingsJson; `ssidli` → ssidJson; `errors` → errorsJson; `devlis` → incDeviceList + **initDevList()**; `prfile` → flashProfileJson; `otaupd` → otaJson; `corelg` → **addCoreMsg(txt)**. +- **parseAllBlob(blob, ws)** (L476–563): по заголовку обновляет layout/params и вызывает колбэки. + - `status` → **updateWidget(statusJson)** (обновляет `layoutJson[i]` по topic). + - `layout` → **combineLayoutsInOne(ws, devLayout)** → дополняет `layoutJson`, затем **sortingLayout(ws)** → в конце `wsSendMsg(ws, "/params|")`. + - `params` → мержит в `paramsJson`, **updateAllStatuses(ws)** (обход layoutJson + `wsSendMsg(ws, "/charts|")`), **onParced()**. + - `charta` / `chartb` → **apdateWidgetByArray(...)** (обновление `layoutJson` по topic, накопление массивов). + +Цепочки из парсеров: **initDevList()** → devListOverride/devListCombine → **connectToAllDevices()**; **sortingLayout** → wsSendMsg; **updateAllStatuses** → wsSendMsg; **onParced()** → getVersionsList, getModInfo, getProfile, pageReady.*. + +### Слой 4 — Использование WS на уровне приложения + +Вызовы **wsSendMsg** / **sendToAllDevices** из: навигация (handleNavigation, sendCurrentPageNameToSelectedWs), saveConfig/saveSett/saveList/saveMqtt/cleanLogs, wsPush (dashboard), rebootEsp, updateBuild, cancelAlarm, ssidClick, moduleOrder, ListPage (sendToAllDevices), wsTestMsgTask, sortingLayout, updateAllStatuses. + +Команды, уходящие по WS: `currentPageName` (страница), `/devlist|`, `/tst|`, `/params|`, `/charts|`, `/tuoyal|...`, `/gifnoc|...`, `/oiranecs|...`, `/sgnittes|...`, `/tsil|...`, `/clean|`, `/mqtt|`, `/control|key/status`, `/scan|`, `/reboot|`, `/update|path`, `/rorre|...`, `/order|...`. + +--- + +## Графики (charts): данные, протокол, костыли + +Полная совместимость с серверной частью обязательна — форматы менять нельзя. + +### Цепочка запросов + +1. После прихода **layout** (parseAllBlob): `combineLayoutsInOne` → `sortingLayout(ws)` → отправка `**/params|**`. +2. После прихода **params** (parseAllBlob): мерж в `paramsJson`, **updateAllStatuses(ws)** (подставляет значения из paramsJson в `layoutJson[i].status` по topic), затем отправка `**/charts|**`. +3. Устройство отвечает пакетами с заголовками **charta** или **chartb**; данные попадают в `layoutJson[i].status` по совпадению **topic** через **apdateWidgetByArray** (опечатка в имени: «apdate»). + +### Формат blob для charta (особенности и костыли) + +- Стандартный префикс: байты 0–6 — заголовок `"charta"`, 7–11 — размер `size` (текст). +- **Два фрагмента в одном blob** (в отличие от остальных типов): + - **Доп. JSON (метаданные, topic и т.д.)**: байты **12…size** читаются через **getJsonAsJson(blob, size, out)** → `blob.slice(12, size)` → парсинг как JSON. Это единственное место в проекте, где используется срез `12..size` (все остальные — `size..length` или `0..6`, `7..11`). + - **Данные графика (массив точек)**: байты **size…length** читаются через **getPayloadAsTxt(blob, size)**. +- **Костыль парсинга payload**: сервер отдаёт текст, который не является валидным JSON-массивом. Код приводит его к массиву так: `txt = "[" + txt.substring(0, txt.length - 1) + "]"` — обрезается последний символ (ожидается лишняя запятая или символ в конце) и строка оборачивается в `[]`. Менять эту логику нельзя без изменения сервера. +- Итог: `finalDataJson = { status: chartJson, ...addJson }`; в `addJson` должен быть **topic** для сопоставления с `layoutJson[i].topic`; вызов **apdateWidgetByArray(finalDataJson)** дописывает/объединяет массив в `layoutJson[i].status`. + +### Формат blob для chartb + +- Обычный формат: заголовок `"chartb"`, размер, payload — один JSON через **getPayloadAsJson(blob, size, out)**. В payload ожидается объект с полем **topic** и массивом **status**; тот же **apdateWidgetByArray(status)** сливает данные в соответствующий виджет по topic. + +### Виджет Chart.svelte и структура status + +- В **layoutJson** у элемента с `widget === "chart"` поле **status** — массив точек: `[{ x, y1 }, ...]` (x — timestamp в секундах, y1 — значение). +- **Chart.svelte** (svelte-frappe-charts): читает `widget.status` как массив; для типа `bar` — метки по дате (getDDMM), для `line` — первая точка по дате, остальные по времени getHHMM. Поддерживается **widget.maxCount**: при `maxCount === 0` график очищается (`widget.status = []`), иначе данные накапливаются. +- Для не-bar графиков в **generateLayout** в layout добавляется дополнительный виджет **input** с типом date и topic `...config.id + "-date"` (выбор диапазона дат для запроса с устройства). + +### Что при рефакторинге не трогать + +- Порядок и размеры срезов blob для charta: 0–6, 7–11, **12–size** (addJson), **size–length** (текст массива). +- Преобразование `"[" + txt.substring(0, txt.length - 1) + "]"` для charta. +- Имя функции **apdateWidgetByArray** (используется в parseAllBlob и при рефакторинге лучше оставить как есть, чтобы не ломать поиск/привязки). +- Связку: params → updateAllStatuses → отправка `/charts|` → приход charta/chartb → apdateWidgetByArray по topic. + +--- + +## Страница конфигуратора и таблица элементов + +### Источники данных (всё с устройства по WS, кроме configurations) + +- **itemsJson** (blob `itemsj`): список элементов для выпадающего списка «добавить элемент». Структура: элементы с полями **num**, **name**; элементы с полем **header** выводятся как ``, без header — как `