mirror of
https://github.com/IoTManagerProject/IoTManagerWeb.git
synced 2026-03-26 15:02:21 +03:00
mocup backend for testing
This commit is contained in:
461
plan.md
Normal file
461
plan.md
Normal file
@@ -0,0 +1,461 @@
|
||||
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 App’s 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 3–4, 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/
|
||||
|
||||
|
||||
Reference in New Issue
Block a user