diff --git a/CLAUDE.md b/CLAUDE.md index d150201..7af1001 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,16 +7,16 @@ 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 -- **Vue web client** (`webclient-vue/`) — Vue 3 + Pinia + Vite frontend using `gnexus-ui-kit` +- **Vue web client** (`webclient/`) — Vue 3 + Pinia + Vite frontend using `gnexus-ui-kit` - **Legacy web client** (`webclient_legacy/`) — old vanilla JS frontend (archived, no longer served) --- ## Build & Dev Commands -### Vue Web Client (webclient-vue/) +### Vue Web Client (webclient/) ```bash -cd webclient-vue +cd webclient 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/ @@ -71,7 +71,7 @@ ### 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`. -### Vue Web Client Structure (webclient-vue/) +### Vue Web Client Structure (webclient/) - `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` @@ -112,13 +112,13 @@ | `server/SHServ/Models/` | DB query layer | | `server/console.php` | CLI entry point for server-side scripts | | `webclient_legacy/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 | +| `webclient/src/api/client.js` | Vue API client wrapper | +| `webclient/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 | +| `webclient/docs/migration-plan.md` | Vue client migration plan (Phases 1–6) | +| `webclient/docs/smoke-checklist.md` | UI smoke checklist for releases | diff --git a/webclient-vue/.env b/webclient-vue/.env deleted file mode 100644 index a469b08..0000000 --- a/webclient-vue/.env +++ /dev/null @@ -1,3 +0,0 @@ -VITE_API_BASE_URL= -VITE_API_PROXY_PATH=/proxy.php -VITE_API_TIMEOUT_MS=10000 diff --git a/webclient-vue/.env.example b/webclient-vue/.env.example deleted file mode 100644 index d51c7aa..0000000 --- a/webclient-vue/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -VITE_API_BASE_URL=http://smart-home-serv.local -VITE_API_PROXY_PATH=/proxy.php -VITE_API_TIMEOUT_MS=10000 diff --git a/webclient-vue/.gitignore b/webclient-vue/.gitignore deleted file mode 100644 index f4650bb..0000000 --- a/webclient-vue/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules/ -dist/ -.env.local -.env.*.local diff --git a/webclient-vue/coverage/api/client.js.html b/webclient-vue/coverage/api/client.js.html deleted file mode 100644 index ef59379..0000000 --- a/webclient-vue/coverage/api/client.js.html +++ /dev/null @@ -1,283 +0,0 @@ - - - - - - Code coverage report for api/client.js - - - - - - - - - -
-
-

All files / api client.js

