Newer
Older
smart-home-server / webclient / DOCS.md
@Eugene Sukhodolskiy Eugene Sukhodolskiy 17 hours ago 16 KB Bugfixes. Webclient. DataProvider

Webclient

SPA-панель управления умным домом. Общается с сервером через REST API (через proxy.php). Живёт в /webclient, собирается локально, деплоится статикой.


Сборка и разработка

npm install   # один раз
npm start     # сборка + watch + live-reload

Gulp собирает параллельно:

  • src/scss/main.scssdist/css/main.css (sass → autoprefixer → minify + sourcemap)
  • src/js/index.jsdist/js/main.js (esbuild bundle + minify + sourcemap)

После сборки следит за изменениями файлов, пересобирает на лету.

Конфигурация сборки

В gulpfile.js есть закомментированная задача serve (BrowserSync). Чтобы включить — раскомментировать вызов serve в exports.default.


Конфигурация приложения

config.php — единственный файл настроек:

return [
    "version"  => "0.2 dev",
    "server"   => "http://192.168.1.101",   // адрес сервера SHServ
    "allowed_prefixes" => ["/api/v1/"],     // пути, разрешённые через прокси
    "proxy.allowed_origins" => [            // CORS whitelist для proxy.php
        "http://localhost:5173",
    ],
];

proxy.php — CORS-прокси, стоит между браузером и сервером SHServ. Форвардит запросы только по путям из allowed_prefixes, проставляет CORS-заголовки, форвардит Authorization, Content-Type, Accept.

ui.php — kitchensink-страница для просмотра всех UI-компонентов. Открывается в браузере напрямую, не является частью SPA.


JS-архитектура

Точка входа

src/js/index.js — инициализация при DOMContentLoaded:

  1. Глобальные синглтоны вешаются на window: DataProvider, Toasts, Modals, Helper, confirmPopup, advancedSelect, editableString
  2. hud() — инициализирует навигацию
  3. new SmartHomeApi(...) — создаётся клиент API
  4. new Screens(...) — создаётся менеджер экранов
  5. routes(screens, sh_api) — регистрируются все экраны
  6. screens.routing() — запускается роутинг (polling hash каждые 50 мс)

Константа API_BASEURL инжектируется esbuild при сборке из gulpfile.js.


Роутинг

Hash-based SPA. Формат: #!/path.

Маршруты определены в src/js/routes.js:

Hash Экран
#!/ редирект → #!/areas/tree
#!/devices список устройств
#!/devices/scanning поиск новых устройств
#!/scripts/actions action-скрипты
#!/scripts/regular regular-скрипты
#!/scripts/scopes scope-файлы
#!/areas/tree дерево областей
всё остальное экран 404

Screens

src/js/components/Screens.js — движок SPA.

Структура объекта экрана:

{
  alias: "my_screen",
  renderer: () => "<div>...</div>",     // возвращает HTML-строку
  initer: (scr) => {                    // навешивает логику
    // загрузить данные, добавить обработчики
    scr.ready();                        // обязательно в конце
  }
}

Ключевые методы:

Метод Что делает
screens.add(route, screenObj) Зарегистрировать экран
screens.switch(alias) Переключиться на экран (пересоздаёт DOM)
screens.reload() Пересоздать текущий экран
screens.reinit() Перезапустить initer без пересоздания DOM
screens.ready() Пометить экран как готовый (убирает лоадер)
screens.error(title, text) Показать экран ошибки
screens.onSwitch(cb) Хук на переключение экрана

SmartHomeApi

src/js/sh/SmartHomeApi.js — HTTP-клиент, callback-style.

const api = new SmartHomeApi({
    base_url:   API_BASEURL,
    token:      "YOUR_TOKEN",
    timeout_ms: 10000,
    proxy_path: "/proxy.php",
    on_unauthorized: ({ error }) => { /* ... */ }
});

Все методы принимают коллбэк (err, data, meta):

  • errnull при успехе, иначе { type, message, status_code?, raw? }
  • data — распарсенный JSON-ответ
  • meta{ url, method, status_code, headers }

Типы ошибок: network_error, timeout, http_error, api_error.

Модули:

api.devicessrc/js/sh/modules/DevicesApi.js

