SPA-панель управления умным домом. Общается с сервером через REST API (через proxy.php). Живёт в /webclient, собирается локально, деплоится статикой.
npm install # один раз npm start # сборка + watch + live-reload
Gulp собирает параллельно:
src/scss/main.scss → dist/css/main.css (sass → autoprefixer → minify + sourcemap)src/js/index.js → dist/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.
src/js/index.js — инициализация при DOMContentLoaded:
window: DataProvider, Toasts, Modals, Helper, confirmPopup, advancedSelect, editableStringhud() — инициализирует навигациюnew SmartHomeApi(...) — создаётся клиент APInew Screens(...) — создаётся менеджер экрановroutes(screens, sh_api) — регистрируются все экраны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 |
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) |
Хук на переключение экрана |
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):
err — null при успехе, иначе { type, message, status_code?, raw? }data — распарсенный JSON-ответmeta — { url, method, status_code, headers }Типы ошибок: network_error, timeout, http_error, api_error.
Модули:
api.devices → src/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.areas → src/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.scripts → src/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 |
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() |
Создать файл 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();
});
}
};
}Зарегистрировать маршрут в src/js/routes.js:
screens.add("#!/my-section", myScreen(sh_api));Добавить ссылку в навигацию в src/js/components/hud.js.
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$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 мс.
| Компонент | Файл | Назначение |
|---|---|---|
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