-
- -
- 100% - Statements - 12/12 -
- - -
- 83.33% - Branches - 15/18 -
- - -
- 100% - Functions - 4/4 -
- - -
- 100% - Lines - 12/12 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67  -  -  -4x -  -  -  -  -  -  -  -20x -20x -  -18x -1x -  -  -  -  -  -  -  -  -  -17x -1x -  -  -  -  -  -  -  -  -  -  -16x -  -  -  -  -  -2x -2x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -10x -  -  -  -5x -  - 
import { requestHttp } from "./http";
- 
-function makeError(type, message, extra = {}) {
-  return {
-    type,
-    message,
-    ...extra,
-  };
-}
- 
-export async function apiRequest(method, path, body, options) {
-  try {
-    const { response, data, meta } = await requestHttp(method, path, body, options);
- 
-    if (!response.ok) {
-      return {
-        ok: false,
-        error: makeError("http_error", `HTTP ${response.status}`, {
-          statusCode: response.status,
-          raw: data,
-        }),
-        meta,
-      };
-    }
- 
-    if (data && typeof data === "object" && (data.status === false || data.status === "error")) {
-      return {
-        ok: false,
-        error: makeError("api_error", data.msg || data.message || "API error", {
-          errorAlias: data.error_alias,
-          failedFields: data.failed_fields || [],
-          raw: data,
-        }),
-        meta,
-      };
-    }
- 
-    return {
-      ok: true,
-      data,
-      meta,
-    };
-  } catch (error) {
-    const isAbort = error?.name === "AbortError";
-    return {
-      ok: false,
-      error: makeError(isAbort ? "timeout" : "network_error", error?.message || "Network error", {
-        details: error,
-      }),
-      meta: {
-        url: path,
-        method,
-        statusCode: 0,
-        headers: null,
-      },
-    };
-  }
-}
- 
-export function apiGet(path, options) {
-  return apiRequest("GET", path, null, options);
-}
- 
-export function apiPost(path, body, options) {
-  return apiRequest("POST", path, body, options);
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/api/http.js.html b/webclient-vue/coverage/api/http.js.html deleted file mode 100644 index 9a4e8e1..0000000 --- a/webclient-vue/coverage/api/http.js.html +++ /dev/null @@ -1,361 +0,0 @@ - - - - - - Code coverage report for api/http.js - - - - - - - - - -
-
-

All files / api http.js

-
- -
- 92.68% - Statements - 38/41 -
- - -
- 89.18% - Branches - 33/37 -
- - -
- 66.66% - Functions - 4/6 -
- - -
- 97.43% - Lines - 38/39 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -934x -  -  -23x -  -23x -23x -  -  -  -23x -  -  -23x -23x -  -  -  -23x -23x -  -23x -22x -  -  -1x -  -  -  -23x -  -23x -1x -  -  -22x -  -  -  -23x -23x -23x -23x -23x -  -23x -  -  -  -  -23x -  -  -  -  -  -23x -8x -  -  -23x -6x -6x -  -  -23x -23x -23x -23x -  -23x -23x -23x -  -1x -  -  -  -23x -  -  -  -  -  -  -  -  -  -  -23x -  -  - 
const DEFAULT_TIMEOUT_MS = Number(import.meta.env.VITE_API_TIMEOUT_MS || 10000);
- 
-function buildQuery(params) {
-  const query = new URLSearchParams();
- 
-  for (const [key, value] of Object.entries(params || {})) {
-    Iif (value === undefined || value === null) {
-      continue;
-    }
- 
-    query.append(key, String(value));
-  }
- 
-  const serialized = query.toString();
-  return serialized ? `?${serialized}` : "";
-}
- 
-function joinUrl(baseUrl, path) {
-  const base = String(baseUrl || "").replace(/\/+$/, "");
-  const nextPath = String(path || "").replace(/^\/+/, "");
- 
-  if (!base) {
-    return `/${nextPath}`;
-  }
- 
-  return `${base}/${nextPath}`;
-}
- 
-function wrapProxyPath(path, query) {
-  const proxyPath = import.meta.env.VITE_API_PROXY_PATH || "";
- 
-  if (!proxyPath) {
-    return `${path}${buildQuery(query)}`;
-  }
- 
-  return `${proxyPath}${buildQuery({ path, ...(query || {}) })}`;
-}
- 
-export async function requestHttp(method, path, body, options = {}) {
-  const timeoutMs = Number(options.timeoutMs || DEFAULT_TIMEOUT_MS);
-  const controller = new AbortController();
-  const timeout = setTimeout(() => controller.abort(), timeoutMs);
-  const baseUrl = import.meta.env.VITE_API_BASE_URL || "";
-  const url = joinUrl(baseUrl, wrapProxyPath(path, options.query));
- 
-  const headers = {
-    Accept: "application/json",
-    ...(options.headers || {}),
-  };
- 
-  const init = {
-    method,
-    headers,
-    signal: controller.signal,
-  };
- 
-  if (options.signal) {
-    options.signal.addEventListener("abort", () => controller.abort(), { once: true });
-  }
- 
-  if (body !== undefined && body !== null) {
-    headers["Content-Type"] = "application/json";
-    init.body = JSON.stringify(body);
-  }
- 
-  try {
-    const response = await fetch(url, init);
-    const text = await response.text();
-    let data = text;
- 
-    Eif (text) {
-      try {
-        data = JSON.parse(text);
-      } catch (_) {
-        data = text;
-      }
-    }
- 
-    return {
-      response,
-      data,
-      meta: {
-        url,
-        method,
-        statusCode: response.status,
-        headers: response.headers,
-      },
-    };
-  } finally {
-    clearTimeout(timeout);
-  }
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/api/index.html b/webclient-vue/coverage/api/index.html deleted file mode 100644 index 84548cd..0000000 --- a/webclient-vue/coverage/api/index.html +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - Code coverage report for api - - - - - - - - - -
-
-

All files api

-
- -
- 94.82% - Statements - 55/58 -
- - -
- 88.13% - Branches - 52/59 -
- - -
- 81.81% - Functions - 9/11 -
- - -
- 98.21% - Lines - 55/56 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
client.js -
-
100%12/1283.33%15/18100%4/4100%12/12
http.js -
-
92.68%38/4189.18%33/3766.66%4/697.43%38/39
mappers.js -
-
100%5/5100%4/4100%1/1100%5/5
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/api/mappers.js.html b/webclient-vue/coverage/api/mappers.js.html deleted file mode 100644 index 91619f5..0000000 --- a/webclient-vue/coverage/api/mappers.js.html +++ /dev/null @@ -1,145 +0,0 @@ - - - - - - Code coverage report for api/mappers.js - - - - - - - - - -
-
-

All files / api mappers.js

-
- -
- 100% - Statements - 5/5 -
- - -
- 100% - Branches - 4/4 -
- - -
- 100% - Functions - 1/1 -
- - -
- 100% - Lines - 5/5 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -213x -  -  -  -  -  -  -  -  -  -  -  -5x -  -5x -13x -  -  -5x -  - 
const deviceFieldMap = {
-  device_name: "name",
-  device_hard_id: "device_id",
-  device_ip: "ip",
-  device_type: "type",
-  ip_address: "ip",
-  mac_address: "mac",
-  device_mac: "mac",
-  core_version: "firmware_core_version",
-};
- 
-export function unifyDeviceFields(device) {
-  const normalized = {};
- 
-  for (const [field, value] of Object.entries(device || {})) {
-    normalized[deviceFieldMap[field] || field] = value;
-  }
- 
-  return normalized;
-}
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/api/modules/areas.js.html b/webclient-vue/coverage/api/modules/areas.js.html deleted file mode 100644 index 004e101..0000000 --- a/webclient-vue/coverage/api/modules/areas.js.html +++ /dev/null @@ -1,214 +0,0 @@ - - - - - - Code coverage report for api/modules/areas.js - - - - - - - - - -
-
-

All files / api/modules areas.js

-
- -
- 100% - Statements - 11/11 -
- - -
- 100% - Branches - 0/0 -
- - -
- 100% - Functions - 10/10 -
- - -
- 100% - Lines - 11/11 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44  -  -2x -  -5x -  -  -  -1x -  -  -  -2x -  -  -  -2x -  -  -  -1x -  -  -  -1x -  -  -  -2x -  -  -  -1x -  -  -  -1x -  -  -  -1x -  -  - 
import { apiGet, apiPost } from "../client";
- 
-export const areasApi = {
-  list(options) {
-    return apiGet("/api/v1/areas/list", options);
-  },
- 
-  innerList(areaId, options) {
-    return apiGet(`/api/v1/areas/id/${encodeURIComponent(String(areaId))}/list`, options);
-  },
- 
-  newArea(payload) {
-    return apiPost("/api/v1/areas/new-area", payload);
-  },
- 
-  remove(areaId) {
-    return apiGet(`/api/v1/areas/id/${encodeURIComponent(String(areaId))}/remove`);
-  },
- 
-  devices(areaId) {
-    return apiGet(`/api/v1/areas/id/${encodeURIComponent(String(areaId))}/devices`);
-  },
- 
-  scripts(areaId) {
-    return apiGet(`/api/v1/areas/id/${encodeURIComponent(String(areaId))}/scripts`);
-  },
- 
-  updateDisplayName(payload) {
-    return apiPost("/api/v1/areas/update-display-name", payload);
-  },
- 
-  updateAlias(payload) {
-    return apiPost("/api/v1/areas/update-alias", payload);
-  },
- 
-  placeInArea(payload) {
-    return apiPost("/api/v1/areas/place-in-area", payload);
-  },
- 
-  unassign(areaId) {
-    return apiGet(`/api/v1/areas/id/${encodeURIComponent(String(areaId))}/unassign-from-area`);
-  },
-};
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/api/modules/devices.js.html b/webclient-vue/coverage/api/modules/devices.js.html deleted file mode 100644 index ffadda6..0000000 --- a/webclient-vue/coverage/api/modules/devices.js.html +++ /dev/null @@ -1,250 +0,0 @@ - - - - - - Code coverage report for api/modules/devices.js - - - - - - - - - -
-
-

All files / api/modules devices.js

-
- -
- 92.3% - Statements - 12/13 -
- - -
- 75% - Branches - 3/4 -
- - -
- 100% - Functions - 9/9 -
- - -
- 92.3% - Lines - 12/13 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56  -  -  -  -2x -  -  -  -1x -  -  -  -1x -  -1x -  -  -  -  -  -  -  -  -  -  -  -2x -  -1x -  -  -  -1x -  -  -  -1x -  -  -  -1x -  -  -  -3x -  -  -  -1x -  -  -  -2x -  -  - 
import { apiGet, apiPost } from "../client";
-import { unifyDeviceFields } from "../mappers";
- 
-function safeId(id) {
-  return encodeURIComponent(String(id));
-}
- 
-function mapDevicesResponse(result) {
-  Iif (!result.ok) {
-    return result;
-  }
- 
-  const devices = result.data?.data?.devices || [];
- 
-  return {
-    ...result,
-    data: {
-      ...result.data,
-      data: {
-        ...result.data?.data,
-        devices: devices.map(unifyDeviceFields),
-      },
-    },
-  };
-}
- 
-export const devicesApi = {
-  async list() {
-    return mapDevicesResponse(await apiGet("/api/v1/devices/list"));
-  },
- 
-  status(id, options) {
-    return apiGet(`/api/v1/devices/id/${safeId(id)}/status`, options);
-  },
- 
-  reboot(id) {
-    return apiGet(`/api/v1/devices/id/${safeId(id)}/reboot`);
-  },
- 
-  action(payload) {
-    return apiPost("/api/v1/devices/action", payload);
-  },
- 
-  scanningSetup(options) {
-    return apiGet("/api/v1/devices/scanning/setup", options);
-  },
- 
-  scanningAll(options) {
-    return apiGet("/api/v1/devices/scanning/all", options);
-  },
- 
-  setupNewDevice(payload) {
-    return apiPost("/api/v1/devices/setup/new-device", payload);
-  },
-};
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/api/modules/index.html b/webclient-vue/coverage/api/modules/index.html deleted file mode 100644 index 5f51007..0000000 --- a/webclient-vue/coverage/api/modules/index.html +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - Code coverage report for api/modules - - - - - - - - - -
-
-

All files api/modules

-
- -
- 96.77% - Statements - 30/31 -
- - -
- 77.77% - Branches - 7/9 -
- - -
- 100% - Functions - 25/25 -
- - -
- 96.77% - Lines - 30/31 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
areas.js -
-
100%11/11100%0/0100%10/10100%11/11
devices.js -
-
92.3%12/1375%3/4100%9/992.3%12/13
scripts.js -
-
100%7/780%4/5100%6/6100%7/7
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/api/modules/scripts.js.html b/webclient-vue/coverage/api/modules/scripts.js.html deleted file mode 100644 index 198eef3..0000000 --- a/webclient-vue/coverage/api/modules/scripts.js.html +++ /dev/null @@ -1,166 +0,0 @@ - - - - - - Code coverage report for api/modules/scripts.js - - - - - - - - - -
-
-

All files / api/modules scripts.js

-
- -
- 100% - Statements - 7/7 -
- - -
- 80% - Branches - 4/5 -
- - -
- 100% - Functions - 6/6 -
- - -
- 100% - Lines - 7/7 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28  -  -2x -  -3x -  -  -  -1x -  -  -  -1x -  -  -  -2x -  -  -  -2x -  -  -  -1x -  -  - 
import { apiGet, apiPost } from "../client";
- 
-export const scriptsApi = {
-  actionsList(options) {
-    return apiGet("/api/v1/scripts/actions/list", options);
-  },
- 
-  regularList(options) {
-    return apiGet("/api/v1/scripts/regular/list", options);
-  },
- 
-  scopesList(options) {
-    return apiGet("/api/v1/scripts/scopes/list", options);
-  },
- 
-  runAction(alias, params = {}) {
-    return apiPost("/api/v1/scripts/actions/run", { alias, params });
-  },
- 
-  setRegularState(alias, enabled) {
-    return apiGet(`/api/v1/scripts/regular/alias/${encodeURIComponent(alias)}/${enabled ? "enable" : "disable"}`);
-  },
- 
-  setScopeState(name, enabled) {
-    return apiGet(`/api/v1/scripts/actions/scope/${encodeURIComponent(name)}/${enabled ? "enable" : "disable"}`);
-  },
-};
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/base.css b/webclient-vue/coverage/base.css deleted file mode 100644 index f418035..0000000 --- a/webclient-vue/coverage/base.css +++ /dev/null @@ -1,224 +0,0 @@ -body, html { - margin:0; padding: 0; - height: 100%; -} -body { - font-family: Helvetica Neue, Helvetica, Arial; - font-size: 14px; - color:#333; -} -.small { font-size: 12px; } -*, *:after, *:before { - -webkit-box-sizing:border-box; - -moz-box-sizing:border-box; - box-sizing:border-box; - } -h1 { font-size: 20px; margin: 0;} -h2 { font-size: 14px; } -pre { - font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; - margin: 0; - padding: 0; - -moz-tab-size: 2; - -o-tab-size: 2; - tab-size: 2; -} -a { color:#0074D9; text-decoration:none; } -a:hover { text-decoration:underline; } -.strong { font-weight: bold; } -.space-top1 { padding: 10px 0 0 0; } -.pad2y { padding: 20px 0; } -.pad1y { padding: 10px 0; } -.pad2x { padding: 0 20px; } -.pad2 { padding: 20px; } -.pad1 { padding: 10px; } -.space-left2 { padding-left:55px; } -.space-right2 { padding-right:20px; } -.center { text-align:center; } -.clearfix { display:block; } -.clearfix:after { - content:''; - display:block; - height:0; - clear:both; - visibility:hidden; - } -.fl { float: left; } -@media only screen and (max-width:640px) { - .col3 { width:100%; max-width:100%; } - .hide-mobile { display:none!important; } -} - -.quiet { - color: #7f7f7f; - color: rgba(0,0,0,0.5); -} -.quiet a { opacity: 0.7; } - -.fraction { - font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; - font-size: 10px; - color: #555; - background: #E8E8E8; - padding: 4px 5px; - border-radius: 3px; - vertical-align: middle; -} - -div.path a:link, div.path a:visited { color: #333; } -table.coverage { - border-collapse: collapse; - margin: 10px 0 0 0; - padding: 0; -} - -table.coverage td { - margin: 0; - padding: 0; - vertical-align: top; -} -table.coverage td.line-count { - text-align: right; - padding: 0 5px 0 20px; -} -table.coverage td.line-coverage { - text-align: right; - padding-right: 10px; - min-width:20px; -} - -table.coverage td span.cline-any { - display: inline-block; - padding: 0 5px; - width: 100%; -} -.missing-if-branch { - display: inline-block; - margin-right: 5px; - border-radius: 3px; - position: relative; - padding: 0 4px; - background: #333; - color: yellow; -} - -.skip-if-branch { - display: none; - margin-right: 10px; - position: relative; - padding: 0 4px; - background: #ccc; - color: white; -} -.missing-if-branch .typ, .skip-if-branch .typ { - color: inherit !important; -} -.coverage-summary { - border-collapse: collapse; - width: 100%; -} -.coverage-summary tr { border-bottom: 1px solid #bbb; } -.keyline-all { border: 1px solid #ddd; } -.coverage-summary td, .coverage-summary th { padding: 10px; } -.coverage-summary tbody { border: 1px solid #bbb; } -.coverage-summary td { border-right: 1px solid #bbb; } -.coverage-summary td:last-child { border-right: none; } -.coverage-summary th { - text-align: left; - font-weight: normal; - white-space: nowrap; -} -.coverage-summary th.file { border-right: none !important; } -.coverage-summary th.pct { } -.coverage-summary th.pic, -.coverage-summary th.abs, -.coverage-summary td.pct, -.coverage-summary td.abs { text-align: right; } -.coverage-summary td.file { white-space: nowrap; } -.coverage-summary td.pic { min-width: 120px !important; } -.coverage-summary tfoot td { } - -.coverage-summary .sorter { - height: 10px; - width: 7px; - display: inline-block; - margin-left: 0.5em; - background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; -} -.coverage-summary .sorted .sorter { - background-position: 0 -20px; -} -.coverage-summary .sorted-desc .sorter { - background-position: 0 -10px; -} -.status-line { height: 10px; } -/* yellow */ -.cbranch-no { background: yellow !important; color: #111; } -/* dark red */ -.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } -.low .chart { border:1px solid #C21F39 } -.highlighted, -.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ - background: #C21F39 !important; -} -/* medium red */ -.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } -/* light red */ -.low, .cline-no { background:#FCE1E5 } -/* light green */ -.high, .cline-yes { background:rgb(230,245,208) } -/* medium green */ -.cstat-yes { background:rgb(161,215,106) } -/* dark green */ -.status-line.high, .high .cover-fill { background:rgb(77,146,33) } -.high .chart { border:1px solid rgb(77,146,33) } -/* dark yellow (gold) */ -.status-line.medium, .medium .cover-fill { background: #f9cd0b; } -.medium .chart { border:1px solid #f9cd0b; } -/* light yellow */ -.medium { background: #fff4c2; } - -.cstat-skip { background: #ddd; color: #111; } -.fstat-skip { background: #ddd; color: #111 !important; } -.cbranch-skip { background: #ddd !important; color: #111; } - -span.cline-neutral { background: #eaeaea; } - -.coverage-summary td.empty { - opacity: .5; - padding-top: 4px; - padding-bottom: 4px; - line-height: 1; - color: #888; -} - -.cover-fill, .cover-empty { - display:inline-block; - height: 12px; -} -.chart { - line-height: 0; -} -.cover-empty { - background: white; -} -.cover-full { - border-right: none !important; -} -pre.prettyprint { - border: none !important; - padding: 0 !important; - margin: 0 !important; -} -.com { color: #999 !important; } -.ignore-none { color: #999; font-weight: normal; } - -.wrapper { - min-height: 100%; - height: auto !important; - height: 100%; - margin: 0 auto -48px; -} -.footer, .push { - height: 48px; -} diff --git a/webclient-vue/coverage/block-navigation.js b/webclient-vue/coverage/block-navigation.js deleted file mode 100644 index 530d1ed..0000000 --- a/webclient-vue/coverage/block-navigation.js +++ /dev/null @@ -1,87 +0,0 @@ -/* eslint-disable */ -var jumpToCode = (function init() { - // Classes of code we would like to highlight in the file view - var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; - - // Elements to highlight in the file listing view - var fileListingElements = ['td.pct.low']; - - // We don't want to select elements that are direct descendants of another match - var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` - - // Selector that finds elements on the page to which we can jump - var selector = - fileListingElements.join(', ') + - ', ' + - notSelector + - missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` - - // The NodeList of matching elements - var missingCoverageElements = document.querySelectorAll(selector); - - var currentIndex; - - function toggleClass(index) { - missingCoverageElements - .item(currentIndex) - .classList.remove('highlighted'); - missingCoverageElements.item(index).classList.add('highlighted'); - } - - function makeCurrent(index) { - toggleClass(index); - currentIndex = index; - missingCoverageElements.item(index).scrollIntoView({ - behavior: 'smooth', - block: 'center', - inline: 'center' - }); - } - - function goToPrevious() { - var nextIndex = 0; - if (typeof currentIndex !== 'number' || currentIndex === 0) { - nextIndex = missingCoverageElements.length - 1; - } else if (missingCoverageElements.length > 1) { - nextIndex = currentIndex - 1; - } - - makeCurrent(nextIndex); - } - - function goToNext() { - var nextIndex = 0; - - if ( - typeof currentIndex === 'number' && - currentIndex < missingCoverageElements.length - 1 - ) { - nextIndex = currentIndex + 1; - } - - makeCurrent(nextIndex); - } - - return function jump(event) { - if ( - document.getElementById('fileSearch') === document.activeElement && - document.activeElement != null - ) { - // if we're currently focused on the search input, we don't want to navigate - return; - } - - switch (event.which) { - case 78: // n - case 74: // j - goToNext(); - break; - case 66: // b - case 75: // k - case 80: // p - goToPrevious(); - break; - } - }; -})(); -window.addEventListener('keydown', jumpToCode); diff --git a/webclient-vue/coverage/components/feedback/AppEmptyState.vue.html b/webclient-vue/coverage/components/feedback/AppEmptyState.vue.html deleted file mode 100644 index 1efea36..0000000 --- a/webclient-vue/coverage/components/feedback/AppEmptyState.vue.html +++ /dev/null @@ -1,139 +0,0 @@ - - - - - - Code coverage report for components/feedback/AppEmptyState.vue - - - - - - - - - -
-
-

All files / components/feedback AppEmptyState.vue

-
- -
- 100% - Statements - 1/1 -
- - -
- 100% - Branches - 0/0 -
- - -
- 100% - Functions - 0/0 -
- - -
- 100% - Lines - 1/1 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19  -9x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<template>
-  <GnEmptyState :title="title" :text="message" icon="ph-package" />
-</template>
- 
-<script setup>
-import { GnEmptyState } from "gnexus-ui-kit/vue";
- 
-defineProps({
-  title: {
-    type: String,
-    default: "Nothing here",
-  },
-  message: {
-    type: String,
-    default: "",
-  },
-});
-</script>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/components/feedback/AppErrorState.vue.html b/webclient-vue/coverage/components/feedback/AppErrorState.vue.html deleted file mode 100644 index 32bc2fd..0000000 --- a/webclient-vue/coverage/components/feedback/AppErrorState.vue.html +++ /dev/null @@ -1,163 +0,0 @@ - - - - - - Code coverage report for components/feedback/AppErrorState.vue - - - - - - - - - -
-
-

All files / components/feedback AppErrorState.vue

-
- -
- 100% - Statements - 3/3 -
- - -
- 100% - Branches - 4/4 -
- - -
- 100% - Functions - 2/2 -
- - -
- 100% - Lines - 3/3 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27  -4x -4x -  -2x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<template>
-  <GnAlert variant="danger" role="alert">
-    <strong>{{ title }}</strong>
-    <p v-if="message">{{ message }}</p>
-    <GnButton v-if="retry" variant="danger" @click="retry">Retry</GnButton>
-  </GnAlert>
-</template>
- 
-<script setup>
-import { GnAlert, GnButton } from "gnexus-ui-kit/vue";
- 
-defineProps({
-  title: {
-    type: String,
-    default: "Request failed",
-  },
-  message: {
-    type: String,
-    default: "",
-  },
-  retry: {
-    type: Function,
-    default: null,
-  },
-});
-</script>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/components/feedback/AppLoadingState.vue.html b/webclient-vue/coverage/components/feedback/AppLoadingState.vue.html deleted file mode 100644 index dafb546..0000000 --- a/webclient-vue/coverage/components/feedback/AppLoadingState.vue.html +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - Code coverage report for components/feedback/AppLoadingState.vue - - - - - - - - - -
-
-

All files / components/feedback AppLoadingState.vue

-
- -
- 100% - Statements - 1/1 -
- - -
- 100% - Branches - 0/0 -
- - -
- 100% - Functions - 0/0 -
- - -
- 100% - Lines - 1/1 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15  -8x -  -  -  -  -  -  -  -  -  -  -  -  - 
<template>
-  <GnLoader circle :label="text" />
-</template>
- 
-<script setup>
-import { GnLoader } from "gnexus-ui-kit/vue";
- 
-defineProps({
-  text: {
-    type: String,
-    default: "Loading",
-  },
-});
-</script>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/components/feedback/index.html b/webclient-vue/coverage/components/feedback/index.html deleted file mode 100644 index 83e7fa7..0000000 --- a/webclient-vue/coverage/components/feedback/index.html +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - Code coverage report for components/feedback - - - - - - - - - -
-
-

All files components/feedback

-
- -
- 100% - Statements - 5/5 -
- - -
- 100% - Branches - 4/4 -
- - -
- 100% - Functions - 2/2 -
- - -
- 100% - Lines - 5/5 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
AppEmptyState.vue -
-
100%1/1100%0/0100%0/0100%1/1
AppErrorState.vue -
-
100%3/3100%4/4100%2/2100%3/3
AppLoadingState.vue -
-
100%1/1100%0/0100%0/0100%1/1
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/components/layout/AppShell.vue.html b/webclient-vue/coverage/components/layout/AppShell.vue.html deleted file mode 100644 index 72e1e86..0000000 --- a/webclient-vue/coverage/components/layout/AppShell.vue.html +++ /dev/null @@ -1,169 +0,0 @@ - - - - - - Code coverage report for components/layout/AppShell.vue - - - - - - - - - -
-
-

All files / components/layout AppShell.vue

-
- -
- 100% - Statements - 3/3 -
- - -
- 100% - Branches - 0/0 -
- - -
- 100% - Functions - 1/1 -
- - -
- 100% - Lines - 3/3 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29  -3x -  -  -  -  -  -  -  -  -3x -  -  -  -  -  -  -  -3x -  -  -  -  -  -  -  -  -  - 
<template>
-  <GnNavigationShell
-    brand="SHSERV WEB CLIENT"
-    logo-src=""
-    title="Navigation"
-    subtitle="Smart Home"
-    :items="navItems"
-    active-match="prefix"
-  >
-    <template #content>
-      <slot />
-    </template>
-  </GnNavigationShell>
-</template>
- 
-<script setup>
-import { GnNavigationShell } from "gnexus-ui-kit/vue";
- 
-const navItems = [
-  { label: "Favorites", to: "/areas/favorites", icon: "ph-star" },
-  { label: "Areas", to: "/areas/tree", icon: "ph-map-trifold" },
-  { label: "Devices", to: "/devices", icon: "ph-cpu" },
-  { label: "Scanning", to: "/devices/scanning", icon: "ph-magnifying-glass" },
-  { label: "Actions", to: "/scripts/actions", icon: "ph-play" },
-  { label: "Regular", to: "/scripts/regular", icon: "ph-clock" },
-  { label: "Scopes", to: "/scripts/scopes", icon: "ph-brackets-curly" },
-];
-</script>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/components/layout/index.html b/webclient-vue/coverage/components/layout/index.html deleted file mode 100644 index d229c9c..0000000 --- a/webclient-vue/coverage/components/layout/index.html +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - Code coverage report for components/layout - - - - - - - - - -
-
-

All files components/layout

-
- -
- 100% - Statements - 3/3 -
- - -
- 100% - Branches - 0/0 -
- - -
- 100% - Functions - 1/1 -
- - -
- 100% - Lines - 3/3 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
AppShell.vue -
-
100%3/3100%0/0100%1/1100%3/3
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/favicon.png b/webclient-vue/coverage/favicon.png deleted file mode 100644 index c1525b8..0000000 --- a/webclient-vue/coverage/favicon.png +++ /dev/null Binary files differ diff --git a/webclient-vue/coverage/features/areas/components/AreaTreeNode.vue.html b/webclient-vue/coverage/features/areas/components/AreaTreeNode.vue.html deleted file mode 100644 index 3bc771d..0000000 --- a/webclient-vue/coverage/features/areas/components/AreaTreeNode.vue.html +++ /dev/null @@ -1,379 +0,0 @@ - - - - - - Code coverage report for features/areas/components/AreaTreeNode.vue - - - - - - - - - -
-
-

All files / features/areas/components AreaTreeNode.vue

-
- -
- 90.32% - Statements - 28/31 -
- - -
- 100% - Branches - 39/39 -
- - -
- 83.33% - Functions - 15/18 -
- - -
- 88.88% - Lines - 24/27 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99  -26x -2x -  -  -  -  -  -1x -  -  -  -  -2x -  -  -23x -  -  -  -  -2x -  -  -  -1x -  -  -18x -  -  -  -  -1x -  -  -18x -  -  -  -1x -  -  -18x -  -  -  -1x -  -19x -  -  -  -  -  -1x -  -  -  -  -  -  -  -  -  -  -  -2x -  -  -  -  -  -  -  -  -  -  -19x -  -  -  -  -  -  -  -  -  -  -19x -  -19x -19x -23x -24x -24x -19x -  - 
<template>
-  <li v-if="!isCycle" class="area-tree-node" :class="{ 'is-open': isOpen, 'is-leaf': isLeaf }">
-    <article class="area-tree-card">
-      <button
-        class="tree-toggle"
-        type="button"
-        :disabled="isLeaf"
-        :aria-expanded="isOpen"
-        @click="isOpen = !isOpen"
-      >
-        {{ isLeaf ? "·" : isOpen ? "−" : "+" }}
-      </button>
- 
-      <div class="area-tree-info">
-        <h2>{{ area.display_name }}</h2>
-        <p>
-          <GnBadge variant="secondary">{{ area.type }}</GnBadge>
-          <code>{{ area.alias }}</code>
-        </p>
-      </div>
- 
-      <div class="area-tree-actions">
-        <GnButton
-          variant="secondary"
-          icon="ph-pencil"
-          @click="emit('rename', area)"
-        >
-          Rename
-        </GnButton>
-        <GnButton
-          variant="secondary"
-          icon="ph-arrow-up"
-          :disabled="area.parent_id == null || area.parent_id === 0"
-          @click="emit('unassign', area)"
-        >
-          Unassign
-        </GnButton>
-        <GnButton
-          variant="danger"
-          icon="ph-trash"
-          @click="emit('remove', area)"
-        >
-          Remove
-        </GnButton>
-        <GnButton
-          :variant="isFavorite ? 'warning' : 'secondary'"
-          :icon="isFavorite ? 'ph-star' : 'ph-star'"
-          @click="favoritesStore.toggle(area.id)"
-        >
-          {{ isFavorite ? "Unstar" : "Star" }}
-        </GnButton>
-      </div>
-    </article>
- 
-    <ul v-if="area.children?.length && isOpen" class="area-tree-children">
-      <AreaTreeNode
-        v-for="child in area.children"
-        :key="child.id"
-        :area="child"
-        :ancestors="nextAncestors"
-        @rename="(a) => emit('rename', a)"
-        @remove="(a) => emit('remove', a)"
-        @unassign="(a) => emit('unassign', a)"
-      />
-    </ul>
-  </li>
-  <li v-else class="area-tree-node">
-    <article class="area-tree-card area-tree-cycle">
-      Cycle skipped for area ID {{ area.id }}
-    </article>
-  </li>
-</template>
- 
-<script setup>
-import { computed, ref } from "vue";
-import { useFavoritesStore } from "../../../stores/favorites";
-import { GnBadge, GnButton } from "gnexus-ui-kit/vue";
- 
-const props = defineProps({
-  area: {
-    type: Object,
-    required: true,
-  },
-  ancestors: {
-    type: Array,
-    default: () => [],
-  },
-});
- 
-const emit = defineEmits(["rename", "remove", "unassign"]);
- 
-const favoritesStore = useFavoritesStore();
-const isOpen = ref(false);
-const isLeaf = computed(() => !props.area.children?.length);
-const isFavorite = computed(() => favoritesStore.has(props.area.id));
-const isCycle = computed(() => props.ancestors.includes(String(props.area.id)));
-const nextAncestors = computed(() => [...props.ancestors, String(props.area.id)]);
-</script>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/features/areas/components/index.html b/webclient-vue/coverage/features/areas/components/index.html deleted file mode 100644 index 5c8edf2..0000000 --- a/webclient-vue/coverage/features/areas/components/index.html +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - Code coverage report for features/areas/components - - - - - - - - - -
-
-

All files features/areas/components

-
- -
- 90.32% - Statements - 28/31 -
- - -
- 100% - Branches - 39/39 -
- - -
- 83.33% - Functions - 15/18 -
- - -
- 88.88% - Lines - 24/27 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
AreaTreeNode.vue -
-
90.32%28/31100%39/3983.33%15/1888.88%24/27
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/features/areas/pages/AreaTreePage.vue.html b/webclient-vue/coverage/features/areas/pages/AreaTreePage.vue.html deleted file mode 100644 index 7909b01..0000000 --- a/webclient-vue/coverage/features/areas/pages/AreaTreePage.vue.html +++ /dev/null @@ -1,568 +0,0 @@ - - - - - - Code coverage report for features/areas/pages/AreaTreePage.vue - - - - - - - - - -
-
-

All files / features/areas/pages AreaTreePage.vue

-
- -
- 29.85% - Statements - 20/67 -
- - -
- 25% - Branches - 11/44 -
- - -
- 14.28% - Functions - 4/28 -
- - -
- 29.5% - Lines - 18/61 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162  -15x -  -  -4x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -14x -  -  -  -  -  -  -  -  -  -  -1x -  -  -1x -  -  -1x -  -  -  -  -  -  -  -  -  -  -  -  -1x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -4x -  -4x -4x -4x -4x -  -4x -4x -4x -4x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -4x -4x -  -  -  -  -  -  -  -  - 
<template>
-  <section class="page">
-    <GnPageHeader title="Tree" kicker="Areas">
-      <template #actions>
-        <GnButton variant="primary" icon="ph-plus" @click="openCreate">Create area</GnButton>
-      </template>
-    </GnPageHeader>
- 
-    <AppLoadingState v-if="areasStore.isLoading" text="Loading areas tree" />
- 
-    <AppErrorState
-      v-else-if="areasStore.error"
-      title="Areas loading failed"
-      :message="areasStore.error.message"
-      :retry="areasStore.loadAreas"
-    />
- 
-    <AppEmptyState
-      v-else-if="areasStore.areaTree.length === 0"
-      title="No areas"
-      message="No areas found. Create one to get started."
-    />
- 
-    <ul v-else class="area-tree">
-      <AreaTreeNode
-        v-for="area in areasStore.areaTree"
-        :key="area.id"
-        :area="area"
-        @rename="openRename"
-        @remove="remove"
-        @unassign="unassign"
-      />
-    </ul>
- 
-    <GnModal :open="showCreateModal" title="Create area" @update:open="showCreateModal = $event">
-      <div class="form-group">
-        <GnInput v-model="createForm.type" label="Type" placeholder="room" />
-      </div>
-      <div class="form-group">
-        <GnInput v-model="createForm.alias" label="Alias" placeholder="kitchen" />
-      </div>
-      <div class="form-group">
-        <GnInput v-model="createForm.display_name" label="Display name" placeholder="Kitchen" />
-      </div>
-      <div v-if="createError" class="form-group">
-        <GnAlert variant="danger">{{ createError }}</GnAlert>
-      </div>
-      <template #footer>
-        <GnButton variant="secondary" @click="showCreateModal = false">Cancel</GnButton>
-        <GnButton variant="primary" icon="ph-plus" :loading="createLoading" @click="submitCreate">Create</GnButton>
-      </template>
-    </GnModal>
- 
-    <GnModal :open="showRenameModal" title="Rename area" @update:open="showRenameModal = $event">
-      <div class="form-group">
-        <GnInput v-model="renameForm.display_name" label="Display name" />
-      </div>
-      <div v-if="renameError" class="form-group">
-        <GnAlert variant="danger">{{ renameError }}</GnAlert>
-      </div>
-      <template #footer>
-        <GnButton variant="secondary" @click="showRenameModal = false">Cancel</GnButton>
-        <GnButton variant="primary" icon="ph-check" :loading="renameLoading" @click="submitRename">Rename</GnButton>
-      </template>
-    </GnModal>
-  </section>
-</template>
- 
-<script setup>
-import { ref, reactive, onMounted } from "vue";
-import { useAreasStore } from "../../../stores/areas";
-import {
-  GnPageHeader,
-  GnButton,
-  GnModal,
-  GnInput,
-  GnAlert,
-} from "gnexus-ui-kit/vue";
-import AreaTreeNode from "../components/AreaTreeNode.vue";
-import AppLoadingState from "../../../components/feedback/AppLoadingState.vue";
-import AppErrorState from "../../../components/feedback/AppErrorState.vue";
-import AppEmptyState from "../../../components/feedback/AppEmptyState.vue";
- 
-const areasStore = useAreasStore();
- 
-const showCreateModal = ref(false);
-const createLoading = ref(false);
-const createError = ref("");
-const createForm = reactive({ type: "", alias: "", display_name: "" });
- 
-const showRenameModal = ref(false);
-const renameLoading = ref(false);
-const renameError = ref("");
-const renameForm = reactive({ areaId: null, display_name: "" });
- 
-function openCreate() {
-  createForm.type = "";
-  createForm.alias = "";
-  createForm.display_name = "";
-  createError.value = "";
-  showCreateModal.value = true;
-}
- 
-async function submitCreate() {
-  createLoading.value = true;
-  createError.value = "";
- 
-  const result = await areasStore.createArea({ ...createForm });
-  createLoading.value = false;
- 
-  if (!result.ok) {
-    createError.value = result.error?.message || "Failed to create area";
-    return;
-  }
- 
-  showCreateModal.value = false;
-}
- 
-function openRename(area) {
-  renameForm.areaId = area.id;
-  renameForm.display_name = area.display_name;
-  renameError.value = "";
-  showRenameModal.value = true;
-}
- 
-async function submitRename() {
-  renameLoading.value = true;
-  renameError.value = "";
- 
-  const result = await areasStore.renameArea(renameForm.areaId, renameForm.display_name);
-  renameLoading.value = false;
- 
-  if (!result.ok) {
-    renameError.value = result.error?.message || "Failed to rename area";
-    return;
-  }
- 
-  showRenameModal.value = false;
-}
- 
-async function remove(area) {
-  if (!window.confirm(`Remove area "${area.display_name}"?`)) {
-    return;
-  }
-  await areasStore.removeArea(area.id);
-}
- 
-async function unassign(area) {
-  await areasStore.unassignArea(area.id);
-}
- 
-onMounted(() => {
-  areasStore.loadAreas();
-});
-</script>
- 
-<style scoped>
-.form-group {
-  margin-bottom: 16px;
-}
-</style>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/features/areas/pages/index.html b/webclient-vue/coverage/features/areas/pages/index.html deleted file mode 100644 index ca26112..0000000 --- a/webclient-vue/coverage/features/areas/pages/index.html +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - Code coverage report for features/areas/pages - - - - - - - - - -
-
-

All files features/areas/pages

-
- -
- 29.85% - Statements - 20/67 -
- - -
- 25% - Branches - 11/44 -
- - -
- 14.28% - Functions - 4/28 -
- - -
- 29.5% - Lines - 18/61 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
AreaTreePage.vue -
-
29.85%20/6725%11/4414.28%4/2829.5%18/61
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/features/devices/components/DeviceConnectionBadge.vue.html b/webclient-vue/coverage/features/devices/components/DeviceConnectionBadge.vue.html deleted file mode 100644 index 4ac343b..0000000 --- a/webclient-vue/coverage/features/devices/components/DeviceConnectionBadge.vue.html +++ /dev/null @@ -1,169 +0,0 @@ - - - - - - Code coverage report for features/devices/components/DeviceConnectionBadge.vue - - - - - - - - - -
-
-

All files / features/devices/components DeviceConnectionBadge.vue

-
- -
- 100% - Statements - 11/11 -
- - -
- 100% - Branches - 6/6 -
- - -
- 100% - Functions - 3/3 -
- - -
- 100% - Lines - 9/9 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29  -4x -  -  -  -  -  -  -4x -  -  -  -  -  -  -4x -4x -4x -1x -  -  -3x -1x -  -  -2x -  -  - 
<template>
-  <GnBadge :variant="variant">{{ label }}</GnBadge>
-</template>
- 
-<script setup>
-import { computed } from "vue";
-import { GnBadge } from "gnexus-ui-kit/vue";
- 
-const props = defineProps({
-  status: {
-    type: String,
-    default: "unknown",
-  },
-});
- 
-const label = computed(() => props.status || "unknown");
-const variant = computed(() => {
-  if (props.status === "active") {
-    return "success";
-  }
- 
-  if (props.status === "lost") {
-    return "danger";
-  }
- 
-  return "secondary";
-});
-</script>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/features/devices/components/DeviceStateCell.vue.html b/webclient-vue/coverage/features/devices/components/DeviceStateCell.vue.html deleted file mode 100644 index e87bcfc..0000000 --- a/webclient-vue/coverage/features/devices/components/DeviceStateCell.vue.html +++ /dev/null @@ -1,229 +0,0 @@ - - - - - - Code coverage report for features/devices/components/DeviceStateCell.vue - - - - - - - - - -
-
-

All files / features/devices/components DeviceStateCell.vue

-
- -
- 100% - Statements - 16/16 -
- - -
- 85.71% - Branches - 18/21 -
- - -
- 100% - Functions - 3/3 -
- - -
- 100% - Lines - 16/16 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49  -5x -4x -  -  -  -  -  -  -5x -  -  -  -  -  -  -  -  -  -5x -5x -1x -  -  -4x -1x -  -  -3x -1x -  -  -2x -  -  -5x -4x -  -1x -  -1x -  -  -  -2x -  -  -  - 
<template>
-  <GnLoader v-if="state.status === 'loading'" circle :label="label" />
-  <GnBadge v-else :variant="variant">{{ label }}</GnBadge>
-</template>
- 
-<script setup>
-import { computed } from "vue";
-import { GnLoader, GnBadge } from "gnexus-ui-kit/vue";
- 
-const props = defineProps({
-  state: {
-    type: Object,
-    default: () => ({
-      status: "idle",
-      message: "Not loaded",
-    }),
-  },
-});
- 
-const label = computed(() => {
-  if (props.state.status === "ready") {
-    return props.state.message || "ok";
-  }
- 
-  if (props.state.status === "error") {
-    return props.state.message || "Loading error";
-  }
- 
-  if (props.state.status === "skipped") {
-    return props.state.message || "Skipped";
-  }
- 
-  return props.state.message || "Not loaded";
-});
- 
-const variant = computed(() => {
-  switch (props.state.status) {
-    case "ready":
-      return "success";
-    case "error":
-      return "danger";
-    case "skipped":
-    case "idle":
-    default:
-      return "secondary";
-  }
-});
-</script>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/features/devices/components/index.html b/webclient-vue/coverage/features/devices/components/index.html deleted file mode 100644 index d7986ff..0000000 --- a/webclient-vue/coverage/features/devices/components/index.html +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - Code coverage report for features/devices/components - - - - - - - - - -
-
-

All files features/devices/components

-
- -
- 100% - Statements - 27/27 -
- - -
- 88.88% - Branches - 24/27 -
- - -
- 100% - Functions - 6/6 -
- - -
- 100% - Lines - 25/25 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
DeviceConnectionBadge.vue -
-
100%11/11100%6/6100%3/3100%9/9
DeviceStateCell.vue -
-
100%16/1685.71%18/21100%3/3100%16/16
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/features/devices/pages/DevicesScanningPage.vue.html b/webclient-vue/coverage/features/devices/pages/DevicesScanningPage.vue.html deleted file mode 100644 index e08ea94..0000000 --- a/webclient-vue/coverage/features/devices/pages/DevicesScanningPage.vue.html +++ /dev/null @@ -1,655 +0,0 @@ - - - - - - Code coverage report for features/devices/pages/DevicesScanningPage.vue - - - - - - - - - -
-
-

All files / features/devices/pages DevicesScanningPage.vue

-
- -
- 48.21% - Statements - 27/56 -
- - -
- 53.44% - Branches - 31/58 -
- - -
- 43.33% - Functions - 13/30 -
- - -
- 48.07% - Lines - 25/52 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191  -3x -  -  -1x -  -  -  -  -  -3x -  -  -  -  -  -3x -  -  -  -  -  -  -3x -3x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -1x -1x -1x -  -  -  -  -  -  -  -1x -  -  -  -  -1x -1x -  -  -1x -  -  -  -  -  -  -  -1x -  -  -  -  -  -  -  -  -  -1x -  -  -1x -  -  -1x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -1x -  -1x -1x -1x -1x -  -  -  -  -  -  -1x -  -  -  -  -  -  -  -  -1x -1x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<template>
-  <section class="page">
-    <GnPageHeader title="Scanning" kicker="Devices">
-      <template #actions>
-        <div class="devices-actions">
-          <GnButton
-            :variant="scanningStore.mode === 'setup' ? 'primary' : 'secondary'"
-            @click="setMode('setup')"
-          >
-            Setup
-          </GnButton>
-          <GnButton
-            :variant="scanningStore.mode === 'all' ? 'primary' : 'secondary'"
-            @click="setMode('all')"
-          >
-            All
-          </GnButton>
-          <GnButton
-            :loading="scanningStore.isLoading"
-            icon="ph-magnifying-glass"
-            @click="scan"
-          >
-            Scan
-          </GnButton>
-        </div>
-      </template>
-    </GnPageHeader>
- 
-    <AppLoadingState v-if="scanningStore.isLoading" text="Scanning network" />
- 
-    <AppErrorState
-      v-else-if="scanningStore.error"
-      title="Scan failed"
-      :message="scanningStore.error.message"
-      :retry="scan"
-    />
- 
-    <AppEmptyState
-      v-else-if="scanningStore.devices.length === 0"
-      title="No devices found"
-      message="Choose scan mode and click Scan to discover devices."
-    />
- 
-    <div v-else class="devices-panel">
-      <div class="devices-summary">
-        <GnBadge variant="primary">{{ scanningStore.total }} found</GnBadge>
-        <GnBadge variant="secondary">Mode: {{ scanningStore.mode }}</GnBadge>
-      </div>
- 
-      <GnTable
-        :columns="tableColumns"
-        :rows="tableRows"
-        caption="Discovered devices"
-      >
-        <template #cell-device_name="{ row }">
-          <strong>{{ row.device_name }}</strong>
-          <small>{{ row.device_type }}</small>
-        </template>
- 
-        <template #cell-status="{ row }">
-          <GnBadge :variant="row.status === 'setup' ? 'warning' : 'success'">{{ row.status }}</GnBadge>
-        </template>
- 
-        <template #cell-actions="{ row }">
-          <GnButton
-            v-if="row.status === 'setup'"
-            variant="primary"
-            icon="ph-plus"
-            @click="openSetup(row)"
-          >
-            Add
-          </GnButton>
-        </template>
-      </GnTable>
-    </div>
- 
-    <GnModal
-      :open="showSetupModal"
-      title="Setup new device"
-      @update:open="showSetupModal = $event"
-    >
-      <div class="form-group">
-        <GnInput v-model="setupForm.alias" label="Alias" placeholder="kitchen_relay" />
-      </div>
-      <div class="form-group">
-        <GnInput v-model="setupForm.name" label="Name" placeholder="Kitchen Relay" />
-      </div>
-      <div class="form-group">
-        <GnInput v-model="setupForm.description" label="Description" />
-      </div>
-      <div v-if="setupError" class="form-group">
-        <GnAlert variant="danger">{{ setupError }}</GnAlert>
-      </div>
-      <template #footer>
-        <GnButton variant="secondary" @click="showSetupModal = false">Cancel</GnButton>
-        <GnButton variant="primary" icon="ph-plus" :loading="setupLoading" @click="submitSetup">
-          Add device
-        </GnButton>
-      </template>
-    </GnModal>
-  </section>
-</template>
- 
-<script setup>
-import { ref, reactive, computed } from "vue";
-import { useScanningStore } from "../../../stores/scanning";
-import {
-  GnPageHeader,
-  GnButton,
-  GnBadge,
-  GnTable,
-  GnModal,
-  GnInput,
-  GnAlert,
-} from "gnexus-ui-kit/vue";
-import AppEmptyState from "../../../components/feedback/AppEmptyState.vue";
-import AppErrorState from "../../../components/feedback/AppErrorState.vue";
-import AppLoadingState from "../../../components/feedback/AppLoadingState.vue";
- 
-const scanningStore = useScanningStore();
- 
-const showSetupModal = ref(false);
-const setupLoading = ref(false);
-const setupError = ref("");
-const setupForm = reactive({
-  device_ip: "",
-  alias: "",
-  name: "",
-  description: "",
-});
- 
-const tableColumns = [
-  { key: "device_name", label: "Device" },
-  { key: "ip_address", label: "IP" },
-  { key: "mac_address", label: "MAC" },
-  { key: "firmware_version", label: "Firmware" },
-  { key: "status", label: "Status" },
-  { key: "actions", label: "Actions" },
-];
- 
-const tableRows = computed(() =>
-  scanningStore.devices.map((device) => ({
-    id: device.device_id || device.ip_address,
-    device_name: device.device_name || "Unknown",
-    device_type: device.device_type || "unknown",
-    ip_address: device.ip_address || "unknown",
-    mac_address: device.mac_address || "unknown",
-    firmware_version: device.firmware_version || "unknown",
-    status: device.status || "unknown",
-  }))
-);
- 
-function setMode(mode) {
-  scanningStore.setMode(mode);
-}
- 
-function scan() {
-  scanningStore.scan();
-}
- 
-function openSetup(row) {
-  setupForm.device_ip = row.ip_address;
-  setupForm.alias = "";
-  setupForm.name = "";
-  setupForm.description = "";
-  setupError.value = "";
-  showSetupModal.value = true;
-}
- 
-async function submitSetup() {
-  setupLoading.value = true;
-  setupError.value = "";
- 
-  const result = await scanningStore.setupDevice({ ...setupForm });
-  setupLoading.value = false;
- 
-  if (!result.ok) {
-    setupError.value = result.error?.message || "Failed to setup device";
-    return;
-  }
- 
-  showSetupModal.value = false;
-}
-</script>
- 
-<style scoped>
-.form-group {
-  margin-bottom: 16px;
-}
-</style>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/features/devices/pages/index.html b/webclient-vue/coverage/features/devices/pages/index.html deleted file mode 100644 index 6bf0295..0000000 --- a/webclient-vue/coverage/features/devices/pages/index.html +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - Code coverage report for features/devices/pages - - - - - - - - - -
-
-

All files features/devices/pages

-
- -
- 48.21% - Statements - 27/56 -
- - -
- 53.44% - Branches - 31/58 -
- - -
- 43.33% - Functions - 13/30 -
- - -
- 48.07% - Lines - 25/52 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
DevicesScanningPage.vue -
-
48.21%27/5653.44%31/5843.33%13/3048.07%25/52
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/features/scripts/pages/ScriptsActionsPage.vue.html b/webclient-vue/coverage/features/scripts/pages/ScriptsActionsPage.vue.html deleted file mode 100644 index 463020a..0000000 --- a/webclient-vue/coverage/features/scripts/pages/ScriptsActionsPage.vue.html +++ /dev/null @@ -1,436 +0,0 @@ - - - - - - Code coverage report for features/scripts/pages/ScriptsActionsPage.vue - - - - - - - - - -
-
-

All files / features/scripts/pages ScriptsActionsPage.vue

-
- -
- 56% - Statements - 14/25 -
- - -
- 38.09% - Branches - 8/21 -
- - -
- 66.66% - Functions - 8/12 -
- - -
- 52.38% - Lines - 11/21 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118  -3x -  -  -2x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -1x -  -  -  -  -  -1x -  -  -  -1x -  -  -  -  -  -  -  -  -  -  -  -  -  -1x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -1x -1x -  -1x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -1x -1x -  -  -  -  -  -  -  -  -  -  -  -  -  - 
<template>
-  <section class="page">
-    <GnPageHeader title="Actions" kicker="Scripts">
-      <template #actions>
-        <GnBadge variant="primary">{{ scriptsStore.totalActions }} scripts</GnBadge>
-      </template>
-    </GnPageHeader>
- 
-    <AppLoadingState v-if="scriptsStore.isLoadingActions" text="Loading actions" />
- 
-    <AppErrorState
-      v-else-if="scriptsStore.errorActions"
-      title="Actions loading failed"
-      :message="scriptsStore.errorActions.message"
-      :retry="scriptsStore.loadActions"
-    />
- 
-    <AppEmptyState
-      v-else-if="scriptsStore.actions.length === 0"
-      title="No action scripts"
-      message="No action scripts registered."
-    />
- 
-    <div v-else class="area-grid">
-      <GnCard
-        v-for="script in scriptsStore.actions"
-        :key="script.alias"
-        :title="script.name"
-      >
-        <template #default>
-          <div v-html="script.icon" class="script-icon" />
-          <p v-if="script.description">{{ script.description }}</p>
-          <p>
-            <GnBadge :variant="script.state === 'enabled' ? 'success' : 'secondary'"
-            >{{ script.state }}</GnBadge>
-            <code>{{ script.alias }}</code>
-          </p>
-          <small>{{ script.author }}</small>
-        </template>
-        <template #footer>
-          <GnButton
-            variant="primary"
-            icon="ph-play"
-            :loading="scriptsStore.isRunning(script.alias)"
-            :disabled="script.state !== 'enabled'"
-            @click="run(script.alias)"
-          >
-            Run
-          </GnButton>
-        </template>
-      </GnCard>
-    </div>
- 
-    <GnAlert v-if="resultAlert" :variant="resultAlert.variant" class="result-alert">
-      <strong>{{ resultAlert.title }}</strong>
-      <p v-if="resultAlert.message">{{ resultAlert.message }}</p>
-    </GnAlert>
-  </section>
-</template>
- 
-<script setup>
-import { ref, onMounted, computed } from "vue";
-import { useScriptsStore } from "../../../stores/scripts";
-import {
-  GnPageHeader,
-  GnBadge,
-  GnCard,
-  GnButton,
-  GnAlert,
-} from "gnexus-ui-kit/vue";
-import AppEmptyState from "../../../components/feedback/AppEmptyState.vue";
-import AppErrorState from "../../../components/feedback/AppErrorState.vue";
-import AppLoadingState from "../../../components/feedback/AppLoadingState.vue";
- 
-const scriptsStore = useScriptsStore();
-const resultAlert = ref(null);
- 
-const resultAlertComputed = computed(() => {
-  const r = scriptsStore.lastRunResult;
-  if (!r) return null;
- 
-  if (r.ok) {
-    return {
-      variant: "success",
-      title: `Ran ${r.alias}`,
-      message: r.execTime ? `Exec time: ${r.execTime}` : undefined,
-    };
-  }
- 
-  return {
-    variant: "danger",
-    title: `Failed ${r.alias}`,
-    message: r.error?.message || "Unknown error",
-  };
-});
- 
-async function run(alias) {
-  resultAlert.value = null;
-  const result = await scriptsStore.runScript(alias);
-  resultAlert.value = resultAlertComputed.value;
-}
- 
-onMounted(() => {
-  scriptsStore.loadActions();
-});
-</script>
- 
-<style scoped>
-.script-icon {
-  font-size: 32px;
-  margin-bottom: 12px;
-}
- 
-.result-alert {
-  margin-top: 24px;
-}
-</style>
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/features/scripts/pages/index.html b/webclient-vue/coverage/features/scripts/pages/index.html deleted file mode 100644 index dd69271..0000000 --- a/webclient-vue/coverage/features/scripts/pages/index.html +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - Code coverage report for features/scripts/pages - - - - - - - - - -
-
-

All files features/scripts/pages

-
- -
- 56% - Statements - 14/25 -
- - -
- 38.09% - Branches - 8/21 -
- - -
- 66.66% - Functions - 8/12 -
- - -
- 52.38% - Lines - 11/21 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
ScriptsActionsPage.vue -
-
56%14/2538.09%8/2166.66%8/1252.38%11/21
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/index.html b/webclient-vue/coverage/index.html deleted file mode 100644 index f039897..0000000 --- a/webclient-vue/coverage/index.html +++ /dev/null @@ -1,266 +0,0 @@ - - - - - - Code coverage report for All files - - - - - - - - - -
-
-

All files

-
- -
- 79.96% - Statements - 459/574 -
- - -
- 68% - Branches - 272/400 -
- - -
- 70.68% - Functions - 135/191 -
- - -
- 80.81% - Lines - 438/542 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
api -
-
94.82%55/5888.13%52/5981.81%9/1198.21%55/56
api/modules -
-
96.77%30/3177.77%7/9100%25/2596.77%30/31
components/feedback -
-
100%5/5100%4/4100%2/2100%5/5
components/layout -
-
100%3/3100%0/0100%1/1100%3/3
features/areas/components -
-
90.32%28/31100%39/3983.33%15/1888.88%24/27
features/areas/pages -
-
29.85%20/6725%11/4414.28%4/2829.5%18/61
features/devices/components -
-
100%27/2788.88%24/27100%6/6100%25/25
features/devices/pages -
-
48.21%27/5653.44%31/5843.33%13/3048.07%25/52
features/scripts/pages -
-
56%14/2538.09%8/2166.66%8/1252.38%11/21
stores -
-
93.03%227/24466.38%79/11990.9%50/5593.19%219/235
test/mocks -
-
85.18%23/2785%17/2066.66%2/388.46%23/26
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/prettify.css b/webclient-vue/coverage/prettify.css deleted file mode 100644 index b317a7c..0000000 --- a/webclient-vue/coverage/prettify.css +++ /dev/null @@ -1 +0,0 @@ -.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/webclient-vue/coverage/prettify.js b/webclient-vue/coverage/prettify.js deleted file mode 100644 index b322523..0000000 --- a/webclient-vue/coverage/prettify.js +++ /dev/null @@ -1,2 +0,0 @@ -/* eslint-disable */ -window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/webclient-vue/coverage/sort-arrow-sprite.png b/webclient-vue/coverage/sort-arrow-sprite.png deleted file mode 100644 index 6ed6831..0000000 --- a/webclient-vue/coverage/sort-arrow-sprite.png +++ /dev/null Binary files differ diff --git a/webclient-vue/coverage/sorter.js b/webclient-vue/coverage/sorter.js deleted file mode 100644 index 4ed70ae..0000000 --- a/webclient-vue/coverage/sorter.js +++ /dev/null @@ -1,210 +0,0 @@ -/* eslint-disable */ -var addSorting = (function() { - 'use strict'; - var cols, - currentSort = { - index: 0, - desc: false - }; - - // returns the summary table element - function getTable() { - return document.querySelector('.coverage-summary'); - } - // returns the thead element of the summary table - function getTableHeader() { - return getTable().querySelector('thead tr'); - } - // returns the tbody element of the summary table - function getTableBody() { - return getTable().querySelector('tbody'); - } - // returns the th element for nth column - function getNthColumn(n) { - return getTableHeader().querySelectorAll('th')[n]; - } - - function onFilterInput() { - const searchValue = document.getElementById('fileSearch').value; - const rows = document.getElementsByTagName('tbody')[0].children; - - // Try to create a RegExp from the searchValue. If it fails (invalid regex), - // it will be treated as a plain text search - let searchRegex; - try { - searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive - } catch (error) { - searchRegex = null; - } - - for (let i = 0; i < rows.length; i++) { - const row = rows[i]; - let isMatch = false; - - if (searchRegex) { - // If a valid regex was created, use it for matching - isMatch = searchRegex.test(row.textContent); - } else { - // Otherwise, fall back to the original plain text search - isMatch = row.textContent - .toLowerCase() - .includes(searchValue.toLowerCase()); - } - - row.style.display = isMatch ? '' : 'none'; - } - } - - // loads the search box - function addSearchBox() { - var template = document.getElementById('filterTemplate'); - var templateClone = template.content.cloneNode(true); - templateClone.getElementById('fileSearch').oninput = onFilterInput; - template.parentElement.appendChild(templateClone); - } - - // loads all columns - function loadColumns() { - var colNodes = getTableHeader().querySelectorAll('th'), - colNode, - cols = [], - col, - i; - - for (i = 0; i < colNodes.length; i += 1) { - colNode = colNodes[i]; - col = { - key: colNode.getAttribute('data-col'), - sortable: !colNode.getAttribute('data-nosort'), - type: colNode.getAttribute('data-type') || 'string' - }; - cols.push(col); - if (col.sortable) { - col.defaultDescSort = col.type === 'number'; - colNode.innerHTML = - colNode.innerHTML + ''; - } - } - return cols; - } - // attaches a data attribute to every tr element with an object - // of data values keyed by column name - function loadRowData(tableRow) { - var tableCols = tableRow.querySelectorAll('td'), - colNode, - col, - data = {}, - i, - val; - for (i = 0; i < tableCols.length; i += 1) { - colNode = tableCols[i]; - col = cols[i]; - val = colNode.getAttribute('data-value'); - if (col.type === 'number') { - val = Number(val); - } - data[col.key] = val; - } - return data; - } - // loads all row data - function loadData() { - var rows = getTableBody().querySelectorAll('tr'), - i; - - for (i = 0; i < rows.length; i += 1) { - rows[i].data = loadRowData(rows[i]); - } - } - // sorts the table using the data for the ith column - function sortByIndex(index, desc) { - var key = cols[index].key, - sorter = function(a, b) { - a = a.data[key]; - b = b.data[key]; - return a < b ? -1 : a > b ? 1 : 0; - }, - finalSorter = sorter, - tableBody = document.querySelector('.coverage-summary tbody'), - rowNodes = tableBody.querySelectorAll('tr'), - rows = [], - i; - - if (desc) { - finalSorter = function(a, b) { - return -1 * sorter(a, b); - }; - } - - for (i = 0; i < rowNodes.length; i += 1) { - rows.push(rowNodes[i]); - tableBody.removeChild(rowNodes[i]); - } - - rows.sort(finalSorter); - - for (i = 0; i < rows.length; i += 1) { - tableBody.appendChild(rows[i]); - } - } - // removes sort indicators for current column being sorted - function removeSortIndicators() { - var col = getNthColumn(currentSort.index), - cls = col.className; - - cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); - col.className = cls; - } - // adds sort indicators for current column being sorted - function addSortIndicators() { - getNthColumn(currentSort.index).className += currentSort.desc - ? ' sorted-desc' - : ' sorted'; - } - // adds event listeners for all sorter widgets - function enableUI() { - var i, - el, - ithSorter = function ithSorter(i) { - var col = cols[i]; - - return function() { - var desc = col.defaultDescSort; - - if (currentSort.index === i) { - desc = !currentSort.desc; - } - sortByIndex(i, desc); - removeSortIndicators(); - currentSort.index = i; - currentSort.desc = desc; - addSortIndicators(); - }; - }; - for (i = 0; i < cols.length; i += 1) { - if (cols[i].sortable) { - // add the click event handler on the th so users - // dont have to click on those tiny arrows - el = getNthColumn(i).querySelector('.sorter').parentElement; - if (el.addEventListener) { - el.addEventListener('click', ithSorter(i)); - } else { - el.attachEvent('onclick', ithSorter(i)); - } - } - } - } - // adds sorting functionality to the UI - return function() { - if (!getTable()) { - return; - } - cols = loadColumns(); - loadData(); - addSearchBox(); - addSortIndicators(); - enableUI(); - }; -})(); - -window.addEventListener('load', addSorting); diff --git a/webclient-vue/coverage/stores/areas.js.html b/webclient-vue/coverage/stores/areas.js.html deleted file mode 100644 index 8d9105b..0000000 --- a/webclient-vue/coverage/stores/areas.js.html +++ /dev/null @@ -1,454 +0,0 @@ - - - - - - Code coverage report for stores/areas.js - - - - - - - - - -
-
-

All files / stores areas.js

-
- -
- 93.22% - Statements - 55/59 -
- - -
- 68.75% - Branches - 22/32 -
- - -
- 84.61% - Functions - 11/13 -
- - -
- 94.54% - Lines - 52/55 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124  -  -  -  -15x -15x -  -15x -22x -  -  -15x -22x -22x -22x -  -22x -2x -  -20x -  -  -  -15x -  -  -  -15x -  -  -2x -14x -  -  -  -  -  -  -  -  -  -  -  -  -15x -  -  -  -  -  -6x -6x -6x -  -6x -6x -  -6x -6x -6x -  -6x -1x -  -  -1x -1x -  -  -5x -6x -  -  -  -2x -  -2x -2x -2x -2x -  -  -  -2x -  -  -  -2x -  -2x -2x -2x -2x -  -  -  -2x -  -  -  -2x -  -2x -4x -  -  -2x -  -  -  -1x -  -1x -1x -1x -1x -  -  -  -1x -  -  -  - 
import { defineStore } from "pinia";
-import { areasApi } from "../api/modules/areas";
- 
-function buildAreaTree(areas) {
-  const map = {};
-  const roots = [];
- 
-  for (const area of areas) {
-    map[area.id] = { ...area, children: [] };
-  }
- 
-  for (const area of areas) {
-    const node = map[area.id];
-    const isSelfReference = area.parent_id && area.parent_id == area.id;
-    const parentExists = area.parent_id && map[area.parent_id];
- 
-    if (!isSelfReference && parentExists) {
-      map[area.parent_id].children.push(node);
-    } else {
-      roots.push(node);
-    }
-  }
- 
-  Iif (roots.length === 0 && areas.length > 0) {
-    return Object.values(map);
-  }
- 
-  return roots;
-}
- 
-export const useAreasStore = defineStore("areas", {
-  state: () => ({
-    areas: [],
-    isLoading: false,
-    error: null,
-    _listAbortController: null,
-  }),
- 
-  getters: {
-    areasById(state) {
-      return Object.fromEntries(state.areas.map((area) => [String(area.id), area]));
-    },
- 
-    areaTree(state) {
-      return buildAreaTree(state.areas);
-    },
-  },
- 
-  actions: {
-    async loadAreas() {
-      this._listAbortController?.abort();
-      const controller = new AbortController();
-      this._listAbortController = controller;
- 
-      this.isLoading = true;
-      this.error = null;
- 
-      const result = await areasApi.list({ signal: controller.signal });
-      this._listAbortController = null;
-      this.isLoading = false;
- 
-      if (!result.ok) {
-        Iif (result.error?.type === "timeout") {
-          return result;
-        }
-        this.error = result.error;
-        return result;
-      }
- 
-      this.areas = result.data?.data?.areas || [];
-      return result;
-    },
- 
-    async createArea(payload) {
-      const result = await areasApi.newArea(payload);
- 
-      Eif (result.ok) {
-        const newArea = result.data?.data?.area;
-        Eif (newArea) {
-          this.areas.push(newArea);
-        }
-      }
- 
-      return result;
-    },
- 
-    async renameArea(areaId, displayName) {
-      const result = await areasApi.updateDisplayName({ area_id: areaId, display_name: displayName });
- 
-      Eif (result.ok) {
-        const idx = this.areas.findIndex((a) => a.id === areaId);
-        Eif (idx !== -1) {
-          this.areas[idx] = { ...this.areas[idx], display_name: displayName };
-        }
-      }
- 
-      return result;
-    },
- 
-    async removeArea(areaId) {
-      const result = await areasApi.remove(areaId);
- 
-      Eif (result.ok) {
-        this.areas = this.areas.filter((a) => a.id !== areaId);
-      }
- 
-      return result;
-    },
- 
-    async unassignArea(areaId) {
-      const result = await areasApi.unassign(areaId);
- 
-      Eif (result.ok) {
-        const idx = this.areas.findIndex((a) => a.id === areaId);
-        Eif (idx !== -1) {
-          this.areas[idx] = { ...this.areas[idx], parent_id: 0 };
-        }
-      }
- 
-      return result;
-    },
-  },
-});
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/stores/devices.js.html b/webclient-vue/coverage/stores/devices.js.html deleted file mode 100644 index 1f33d2a..0000000 --- a/webclient-vue/coverage/stores/devices.js.html +++ /dev/null @@ -1,649 +0,0 @@ - - - - - - Code coverage report for stores/devices.js - - - - - - - - - -
-
-

All files / stores devices.js

-
- -
- 92.64% - Statements - 63/68 -
- - -
- 62.5% - Branches - 25/40 -
- - -
- 93.33% - Functions - 14/15 -
- - -
- 92.53% - Lines - 62/67 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189  -  -  -2x -  -  -13x -  -  -  -6x -  -  -  -  -  -  -  -  -  -  -  -1x -1x -  -1x -  -  -  -  -  -  -  -  -  -1x -1x -  -1x -  -  -  -  -  -  -  -  -  -3x -3x -  -  -3x -2x -2x -2x -  -  -  -3x -  -  -2x -10x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -2x -  -  -  -  -2x -2x -2x -  -2x -2x -  -2x -2x -2x -  -2x -1x -  -  -1x -1x -  -  -1x -2x -2x -  -  -  -5x -  -5x -1x -  -  -4x -  -  -  -  -  -  -3x -3x -3x -3x -3x -  -3x -3x -  -3x -3x -1x -  -  -  -  -  -2x -  -  -  -2x -  -  -  -3x -3x -2x -  -2x -  -  -  -2x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -3x -3x -  -  -  -  -  -1x -1x -  -1x -  -1x -1x -  -  -  - 
import { defineStore } from "pinia";
-import { devicesApi } from "../api/modules/devices";
- 
-const DEFAULT_STATE_CONCURRENCY = 4;
- 
-function getDeviceId(device) {
-  return String(device?.id || "");
-}
- 
-function makeDeviceStatePatch(device, patch) {
-  return {
-    deviceId: getDeviceId(device),
-    status: "idle",
-    message: "",
-    response: null,
-    connectionStatus: device?.connection_status || "unknown",
-    updatedAt: null,
-    ...patch,
-  };
-}
- 
-function normalizeStatusSuccess(device, result) {
-  const payload = result.data?.data?.device || {};
-  const response = payload.device_response || {};
- 
-  return makeDeviceStatePatch(device, {
-    status: "ready",
-    message: response.status || "ok",
-    response,
-    connectionStatus: "active",
-    updatedAt: new Date().toISOString(),
-  });
-}
- 
-function normalizeStatusError(device, result) {
-  const raw = result.error?.raw || {};
-  const connectionStatus = raw?.data?.connection_status || device?.connection_status || "unknown";
- 
-  return makeDeviceStatePatch(device, {
-    status: "error",
-    message: result.error?.message || "Device state is unavailable",
-    response: raw,
-    connectionStatus,
-    updatedAt: new Date().toISOString(),
-  });
-}
- 
-async function runLimited(items, concurrency, worker) {
-  let nextIndex = 0;
-  const poolSize = Math.max(1, Math.min(concurrency, items.length));
- 
-  async function runWorker() {
-    while (nextIndex < items.length) {
-      const item = items[nextIndex];
-      nextIndex += 1;
-      await worker(item);
-    }
-  }
- 
-  await Promise.all(Array.from({ length: poolSize }, runWorker));
-}
- 
-export const useDevicesStore = defineStore("devices", {
-  state: () => ({
-    devices: [],
-    isLoading: false,
-    error: null,
-    isLoadingStates: false,
-    stateError: null,
-    stateByDeviceId: {},
-    stateRunId: 0,
-    rebootingIds: new Set(),
-    _listAbortController: null,
-    lastLoadedAt: null,
-  }),
- 
-  getters: {
-    total(state) {
-      return state.devices.length;
-    },
-    isRebooting: (state) => (id) => state.rebootingIds.has(id),
-  },
- 
-  actions: {
-    async loadDevices() {
-      this._listAbortController?.abort();
-      const controller = new AbortController();
-      this._listAbortController = controller;
- 
-      this.isLoading = true;
-      this.error = null;
- 
-      const result = await devicesApi.list({ signal: controller.signal });
-      this._listAbortController = null;
-      this.isLoading = false;
- 
-      if (!result.ok) {
-        Iif (result.error?.type === "timeout") {
-          return result;
-        }
-        this.error = result.error;
-        return result;
-      }
- 
-      this.devices = result.data?.data?.devices || [];
-      this.lastLoadedAt = new Date().toISOString();
-      return result;
-    },
- 
-    setDeviceState(device, patch) {
-      const deviceId = getDeviceId(device);
- 
-      if (!deviceId) {
-        return;
-      }
- 
-      this.stateByDeviceId = {
-        ...this.stateByDeviceId,
-        [deviceId]: makeDeviceStatePatch(device, patch),
-      };
-    },
- 
-    async loadDeviceStates(options = {}) {
-      const runId = this.stateRunId + 1;
-      this.stateRunId = runId;
-      this.isLoadingStates = true;
-      this.stateError = null;
-      this.stateByDeviceId = {};
- 
-      const devices = this.devices.slice();
-      const targets = [];
- 
-      for (const device of devices) {
-        if (device.connection_status === "lost") {
-          this.setDeviceState(device, {
-            status: "skipped",
-            message: "Connection lost",
-            connectionStatus: "lost",
-          });
-        } else {
-          this.setDeviceState(device, {
-            status: "loading",
-            message: "Loading",
-          });
-          targets.push(device);
-        }
-      }
- 
-      try {
-        await runLimited(targets, options.concurrency || DEFAULT_STATE_CONCURRENCY, async (device) => {
-          const result = await devicesApi.status(device.id);
- 
-          Iif (this.stateRunId !== runId) {
-            return;
-          }
- 
-          this.stateByDeviceId = {
-            ...this.stateByDeviceId,
-            [getDeviceId(device)]: result.ok
-              ? normalizeStatusSuccess(device, result)
-              : normalizeStatusError(device, result),
-          };
-        });
-      } catch (error) {
-        if (this.stateRunId === runId) {
-          this.stateError = {
-            type: "state_loader_error",
-            message: error?.message || "Device states loader failed",
-          };
-        }
-      } finally {
-        Eif (this.stateRunId === runId) {
-          this.isLoadingStates = false;
-        }
-      }
-    },
- 
-    async rebootDevice(id) {
-      const deviceId = String(id);
-      this.rebootingIds.add(deviceId);
- 
-      const result = await devicesApi.reboot(id);
- 
-      this.rebootingIds.delete(deviceId);
-      return result;
-    },
-  },
-});
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/stores/favorites.js.html b/webclient-vue/coverage/stores/favorites.js.html deleted file mode 100644 index 58d6fbf..0000000 --- a/webclient-vue/coverage/stores/favorites.js.html +++ /dev/null @@ -1,235 +0,0 @@ - - - - - - Code coverage report for stores/favorites.js - - - - - - - - - -
-
-

All files / stores favorites.js

-
- -
- 100% - Statements - 20/20 -
- - -
- 100% - Branches - 6/6 -
- - -
- 100% - Functions - 8/8 -
- - -
- 100% - Lines - 19/19 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51  -  -3x -  -  -22x -22x -  -1x -  -  -  -  -10x -  -  -3x -22x -  -  -  -  -  -34x -  -  -  -10x -10x -8x -8x -  -  -  -  -3x -2x -  -  -  -3x -1x -1x -  -  -2x -2x -  -  -  - 
import { defineStore } from "pinia";
- 
-const STORAGE_KEY = "sh_fav_areas";
- 
-function readFavorites() {
-  try {
-    return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]").map(String);
-  } catch (_) {
-    return [];
-  }
-}
- 
-function writeFavorites(ids) {
-  localStorage.setItem(STORAGE_KEY, JSON.stringify(ids.map(String)));
-}
- 
-export const useFavoritesStore = defineStore("favorites", {
-  state: () => ({
-    ids: readFavorites(),
-  }),
- 
-  actions: {
-    has(id) {
-      return this.ids.includes(String(id));
-    },
- 
-    add(id) {
-      const nextId = String(id);
-      if (!this.ids.includes(nextId)) {
-        this.ids.push(nextId);
-        writeFavorites(this.ids);
-      }
-    },
- 
-    remove(id) {
-      this.ids = this.ids.filter((item) => item !== String(id));
-      writeFavorites(this.ids);
-    },
- 
-    toggle(id) {
-      if (this.has(id)) {
-        this.remove(id);
-        return false;
-      }
- 
-      this.add(id);
-      return true;
-    },
-  },
-});
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/stores/index.html b/webclient-vue/coverage/stores/index.html deleted file mode 100644 index b2c80ed..0000000 --- a/webclient-vue/coverage/stores/index.html +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - Code coverage report for stores - - - - - - - - - -
-
-

All files stores

-
- -
- 93.03% - Statements - 227/244 -
- - -
- 66.38% - Branches - 79/119 -
- - -
- 90.9% - Functions - 50/55 -
- - -
- 93.19% - Lines - 219/235 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
areas.js -
-
93.22%55/5968.75%22/3284.61%11/1394.54%52/55
devices.js -
-
92.64%63/6862.5%25/4093.33%14/1592.53%62/67
favorites.js -
-
100%20/20100%6/6100%8/8100%19/19
scanning.js -
-
95.45%21/2262.5%5/8100%5/595.45%21/22
scripts.js -
-
90.66%68/7563.63%21/3385.71%12/1490.27%65/72
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/stores/scanning.js.html b/webclient-vue/coverage/stores/scanning.js.html deleted file mode 100644 index e3df393..0000000 --- a/webclient-vue/coverage/stores/scanning.js.html +++ /dev/null @@ -1,259 +0,0 @@ - - - - - - Code coverage report for stores/scanning.js - - - - - - - - - -
-
-

All files / stores scanning.js

-
- -
- 95.45% - Statements - 21/22 -
- - -
- 62.5% - Branches - 5/8 -
- - -
- 100% - Functions - 5/5 -
- - -
- 95.45% - Lines - 21/22 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59  -  -  -2x -7x -  -  -  -  -  -  -  -  -  -1x -  -  -  -  -  -5x -5x -5x -  -5x -5x -  -  -5x -  -  -  -1x -1x -  -1x -1x -  -  -1x -1x -  -  -4x -5x -  -  -  -2x -2x -2x -  -  -  -2x -  -  -  - 
import { defineStore } from "pinia";
-import { devicesApi } from "../api/modules/devices";
- 
-export const useScanningStore = defineStore("scanning", {
-  state: () => ({
-    mode: "setup",
-    devices: [],
-    isLoading: false,
-    error: null,
-    _scanAbortController: null,
-  }),
- 
-  getters: {
-    total(state) {
-      return state.devices.length;
-    },
-  },
- 
-  actions: {
-    async scan() {
-      this._scanAbortController?.abort();
-      const controller = new AbortController();
-      this._scanAbortController = controller;
- 
-      this.isLoading = true;
-      this.error = null;
- 
-      const result =
-        this.mode === "setup"
-          ? await devicesApi.scanningSetup({ signal: controller.signal })
-          : await devicesApi.scanningAll({ signal: controller.signal });
- 
-      this._scanAbortController = null;
-      this.isLoading = false;
- 
-      Eif (!result.ok) {
-        Iif (result.error?.type === "timeout") {
-          return result;
-        }
-        this.error = result.error;
-        return result;
-      }
- 
-      this.devices = result.data?.data?.devices || [];
-      return result;
-    },
- 
-    setMode(mode) {
-      this.mode = mode;
-      this.devices = [];
-      this.error = null;
-    },
- 
-    async setupDevice(payload) {
-      return devicesApi.setupNewDevice(payload);
-    },
-  },
-});
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/stores/scripts.js.html b/webclient-vue/coverage/stores/scripts.js.html deleted file mode 100644 index 0cca471..0000000 --- a/webclient-vue/coverage/stores/scripts.js.html +++ /dev/null @@ -1,535 +0,0 @@ - - - - - - Code coverage report for stores/scripts.js - - - - - - - - - -
-
-

All files / stores scripts.js

-
- -
- 90.66% - Statements - 68/75 -
- - -
- 63.63% - Branches - 21/33 -
- - -
- 85.71% - Functions - 12/14 -
- - -
- 90.27% - Lines - 65/72 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151  -  -  -3x -12x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -2x -  -  -1x -  -  -  -  -4x -4x -4x -  -4x -4x -  -4x -4x -4x -  -4x -1x -  -  -1x -1x -  -  -3x -4x -  -  -  -2x -2x -2x -  -2x -2x -  -2x -2x -2x -  -2x -1x -1x -  -  -  -  -  -1x -2x -  -  -  -2x -2x -2x -  -2x -2x -  -2x -2x -2x -  -2x -1x -1x -  -  -  -  -  -1x -2x -  -  -  -3x -3x -  -3x -  -3x -  -3x -1x -1x -  -  -2x -  -  -  -  -  -3x -  -  -  -1x -  -1x -1x -1x -1x -  -  -  -1x -  -  -  -1x -  -1x -1x -1x -1x -  -  -  -1x -  -  -  - 
import { defineStore } from "pinia";
-import { scriptsApi } from "../api/modules/scripts";
- 
-export const useScriptsStore = defineStore("scripts", {
-  state: () => ({
-    actions: [],
-    regular: [],
-    scopes: [],
-    isLoadingActions: false,
-    isLoadingRegular: false,
-    isLoadingScopes: false,
-    errorActions: null,
-    errorRegular: null,
-    errorScopes: null,
-    runningAliases: new Set(),
-    lastRunResult: null,
-    _actionsAbortController: null,
-    _regularAbortController: null,
-    _scopesAbortController: null,
-  }),
- 
-  getters: {
-    totalActions: (state) => state.actions.length,
-    totalRegular: (state) => state.regular.length,
-    totalScopes: (state) => state.scopes.length,
-    isRunning: (state) => (alias) => state.runningAliases.has(alias),
-  },
- 
-  actions: {
-    async loadActions() {
-      this._actionsAbortController?.abort();
-      const controller = new AbortController();
-      this._actionsAbortController = controller;
- 
-      this.isLoadingActions = true;
-      this.errorActions = null;
- 
-      const result = await scriptsApi.actionsList({ signal: controller.signal });
-      this._actionsAbortController = null;
-      this.isLoadingActions = false;
- 
-      if (!result.ok) {
-        Iif (result.error?.type === "timeout") {
-          return result;
-        }
-        this.errorActions = result.error;
-        return result;
-      }
- 
-      this.actions = result.data?.data?.scripts || [];
-      return result;
-    },
- 
-    async loadRegular() {
-      this._regularAbortController?.abort();
-      const controller = new AbortController();
-      this._regularAbortController = controller;
- 
-      this.isLoadingRegular = true;
-      this.errorRegular = null;
- 
-      const result = await scriptsApi.regularList({ signal: controller.signal });
-      this._regularAbortController = null;
-      this.isLoadingRegular = false;
- 
-      if (!result.ok) {
-        Eif (result.error?.type === "timeout") {
-          return result;
-        }
-        this.errorRegular = result.error;
-        return result;
-      }
- 
-      this.regular = result.data?.data?.scripts || [];
-      return result;
-    },
- 
-    async loadScopes() {
-      this._scopesAbortController?.abort();
-      const controller = new AbortController();
-      this._scopesAbortController = controller;
- 
-      this.isLoadingScopes = true;
-      this.errorScopes = null;
- 
-      const result = await scriptsApi.scopesList({ signal: controller.signal });
-      this._scopesAbortController = null;
-      this.isLoadingScopes = false;
- 
-      if (!result.ok) {
-        Eif (result.error?.type === "timeout") {
-          return result;
-        }
-        this.errorScopes = result.error;
-        return result;
-      }
- 
-      this.scopes = result.data?.data?.scopes || [];
-      return result;
-    },
- 
-    async runScript(alias, params = {}) {
-      this.runningAliases.add(alias);
-      this.lastRunResult = null;
- 
-      const result = await scriptsApi.runAction(alias, params);
- 
-      this.runningAliases.delete(alias);
- 
-      if (!result.ok) {
-        this.lastRunResult = { alias, ok: false, error: result.error };
-        return result;
-      }
- 
-      this.lastRunResult = {
-        alias,
-        ok: true,
-        data: result.data?.data?.return?.result,
-        execTime: result.data?.data?.return?.exec_time,
-      };
-      return result;
-    },
- 
-    async setRegularState(alias, enabled) {
-      const result = await scriptsApi.setRegularState(alias, enabled);
- 
-      Eif (result.ok) {
-        const idx = this.regular.findIndex((s) => s.alias === alias);
-        Eif (idx !== -1) {
-          this.regular[idx] = { ...this.regular[idx], state: enabled ? "enabled" : "disabled" };
-        }
-      }
- 
-      return result;
-    },
- 
-    async setScopeState(name, enabled) {
-      const result = await scriptsApi.setScopeState(name, enabled);
- 
-      Eif (result.ok) {
-        const idx = this.scopes.findIndex((s) => s.name === name);
-        Eif (idx !== -1) {
-          this.scopes[idx] = { ...this.scopes[idx], state: enabled ? "enabled" : "disabled" };
-        }
-      }
- 
-      return result;
-    },
-  },
-});
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/test/mocks/handlers.js.html b/webclient-vue/coverage/test/mocks/handlers.js.html deleted file mode 100644 index 5d49d42..0000000 --- a/webclient-vue/coverage/test/mocks/handlers.js.html +++ /dev/null @@ -1,433 +0,0 @@ - - - - - - Code coverage report for test/mocks/handlers.js - - - - - - - - - -
-
-

All files / test/mocks handlers.js

-
- -
- 85.18% - Statements - 23/27 -
- - -
- 85% - Branches - 17/20 -
- - -
- 66.66% - Functions - 2/3 -
- - -
- 88.46% - Lines - 23/26 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117  -  -20x -  -  -9x -9x -  -9x -4x -  -  -  -  -  -  -  -  -  -  -  -5x -2x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -3x -  -  -  -  -  -  -  -  -  -  -  -3x -2x -  -  -  -  -  -  -  -  -  -  -1x -1x -  -  -  -  -  -  -  -4x -4x -4x -  -4x -1x -  -  -  -  -  -  -  -3x -1x -  -  -  -  -  -2x -1x -  -  -  -  -  -  -  -1x -1x -  -  -  -  -  -  -  -  -  -  -  -  -  - 
import { http, HttpResponse } from "msw";
- 
-export const handlers = [
-  // GET requests via proxy.php
-  http.get("/proxy.php", ({ request }) => {
-    const url = new URL(request.url);
-    const path = url.searchParams.get("path");
- 
-    if (path === "/api/v1/areas/list") {
-      return HttpResponse.json({
-        status: true,
-        data: {
-          areas: [
-            { id: 1, type: "room", alias: "kitchen", display_name: "Kitchen", parent_id: 0 },
-            { id: 2, type: "room", alias: "hall", display_name: "Hall", parent_id: 0 },
-          ],
-          total: 2,
-        },
-      });
-    }
- 
-    if (path === "/api/v1/devices/scanning/setup") {
-      return HttpResponse.json({
-        status: true,
-        data: {
-          devices: [
-            {
-              device_name: "New Device",
-              device_type: "relay",
-              ip_address: "192.168.1.50",
-              mac_address: "A4:CF:12:9B:3F:D2",
-              firmware_version: "1.0",
-              status: "setup",
-            },
-          ],
-        },
-      });
-    }
- 
-    Iif (path === "/api/v1/devices/list") {
-      return HttpResponse.json({
-        status: true,
-        data: {
-          devices: [
-            { id: 1, name: "Relay 1", alias: "relay_1", device_type: "relay", device_ip: "192.168.1.10", connection_status: "active" },
-          ],
-          total: 1,
-        },
-      });
-    }
- 
-    if (path === "/api/v1/scripts/actions/list") {
-      return HttpResponse.json({
-        status: true,
-        data: {
-          scripts: [
-            { alias: "kitchen_light", name: "Kitchen Light", icon: '<i class="ph ph-lightbulb"></i>', state: "enabled", author: "Test" },
-          ],
-          total: 1,
-        },
-      });
-    }
- 
-    Eif (path?.startsWith("/api/v1/areas/id/") && path.endsWith("/remove")) {
-      return HttpResponse.json({ status: true });
-    }
- 
-    return new HttpResponse(null, { status: 404 });
-  }),
- 
-  // POST requests via proxy.php
-  http.post("/proxy.php", async ({ request }) => {
-    const url = new URL(request.url);
-    const path = url.searchParams.get("path");
-    const body = await request.json().catch(() => ({}));
- 
-    if (path === "/api/v1/areas/new-area") {
-      return HttpResponse.json({
-        status: true,
-        data: {
-          area: { id: 3, type: body.type, alias: body.alias, display_name: body.display_name, parent_id: 0 },
-        },
-      });
-    }
- 
-    if (path === "/api/v1/areas/update-display-name") {
-      return HttpResponse.json({
-        status: true,
-        data: { area_id: body.area_id, display_name: body.display_name },
-      });
-    }
- 
-    if (path === "/api/v1/devices/setup/new-device") {
-      return HttpResponse.json({
-        status: true,
-        data: {
-          device: { id: 2, name: body.name, alias: body.alias, device_type: "relay", device_ip: body.device_ip },
-        },
-      });
-    }
- 
-    Eif (path === "/api/v1/scripts/actions/run") {
-      return HttpResponse.json({
-        status: true,
-        data: {
-          return: {
-            result: { ok: true },
-            exec_time: "0.042 seconds",
-          },
-        },
-      });
-    }
- 
-    return new HttpResponse(null, { status: 404 });
-  }),
-];
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/coverage/test/mocks/index.html b/webclient-vue/coverage/test/mocks/index.html deleted file mode 100644 index 38fd128..0000000 --- a/webclient-vue/coverage/test/mocks/index.html +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - Code coverage report for test/mocks - - - - - - - - - -
-
-

All files test/mocks

-
- -
- 85.18% - Statements - 23/27 -
- - -
- 85% - Branches - 17/20 -
- - -
- 66.66% - Functions - 2/3 -
- - -
- 88.46% - Lines - 23/26 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
handlers.js -
-
85.18%23/2785%17/2066.66%2/388.46%23/26
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/webclient-vue/docs/README.md b/webclient-vue/docs/README.md deleted file mode 100644 index 41073a9..0000000 --- a/webclient-vue/docs/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# SHServ Vue Client Specification - -This directory contains the technical specification for a new SHServ web client. - -The current `webclient/` implementation is the reference implementation. The new -client should preserve its functional behavior while moving to Vue and the -`gnexus-ui-kit` design system. - -## Documents - -- `overview.md` - goals, boundaries, architecture direction. -- `current-client-map.md` - current routes, screens, modules, and UI behavior. -- `api-contract.md` - API endpoints used by the client and expected response handling. -- `screens.md` - target screen-by-screen functional specification. -- `state-and-components.md` - current global helpers mapped to Vue stores/components. -- `scaffold.md` - recommended Vue/Vite project scaffold and first milestone. -- `coding-conventions.md` - code organization, state, API, and UI rules. -- `ui-kit-integration.md` - planned `gnexus-ui-kit` adapter strategy. -- `migration-plan.md` - phased plan for building the new client in parallel. - -## Working Rules - -- Keep the current `webclient/` operational until the new client covers the main flows. -- Treat backend API behavior as a contract; document gaps before changing endpoints. -- New UI code should be JavaScript, not TypeScript. -- New UI code should not depend on old global objects such as `Helper`, `Screens`, - `DataProvider`, `Toasts`, or `Modals`. -- Generated build artifacts for the new client should stay inside the new client - directory. diff --git a/webclient-vue/docs/api-contract.md b/webclient-vue/docs/api-contract.md deleted file mode 100644 index ed72228..0000000 --- a/webclient-vue/docs/api-contract.md +++ /dev/null @@ -1,123 +0,0 @@ -# API Contract - -The new client should wrap all backend calls in a dedicated API layer. UI -components should not call `fetch` directly. - -## Transport - -Current dev transport: - -```text -GET|POST {baseUrl}/proxy.php?path=/api/v1/... -``` - -Current backend base from `webclient/config.php`: - -```text -http://smart-home-serv.local -``` - -## Common Response Shape - -Success: - -```json -{ - "status": true, - "data": {} -} -``` - -Error: - -```json -{ - "status": false, - "error_alias": "device_request_fail", - "failed_fields": [], - "msg": "Устройство не отвечает" -} -``` - -The new API layer should normalize responses into one result shape: - -```js -// success -{ ok: true, data, meta } - -// failure -{ ok: false, error, meta } -``` - -## Devices - -| Method | Path | Current Use | -| --- | --- | --- | -| GET | `/api/v1/devices/list` | Devices list. | -| GET | `/api/v1/devices/id/{id}` | Device DB info. | -| GET | `/api/v1/devices/id/{id}/info` | Device DB info plus live `/about`. | -| GET | `/api/v1/devices/id/{id}/status` | Live device state. | -| GET | `/api/v1/devices/scanning/all` | Scan all devices in local network range. | -| GET | `/api/v1/devices/scanning/setup` | Scan setup-mode devices. | -| POST | `/api/v1/devices/setup/new-device` | Register a setup-mode device. | -| GET | `/api/v1/devices/id/{id}/reboot` | Reboot one device. | -| POST | `/api/v1/devices/resetup` | Reset/rebind device token. | -| POST | `/api/v1/devices/reset` | Reset device. | -| POST | `/api/v1/devices/update-name` | Update device name. | -| POST | `/api/v1/devices/update-description` | Update device description. | -| POST | `/api/v1/devices/update-alias` | Update device alias. | -| POST | `/api/v1/devices/place-in-area` | Assign device to area. | -| GET | `/api/v1/devices/id/{id}/unassign-from-area` | Remove device from area. | - -Device state rendering depends on `device_type`: - -- `relay` expects `channels[]` with `id` and `state`. -- `button` expects `channels[]` with `id` and `indicator`. -- `sensor` expects `sensors.light`, `temperature`, `pressure`, `humidity`, - `radar`, and `microphone`. -- `hatch` expects `hatch.state` and `hatch.position_pct`. - -## Scripts - -| Method | Path | Current Use | -| --- | --- | --- | -| GET | `/api/v1/scripts/actions/list` | Action script cards. | -| POST | `/api/v1/scripts/actions/run` | Run action script by alias. | -| GET | `/api/v1/scripts/regular/list` | Regular scripts table. | -| GET | `/api/v1/scripts/regular/alias/{alias}/enable` | Enable regular script. | -| GET | `/api/v1/scripts/regular/alias/{alias}/disable` | Disable regular script. | -| GET | `/api/v1/scripts/scopes/list` | Scopes table. | -| GET | `/api/v1/scripts/scopes/name/{name}/enable` | Enable scope. | -| GET | `/api/v1/scripts/scopes/name/{name}/disable` | Disable scope. | -| GET | `/api/v1/scripts/scopes/name/{name}/remove` | Remove scope. | -| GET | `/api/v1/scripts/scopes/name/{filename}` | Get scope source by filename/name. | -| POST | `/api/v1/scripts/scopes/new` | Create scope. | -| POST | `/api/v1/scripts/scopes/update` | Update scope. | -| POST | `/api/v1/scripts/place-in-area` | Assign script to area. | -| GET | `/api/v1/scripts/id/{id}/unassign-from-area` | Remove script from area. | - -## Areas - -| Method | Path | Current Use | -| --- | --- | --- | -| GET | `/api/v1/areas/list` | Areas tree and favorites. | -| GET | `/api/v1/areas/id/{area_id}/list` | Area inner list. | -| POST | `/api/v1/areas/new-area` | Create area. | -| GET | `/api/v1/areas/id/{area_id}/remove` | Remove area. | -| POST | `/api/v1/areas/place-in-area` | Move area under another area. | -| POST | `/api/v1/areas/update-display-name` | Rename area display name. | -| POST | `/api/v1/areas/update-alias` | Update area alias. | -| GET | `/api/v1/areas/id/{area_id}/devices` | Devices in area. | -| GET | `/api/v1/areas/id/{area_id}/scripts` | Scripts in area. | -| GET | `/api/v1/areas/id/{id}/unassign-from-area` | Remove area from parent. | -| GET | `/api/v1/areas/types/list` | Allowed area types. | -| GET | `/api/v1/areas/reboot_devices` | Reboot all devices. | -| GET | `/api/v1/areas/id/{area_id}/reboot_devices` | Reboot devices in area. | - -## Reliability Requirements - -- Device status loading must be isolated per device. -- A failed device status request must not break the full screen. -- Device status requests should have a concurrency limit. -- The UI should distinguish backend error, network/proxy error, and device offline. -- API callback/consumer errors must not be treated as transport failures. diff --git a/webclient-vue/docs/coding-conventions.md b/webclient-vue/docs/coding-conventions.md deleted file mode 100644 index 1d8a8a3..0000000 --- a/webclient-vue/docs/coding-conventions.md +++ /dev/null @@ -1,113 +0,0 @@ -# Coding Conventions - -## Source Language - -Use JavaScript for the new client. - -Rules: - -- Backend response validation lives in `src/api/schemas`. -- UI-facing normalized models are produced by mapper functions. -- Avoid passing raw backend envelopes deep into UI components. -- If a response shape is uncertain, validate it at the API/module boundary and - return `invalid_response` on mismatch. - -## File Naming - -- Vue components: `PascalCase.vue`. -- Stores: `useXStore` exported from `stores/x.js`. -- Composables: `useThing.js`. -- API modules: lowercase domain names, e.g. `devices.js`. -- Runtime schemas: domain names, e.g. `devices.js`, `areas.js`. - -## Component Rules - -- Keep route components thin. -- Put reusable domain UI in `features/{domain}/components`. -- Put generic reusable UI in `components`. -- Do not pass raw backend DTO-like objects directly into deep UI components unless - the component is API-specific by design. -- Use props/events for reusable components. -- Use stores for screen-level data and cross-component state. - -## Store Rules - -- Stores own fetch/mutation orchestration. -- Stores call API modules. -- Stores expose loading/error states. -- Stores should not know about toasts or router navigation. -- Components or page-level actions decide how to show feedback. - -## API Rules - -- All API methods return normalized `{ ok, data?, error?, meta }` results. -- API modules do not show UI feedback. -- API modules do not mutate stores directly. -- API mappers should be pure functions. -- Request cancellation should use `AbortController`. - -## Error Handling - -Use this UI policy: - -- Route-level fetch failure: show route error state with retry. -- Per-row device status failure: show row-level error/offline state. -- Mutation failure: keep the modal/page open and show toast plus inline field - errors when available. -- Validation failure: show field-level errors before submitting. - -Do not: - -- Treat component render errors as network errors. -- Hide backend `msg` when present. -- Swallow failed mutations silently. - -## Formatting And Style - -- Use tabs or spaces consistently after scaffold decision. -- Keep components small enough that template, script, and styles are easy to scan. -- Prefer explicit names over abbreviations. -- Avoid direct DOM manipulation except for integration boundaries. -- Avoid global singletons unless they are intentional app-level services. - -## Icons - -Current client uses Phosphor Icons. - -New client options: - -- Continue using Phosphor if `gnexus-ui-kit` supports it or does not provide an - icon system. -- Prefer UI-kit icon primitives if they exist. - -Do not mix several icon systems in the same screen. - -## Dates And Formatting - -Centralize formatting in `src/utils/format.js` and `src/utils/dates.js`. - -Required helpers: - -- `formatDate` -- `formatDateTime` -- `formatTimeAgo` -- `formatDeviceConnectionStatus` -- `formatScriptState` - -## CSS And Styling - -- Prefer UI-kit tokens/classes. -- Put app-specific layout styles in `src/styles`. -- Avoid copying old SCSS wholesale. -- If old styles are needed temporarily, isolate them under a legacy namespace. -- Do not override UI-kit internals with brittle selectors. -- Keep UI-kit compatibility wrappers thin enough that design updates from - `gnexus-ui-kit` are visible without rewriting feature components. - -## Accessibility Baseline - -- Buttons must use real `