Files
IoTManagerWeb/plan.md
2026-02-07 00:38:05 +01:00

17 KiB
Raw Permalink Blame History

IoTManagerWeb Refactor Plan (Cursor-friendly, zero behavior change)

  1. Жёсткие правила (обязательные)

0.1. Backend = reference only • Backend не меняем. • При сомнениях: ../IoTManager → IoTManager/src/WsServer.cpp и src/modules/virtual/Loging*.

0.2. Протокол/форматы не менять • Команды и разделитель | — без изменений. • Blob протокол: 6-символьный header + размер (4 байта текстом) + payload. • Единственная входящая строка, которую обрабатываем: event.data === "/tstr|". Всё остальное string — игнор.

0.3. Не “улучшать” странности • Не переименовывать apdateWidgetByArray (опечатка намеренно). • Не менять reverse-команды: /tuoyal|, /gifnoc|, /oiranecs|, /sgnittes|, /tsil|, /rorre| и т.д. • Не “чинить” костыли charta (срезы и substring(0, len-1)).

0.4. Комментарии в новом коде — только EN • В существующих файлах можно не трогать.

  1. Инварианты (то, что нельзя сломать)

Сохраняем и проверяем после каждого шага.

1.1. Критичная цепочка: первое устройство → devlist → подключение ко всем

Исходное состояние: • deviceList на старте содержит 1 элемент: { name:"--", id:"--", ip: myip, ws:0, status:false }. • firstDevListRequest = true ставится в onMount перед connectToAllDevices().

Шаг 1: подключаем только ws=0 • connectToAllDevices() создаёт сокеты для устройств где status === false || undefined. • Изначально это только индекс 0 → подключение к myip:81.

Шаг 2: в open handler • Только когда firstDevListRequest && ws === 0 → отправляем "/devlist|". • Это должно быть один раз (первый open первого сокета).

Шаг 3: приём devlis • В parseBlob по header === "devlis": incDeviceList = out.json → вызвать initDevList().

Шаг 4: initDevList() • Если firstDevListRequest: • devListOverride() полностью заменяет deviceList = incDeviceList, сортирует, • затем обязательно: deviceList[0].status = true (чтобы не переподключить первый сокет). • Иначе: • devListCombine() (combineArrays по IP + sortList). • Затем: firstDevListRequest = false → снова connectToAllDevices().

Запрещено менять порядок: onMount → firstDevListRequest=true → connectToAllDevices → open(ws0) → /devlist| → devlis → initDevList → override/combine → connectToAllDevices снова

1.2. Страница List (devices list) — контракт не ломать • Таблица строится по deviceList. • Удалять первую строку нельзя (кнопка удаления только при i > 0). • udps режим: • onModeChange: saveSett() + applicationReboot(). • onSaveList: • manual: если showInput → addDevInList() → saveList() → applicationReboot(). • manual без showInput → saveList() → applicationReboot(). • auto → saveList() + alert о переходе в ручной. • saveList() отправляет /tsil| с JSON deviceList, но статусы сбрасываются в false перед отправкой.

1.3. String vs Blob входящие

В message handler: • typeof event.data === "string": • обрабатывается только точное совпадение "/tstr|" → ack(ws, true). • любые другие строки игнорируем. • Все полезные данные приходят как Blob → только parseBlob/parseAllBlob.

1.4. Отправка данных устройству (WS) • Все команды уходят как строка через wsSendMsg(ws, msg)/sendToAllDevices(msg). • Формат не менять: "/command|payload" где payload JSON/текст. • На backend sendStringToWs(...) отправляет бинарно, но на фронте это всё равно Blob. Исключение — "/tstr|".

1.5. Charts — строгая совместимость

Цепочка: • пришёл layout → sortingLayout(ws) → отправить "/params|". • пришёл params → updateAllStatuses(ws) → отправить "/charts|". • пришли charta/chartb → apdateWidgetByArray(...) по topic.

