--- 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 — как `