Метод Endpoint
list(cb) GET /api/v1/devices/list
scanning_setup(cb) GET /api/v1/devices/scanning/setup
scanning_all(cb) GET /api/v1/devices/scanning/all
setup_new_device(payload, cb) POST /api/v1/devices/setup/new-device
get(id, cb) GET /api/v1/devices/id/{id}
info(id, cb) GET /api/v1/devices/id/{id}/info
status(id, cb) GET /api/v1/devices/id/{id}/status
action(payload, cb) POST /api/v1/devices/action
remove(id, cb) GET /api/v1/devices/id/{id}/remove
reboot(id, cb) GET /api/v1/devices/id/{id}/reboot
place_in_area(payload, cb) POST /api/v1/devices/place-in-area
unassign_from_area(id, cb) GET /api/v1/devices/id/{id}/unassign-from-area
update_name(payload, cb) POST /api/v1/devices/update-name
update_description(payload, cb) POST /api/v1/devices/update-description
update_alias(payload, cb) POST /api/v1/devices/update-alias
resetup(payload, cb) POST /api/v1/devices/resetup
reset(payload, cb) POST /api/v1/devices/reset

api.areassrc/js/sh/modules/AreasApi.js

Метод Endpoint
list(cb) GET /api/v1/areas/list
inner_list(area_id, cb) GET /api/v1/areas/id/{id}/list
new_area(payload, cb) POST /api/v1/areas/new-area
remove(area_id, cb) GET /api/v1/areas/id/{id}/remove
place_in_area(payload, cb) POST /api/v1/areas/place-in-area
unassign_from_area(id, cb) GET /api/v1/areas/id/{id}/unassign-from-area
update_display_name(payload, cb) POST /api/v1/areas/update-display-name
update_alias(payload, cb) POST /api/v1/areas/update-alias
devices(area_id, cb) GET /api/v1/areas/id/{id}/devices
scripts(area_id, cb) GET /api/v1/areas/id/{id}/scripts
types_list(cb) GET /api/v1/areas/types/list
reboot_devices(area_id, cb) GET /api/v1/areas/id/{id}/reboot_devices

api.scriptssrc/js/sh/modules/ScriptsApi.js

Метод Endpoint
actions_list(cb) GET /api/v1/scripts/actions/list
regular_list(cb) GET /api/v1/scripts/regular/list
scopes_list(cb) GET /api/v1/scripts/scopes/list
run(payload, cb) POST /api/v1/scripts/actions/run
scope_update(payload, cb) POST /api/v1/scripts/scopes/update
action_enable/disable(alias, cb) GET `.../alias/{alias}/enable\ disable`
regular_enable/disable(alias, cb) GET `.../regular/alias/{alias}/enable\ disable`
scope_enable/disable(name, cb) GET `.../scope/{name}/enable\ disable`
place_in_area(payload, cb) POST /api/v1/scripts/place-in-area
unassign_from_area(id, cb) GET /api/v1/scripts/id/{id}/unassign-from-area

DataProvider

src/js/DataProvider.js — кеш данных в рамках сессии. Основное назначение: хранить структуры по ID, чтобы потом получить их в попапах и экранах без повторных запросов к серверу.

Базовый доступ:

window.DataProvider.set("key", value);
window.DataProvider.get("key");
window.DataProvider.setRaw("key", value);  // сохраняет с префиксом "raw."
window.DataProvider.getRaw("key");

Получить из кеша или загрузить с сервера:

DataProvider.getOrFetch(
    `raw.devices.${id}`,
    (cb) => sh_api.devices.get(id, (err, resp) => cb(err, resp?.data?.device)),
    (err, device) => {
        if (err) return scr.error("Ошибка", err.message);
        // данные здесь — либо из кеша, либо свежезагруженные
    }
);

Инвалидация после мутаций:

DataProvider.invalidate("raw.devices.12");          // один ключ
DataProvider.invalidatePrefix("raw.devices");       // всё под префиксом

Получить всё закешированное под префиксом:

const allDevices = DataProvider.getCollection("raw.devices");  // → [device, device, ...]

Ключи кеша, которые заполняют API-модули:

Ключ Заполняется при
raw.devices.{id} api.devices.list()
raw.areas.{id} api.areas.list()
raw.actions_scripts.{alias} api.scripts.actions_list()
raw.regular_scripts.{alias} api.scripts.regular_list()
raw.scopes.{name} api.scripts.scopes_list()
raw.scanning.setup.devices.{i} api.devices.scanning_setup()
raw.scanning.devices.{i} api.devices.scanning_all()

Паттерн добавления нового экрана

  1. Создать файл src/js/components/screens/my-section/my-screen.js:

    export function myScreen(sh_api) {
     return {
         alias: "my_screen",
         renderer: () => `<div class="my-screen">...</div>`,
         initer: (scr) => {
             sh_api.devices.list((err, data) => {
                 if (err) return scr.error("Ошибка", err.message);
                 // заполнить DOM
                 scr.ready();
             });
         }
     };
    }
  2. Зарегистрировать маршрут в src/js/routes.js:

    screens.add("#!/my-section", myScreen(sh_api));
  3. Добавить ссылку в навигацию в src/js/components/hud.js.


SCSS-архитектура