charta: особый формат • header: bytes 0..6 = "charta" • size: bytes 7..11 (текст) • addJson: bytes 12..size (JSON) через getJsonAsJson(blob, size, out) • payload points: bytes size..end (TEXT) через getPayloadAsTxt(blob, size) • костыль массива: txt = "[" + txt.substring(0, txt.length - 1) + "]" — не менять

chartb: • обычный JSON payload через getPayloadAsJson(blob, size, out).

1.6. Export/Import config — формат файла не менять

Export: • exportJson = { mark: "iotm", config: configJson } • файл: pretty JSON + "\n\nscenario=>" + scenarioTxt • имя: export.json, MIME application/json

Import: • читать текст • проверки по порядку: 1. содержит "scenario=>" 2. jsonPart парсится 3. json.mark === "iotm" • затем (после confirm): • configJson = json.config • scenarioTxt = txtPart • перед присвоением: configJson = [], scenarioTxt = "" для реактивности

1.7. Backend reference locations (для сверки) • IoTManager/src/WsServer.cpp — текстовые команды и отправка • IoTManager/src/modules/virtual/Loging*/ — charta/chartb • IoTManager/data_svelte/ — test fixtures (items/config/widgets/layout/scenario/settings…)

  1. Якоря для поиска в коде (вместо line numbers)

Cursor должен ориентироваться по строкам/сигнатурам.

2.1. devlist chain anchors • "/devlist|" (только в open handler) • "devlis" (в parseBlob) • firstDevListRequest • deviceList[0].status = true • connectToAllDevices()

2.2. message handler anchors • event.data === "/tstr|" (строгая проверка) • event.data instanceof Blob

2.3. charts anchors • "charta" / "chartb" • getJsonAsJson(blob, size, out) • txt.substring(0, txt.length - 1) • "/params|", "/charts|"

2.4. export/import anchors • "scenario=>" • mark: "iotm" • export.json

  1. Backups (Step 1 — MUST DO)

3.1. Create _backup_refactor/ and copy files

Create folder _backup_refactor/ in project root (or src/_backup_refactor/).

Copy: • src/App.svelte • src/main.js • src/pages/{Config,Connection,Dashboard,List,Login,Profile,System}.svelte • src/widgets/{Chart,Input,Range,Toggle,Anydata}.svelte • src/components/{Alarm,Card,Modal,ModalPass,Progress}.svelte

Optional: add _backup_refactor/ to .gitignore.

DoD • Backup folder exists and contains exact copies.

Quick check • diff -ru src/App.svelte _backup_refactor/App.svelte shows identical before any edits.

  1. Refactor Steps (small, safe, incremental)

Важное правило для Cursor: каждый шаг — отдельный минимальный дифф, после него smoke-check.

Step A — HTTP transport wrapper (priority 1, low risk)

Goal: вынести fetch-логику из App/pages в src/api/.

A1. Create src/api/http.js • helper requestJson({ url, method, headers, body, mode }) • returns { ok, status, data } or throws with structured error (но не ломаем поведение: лучше возвращать ok=false).

DoD • New module exists, used by ровно одной функцией (например getUser) без изменения результата.

A2. Create src/api/portal.js • Move portal calls: • getUser (/api/user/email, Bearer token token_iotm2) • getModInfo, getProfile (portal/compiler) — как сейчас • getConfigs, makePost (Config page) — как сейчас

DoD • В App/pages только импорт и вызов, UI поведение не меняется.

A3. Create src/api/firmware.js • Move getVersionsList (ver.json from settingsJson.serverip)

DoD • Версии как раньше отображаются/подгружаются.

Checks • Логин/профиль/конфиг-портал/версия работают как до.

Step B — WebSocket transport wrapper deviceSocket.js (priority 2, medium risk)

Goal: вынести только создание сокета + send/isOpen. Никакого протокола, никакого parse, никакого reconnect.

