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

462 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
IoTManagerWeb Refactor Plan (Cursor-friendly, zero behavior change)
0) Жёсткие правила (обязательные)
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…)
2) Якоря для поиска в коде (вместо 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
3) 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.
4) 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).
5) 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)
6) 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.
7) “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
8) 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/
9) 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/