diff --git a/CLAUDE.md b/CLAUDE.md index 32b993f..182bcab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,13 +7,24 @@ A distributed smart home system with three layers: - **ESP8266/ESP32 firmware** (`devices/`) — IoT devices exposing a REST API - **PHP server** (`server/`) — central backend that manages devices, events, and automation scripts -- **Web client** (`webclient/`) — JavaScript/SCSS frontend bundled with Gulp/esbuild +- **Vue web client** (`webclient-vue/`) — Vue 3 + Pinia + Vite frontend using `gnexus-ui-kit` +- **Legacy web client** (`webclient/`) — old vanilla JS frontend (kept as fallback) --- ## Build & Dev Commands -### Web Client +### Vue Web Client (webclient-vue/) +```bash +cd webclient-vue +npm install # install dependencies (Vue 3, Pinia, Vite, gnexus-ui-kit, Phosphor icons) +npm run dev # starts Vite dev server with proxy to PHP backend +npm run build # production build → dist/ +npm test # runs Vitest unit/component/integration tests +npm run test:coverage # tests with coverage report +``` + +### Legacy Web Client (webclient/) ```bash cd webclient npm install # install dependencies (esbuild, sass, gulp, browser-sync, etc.) @@ -60,7 +71,22 @@ ### Automation Scripts PHP classes in `server/ControlScripts/Scopes/`, extending `ControlScripts` base class. Each Scope class implements four methods: `register_sync_map`, `register_events_handlers`, `register_actions_scripts`, `register_regular_scripts`. All Scopes are auto-loaded at startup. See `docs/control-scripts-guide.md`. -### Web Client Structure +### Vue Web Client Structure (webclient-vue/) +- `src/app/main.js` — Vue app entry (createApp + Pinia + router) +- `src/router/routes.js` — hash-router routes +- `src/api/` — HTTP client: `client.js`, `http.js`, `mappers.js`, `modules/{areas,devices,scripts,scanning}.js` +- `src/stores/` — Pinia stores: `areas.js`, `devices.js`, `scripts.js`, `scanning.js`, `favorites.js` +- `src/components/` — thin UI adapters (`AppShell`, `AppEmptyState`, `AppErrorState`, `AppLoadingState`) +- `src/features/{areas,devices,scripts}/pages/` — page components using `gnexus-ui-kit` components +- `src/test/mocks/handlers.js` — MSW mock handlers for integration tests +- `vitest.setup.js` — test setup (jsdom, localStorage mock, router stubs, MSW server) + +### Styling Rules (Vue Client) +- **Always use `gnexus-ui-kit` semantic color classes** (`.text-success`, `.text-danger`, `.bg-primary`, `.badge-warning`, etc.) instead of project-specific CSS custom properties (`--color-accent`, `--color-danger`). +- The kit does not expose CSS variables, so prefer composing with its utility classes or `currentColor` rather than hard-coding hex values. +- This keeps the UI consistent with the design system and avoids drift when the kit's palette evolves. + +### Legacy Web Client Structure (webclient/) - `webclient/src/js/index.js` — app entry point - `webclient/src/js/sh/SmartHomeApi.js` — all server API calls - `webclient/src/js/components/` — UI components (hud, modals, toasts, etc.) @@ -85,10 +111,14 @@ | `server/SHServ/Controllers/` | Request handler logic | | `server/SHServ/Models/` | DB query layer | | `server/console.php` | CLI entry point for server-side scripts | -| `webclient/src/js/sh/SmartHomeApi.js` | JS API client (the contract between frontend and backend) | +| `webclient/src/js/sh/SmartHomeApi.js` | Legacy JS API client | +| `webclient-vue/src/api/client.js` | Vue API client wrapper | +| `webclient-vue/src/app/main.js` | Vue app entry point | | `devices/sh_core_esp8266/src/sh_core.h` | EEPROM layout and all device-side constants | | `docs/device-spec.md` | Device REST API contract (endpoints on the device itself) | | `docs/server-api.md` | **Server REST API** — full reference of all implemented endpoints | | `docs/architecture.md` | Full architecture: firmware contract, events routing, sync map, Fury framework | | `docs/firmware-dev-guide.md` | How to write firmware for a new device type | | `docs/control-scripts-guide.md` | How to write automation scripts (Scope classes) | +| `webclient-vue/docs/migration-plan.md` | Vue client migration plan (Phases 1–6) | +| `webclient-vue/docs/smoke-checklist.md` | UI smoke checklist for releases | diff --git a/webclient-vue/src/components/device/DeviceTable.vue b/webclient-vue/src/components/device/DeviceTable.vue index 2ff6b44..8213308 100644 --- a/webclient-vue/src/components/device/DeviceTable.vue +++ b/webclient-vue/src/components/device/DeviceTable.vue @@ -14,7 +14,14 @@
{{ row.name || row.alias || `Device #${row.id}` }} -
{{ row.alias }} — {{ row.device_ip || '—' }}
@@ -82,10 +89,6 @@ return TYPE_ICONS[type] || TYPE_ICONS.relay; } -function connectionClass(status) { - return status === "active" ? "is-online" : "is-offline"; -} - function stateFor(device) { return ( devicesStore.stateByDeviceId[String(device.id)] || { @@ -137,16 +140,8 @@ height: 8px; border-radius: 50%; flex-shrink: 0; -} - -.status-dot.is-online { - background: var(--color-accent); - box-shadow: 0 0 0 2px rgba(0, 245, 160, 0.25); -} - -.status-dot.is-offline { - background: var(--color-danger); - box-shadow: 0 0 0 2px rgba(255, 61, 0, 0.25); + background: currentColor; + box-shadow: 0 0 4px currentColor; } .device-info small { diff --git a/webclient-vue/src/features/devices/components/DeviceChannelsState.vue b/webclient-vue/src/features/devices/components/DeviceChannelsState.vue new file mode 100644 index 0000000..033b836 --- /dev/null +++ b/webclient-vue/src/features/devices/components/DeviceChannelsState.vue @@ -0,0 +1,193 @@ + + + + + diff --git a/webclient-vue/src/stores/devices.js b/webclient-vue/src/stores/devices.js index bc108a0..f33be24 100644 --- a/webclient-vue/src/stores/devices.js +++ b/webclient-vue/src/stores/devices.js @@ -102,6 +102,58 @@ }; } + async function loadStatesFor(targets, options = {}) { + const runId = stateRunId.value + 1; + stateRunId.value = runId; + isLoadingStates.value = true; + stateError.value = null; + + const items = []; + for (const device of targets) { + if (device.connection_status === "lost") { + setDeviceState(device, { + status: "skipped", + message: "Connection lost", + connectionStatus: "lost", + }); + } else { + setDeviceState(device, { + status: "loading", + message: "Loading", + }); + items.push(device); + } + } + + try { + await runLimited(items, options.concurrency || DEFAULT_STATE_CONCURRENCY, async (device) => { + const result = await devicesApi.status(device.id); + + if (stateRunId.value !== runId) { + return; + } + + stateByDeviceId.value = { + ...stateByDeviceId.value, + [getDeviceId(device)]: result.ok + ? normalizeStatusSuccess(device, result) + : normalizeStatusError(device, result), + }; + }); + } catch (error) { + if (stateRunId.value === runId) { + stateError.value = { + type: "state_loader_error", + message: error?.message || "Device states loader failed", + }; + } + } finally { + if (stateRunId.value === runId) { + isLoadingStates.value = false; + } + } + } + async function loadDeviceStates(options = {}) { const runId = stateRunId.value + 1; stateRunId.value = runId; @@ -269,6 +321,7 @@ loadDevices, setDeviceState, loadDeviceStates, + loadStatesFor, rebootDevice, loadDeviceDetail, loadDeviceStatus,