B1. Create src/api/deviceSocket.js Exports: • createConnection(wsIndex, ip, callbacks) • const socket = new WebSocket("ws://" + ip + ":81") • socket.binaryType = "blob" (fix bug: on instance) • socket.addEventListener("open"...) etc • store sockets by wsIndex in private map/array • send(wsIndex, msg) (only if open) • isOpen(wsIndex) • optional getSocket(wsIndex) (only if needed for debug parity)

Callbacks object:

{ onOpen(wsIndex, event), onMessage(wsIndex, data, event), onClose(wsIndex, event), onError(wsIndex, event), }

DoD • App still controls: • when to send /devlist| • whether to parseBlob/parseAllBlob • ack/reconnect • Only transport moved.

B2. Replace wsConnect/wsSendMsg usage minimally • Keep Apps logic structure intact. • wsConnect(ws) becomes something like: • compute ip via existing getIP(ws) • call deviceSocket.createConnection(ws, ip, { callbacks calling existing handlers })

Hard constraints • open handler must still contain: • if (firstDevListRequest && ws === 0) wsSendMsg(ws, "/devlist|"); • (или эквивалент через deviceSocket.send) • message handler must still contain strict /tstr| check and Blob-only parsing.

DoD • First device → devlist chain works unchanged. • binaryType is correct on each socket instance.

Checks (manual) • On first open of ws=0 exactly one /devlist| goes out. • devlis arrives and triggers initDevList. • After that, new sockets created for other devices.

Step C — UI layout components (priority 3, low risk)

Goal: split header/nav/footer into components without changing props/logic.

Create: • src/components/layout/AppHeader.svelte • src/components/layout/AppNav.svelte • src/components/layout/AppFooter.svelte (optional)

DoD • App.svelte becomes smaller but routes/props unchanged. • No logic moved besides markup.

Step D — Extract logic into lib/ (priority 34, highest risk; do in micro-steps)

Golden rule: move one “узкий контракт” at a time.

D1. src/lib/deviceListManager.js Exports: • sortList(list) • combineArrays(a, b) (merge by IP, preserve ws/index rules) • devListOverride(deviceList, incDeviceList): • returns new list or mutates (choose one, but keep App reactive updates) • MUST set deviceList[0].status = true • devListCombine(deviceList, incDeviceList) • initDevList({ deviceList, incDeviceList, firstDevListRequest }): • chooses override vs combine • returns { deviceList: newList, firstDevListRequest: false }

DoD • App still calls initDevList() from devlis branch, and then calls connectToAllDevices() exactly like before.

Checks • first device is not reconnected (status true for index 0 after override).

D2. src/lib/deviceConnection.js Exports: • getIP(wsIndex, deviceList) (single source of truth) • connectToAllDevices({ deviceList, createConnection }) • createConnection is from api/deviceSocket • connect only devices with status false/undefined • createOpenHandler({ firstDevListRequestRef, currentPageNameRef, selectedWsRef, markDeviceStatus, send, sendCurrentPageNameToSelectedWs }) • returns onOpen(wsIndex) • MUST keep logic: • markDeviceStatus(ws, true) • if firstDevListRequest && ws===0 → send(”/devlist|”) • page request logic as before • handleDevListReceived({ incDeviceList, getState, setState, initDevList, connectToAllDevices }) • calls deviceListManager.initDevList • then calls connectToAllDevices again

DoD • The entire chain “first device → devlis → connect all” remains identical, only moved.

Checks • same order of events, same number of connects/sends.

D3. src/lib/blobProtocol.js Goal: move Blob parsing mechanics (not “what to do after parsing”). Exports: • getPayloadAsJson, getPayloadAsTxt, getJsonAsJson • parseBlob(blob, ws, handlers) • parseAllBlob(blob, ws, handlers)

handlers is a map from header to callback: • itemsj, widget, config, scenar, settin, ssidli, errors, devlis, prfile, otaupd, corelg • for devlis, handler must call handleDevListReceived(...) (passed from App)