src/scss/
├── main.scss               ← точка входа
├── _fonts.scss             ← подключение шрифтов (IBM Plex Mono)
├── _palette-colors.scss    ← все CSS-переменные и SCSS-переменные цветов
├── _mixins.scss            ← media_up/down/between, hover_touch
├── _spacing.scss           ← шкала отступов $space-0..$space-12
├── _utils.scss             ← утилиты: .mt-*, .mb-*, .p-*, .g-*, .d-none
├── _ui.scss                ← импортирует ui_components/
├── _app.scss               ← импортирует app/
├── ui_components/          ← переиспользуемые компоненты
│   ├── _buttons.scss       ← .btn, .btn-primary, .btn-accent, .btn-small, .with-icon
│   ├── _forms.scss         ← .input, .textarea, .select, .form-group, .label
│   ├── _modals.scss        ← .modal, .modal-panel, .modal-header/body/footer
│   ├── _cards.scss         ← .card, .card-content, .script-action
│   ├── _lists.scss         ← .list, .list-item, .list-nav, .list-action
│   ├── _toasts.scss        ← .toast, .toast-success/warning/danger
│   ├── _badges.scss        ← .badge, .badge-success/warning/primary
│   ├── _tables.scss        ← .table, .table-row, .table-empty
│   ├── _alerts.scss        ← .alert, .alert-primary/success/error
│   ├── _typography.scss    ← .h1-.h6, .contrast, .text-light
│   ├── _palette.scss       ← .bg-*, .text-* цветовые утилиты
│   ├── _loader.scss        ← .loader, .circle-loader
│   ├── _advanced-select.scss
│   └── _editable-string.scss
└── app/
    ├── _hud.scss           ← .hud, .navigation, .nav-link
    ├── _load-screen.scss   ← .load-screen, .a-show
    ├── _error-screen.scss  ← .error-screen
    └── _empty-here.scss    ← .empty-here

Цвета (_palette-colors.scss) — тёмная кибер-тема:

  • Фон: $color-black: #0A0A0D, $color-dark: #1A1A23
  • Акценты: $color-cyan: #00FFCC, $color-orange: #ff6f30, $color-electric-blue: #00B3FF
  • Состояния: success $color-neon-green, warning $color-neon-yellow, error #FF3C00

Брейкпоинты в _mixins.scss: xs 360 / sm 480 / md 768 / lg 1024 / xl 1280 / xxl 1440.

Анимационные классы: .a-show (появление), .a-hide (исчезновение) — CSS-переходы 300 мс.


UI-компоненты (JS)

Компонент Файл Назначение
Toasts toasts.js Всплывающие уведомления: .success(msg), .error(msg), .warning(msg)
Modals modals.js Строитель модальных окон: Modals.create(id, { title, body, actions, onready })
confirmPopup confirm-popup.js Диалог подтверждения: confirmPopup("Текст?", onConfirm, onCancel)
advancedSelect advanced-select.js Dropdown с фильтрацией и клавиатурной навигацией
editableString editable-string.js Inline-редактор строки, событие onChange
Helper helper.js Генераторы HTML (Helper.template.*), управление состояниями (Helper.states.*), нормализация данных (Helper.unification.*)

Состояния кнопок/карточек через Helper:

Helper.states.btnLoadingState(btnElement, true);   // показать спиннер
Helper.states.btnLoadingState(btnElement, false);  // восстановить

Структура компонентов экранов

src/js/components/screens/
├── devices/
│   ├── devices.js                  ← роутер к экранам устройств
│   ├── devices-list-screen.js      ← таблица всех устройств
│   ├── devices-scanning-screen.js  ← поиск и добавление новых
│   ├── device-state-component.js   ← рендер состояния по типу (relay/button/sensor/hatch)
│   ├── device-details-popup.js     ← попап с деталями устройства
│   ├── device-setup-form-popup.js  ← форма добавления устройства
│   └── devices-funcs.js            ← общие функции для экранов устройств
├── areas/
│   ├── areas.js                    ← роутер к экранам областей
│   ├── areas-tree-screen.js        ← иерархическое дерево областей
│   ├── areas-create-new-modal.js   ← форма создания области
│   ├── areas-details-modal.js      ← редактирование области
│   ├── areas-devices-modal.js      ← устройства в области
│   ├── areas-actions-modal.js      ← скрипты в области
│   ├── areas-placeto-component.js  ← компонент перемещения в дерево
│   └── areas-funcs.js
└── scripts/
    ├── scripts.js                  ← роутер к экранам скриптов
    ├── scripts-actions-screen.js   ← action-скрипты
    ├── scripts-regular-screen.js   ← regular-скрипты
    ├── scripts-scopes-screen.js    ← scope-файлы
    ├── scripts-action-popup.js     ← детали и запуск action-скрипта
    └── scripts-funcs.js