Charts handling inside parseAllBlob must preserve: • layout flow triggers /params| • params flow triggers /charts| • charta slicing + substring hack • chartb normal JSON

DoD • App owns: state vars, onParced, pageReady, and when to clear/reset. • blobProtocol only parses and calls callbacks.

D4. src/lib/wsReconnect.js Exports: • ack(ws, st) with per-ws timeouts and ping calc • wsTestMsgTask() style function (or start/stop wrapper)

Hard constraints • ack timeout logic stays identical: • on false → start waiting timeout (18s) → mark offline • on true → clear timeout and compute ping • send heartbeat /tst| for online sockets, reconnect offline sockets.

DoD • Heartbeat and reconnect behavior unchanged in UI (percent/timers etc).

  1. App.svelte end state (what remains)

App should keep: • All state variables (deviceList/layoutJson/paramsJson/parsed/pageReady/settingsJson/configJson/scenarioTxt/etc). • Routing (tinro), handleNavigation, UI gating (Alarm vs Routes). • onParced logic and calls to HTTP functions (getVersionsList/getModInfo/getProfile). • Wiring: builds handler maps and passes them into deviceSocket, blobProtocol, deviceConnection, wsReconnect.

App should NOT own: • low-level WS creation (deviceSocket) • list merge/sort logic (deviceListManager) • blob slicing/parsing mechanics (blobProtocol) • reconnect/ack timer internals (wsReconnect) • connection lifecycle logic (deviceConnection)

  1. Smoke Test (manual, but strict)

Run after each step.

6.1. Boot + devlist chain • Open app with one known IP (myip). • Confirm: 1. connects only ws=0 initially 2. on open(ws=0) sends /devlist| 3. receives devlis (Blob) 4. initDevList runs, override sets deviceList[0].status = true 5. connects to other devices (ws=1..n)

6.2. Dashboard chain • On /: • after layout → sends /params| • after params → sends /charts| • receives charta/chartb updates widgets by topic

6.3. List page • Cannot delete first row. • Add device in manual mode works. • Save list sends /tsil| with statuses reset false. • udps toggle triggers saveSett + reboot.

6.4. Config page • Export produces JSON + scenario=>. • Import validates scenario=>, JSON, mark=“iotm”. • After import config/scenario apply correctly. • moduleOrder buttons (btn*) still send /order|....

6.5. Heartbeat • /tst| goes out periodically, /tstr| comes back as string and triggers ack only. • Any other string message is ignored.

  1. “Ripgrep checklist” (after each step)

Cursor должен прогонять поиск и убедиться, что паттерны на месте. • rg -n 'firstDevListRequest' src/App.svelte src/lib -S • rg -n '"/devlist|"' src -S • rg -n 'event.data === "/tstr|"' src -S • rg -n 'instanceof Blob' src -S • rg -n 'charta|chartb' src -S • rg -n 'substring(0, txt.length - 1)' src -S • rg -n 'scenario=>' src -S • rg -n 'mark:\s*"iotm"' src -S • rg -n 'apdateWidgetByArray' src -S

  1. Backend reference notes (quick) • Text commands parsing: IoTManager/src/WsServer.cpp (headerStr до |) • sendFileToWsByFrames: binary frames with 6-char headers (itemsj/widget/config/scenar/settin/layout/params/devlis/prfile/otaupd/charta…) • sendStringToWs: header|0012|payload but sent as binary (front receives Blob) • Exception: /tstr| sent as WStype_TEXT and arrives as string • Logging modules: IoTManager/src/modules/virtual/Loging*/ produce charta/chartb • Test fixtures: IoTManager/data_svelte/

  1. Target folder tree (after completion)

src/ api/ http.js portal.js firmware.js deviceSocket.js lib/ blobProtocol.js deviceListManager.js deviceConnection.js wsReconnect.js dashboardLayout.js # optional components/layout/ AppHeader.svelte AppNav.svelte AppFooter.svelte App.svelte pages/ widgets/ ... _backup_refactor/