From 91ca4e8d576b05c4454c29d47bb4bd2d5df3f1cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=91=D1=80=D0=B0=D1=82=D1=81?= =?UTF-8?q?=D0=BA=D0=B8=D1=85?= Date: Sun, 28 Jun 2026 15:38:50 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B2=D1=8B=D0=BD=D0=B5=D1=81=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B5=D0=BA?= =?UTF-8?q?=20=D0=B2=20=D0=BE=D1=82=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20=D1=84=D0=B0=D0=B9=D0=BB,=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 255 +++++++++++++++++++++++++++++++++++++++++++++ index.html | 11 +- panel-data.json | 113 ++++++++++++++++++++ script.js | 271 ++++++++++++++++++++++++------------------------ settings.json | 4 + 5 files changed, 519 insertions(+), 135 deletions(-) create mode 100644 README.md create mode 100644 panel-data.json create mode 100644 settings.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..e6b8f2a --- /dev/null +++ b/README.md @@ -0,0 +1,255 @@ +# Панель экосистемы + +Статическая навигационная панель для быстрого доступа к сервисам инфраструктуры. Поддерживает иерархию **раздел → подраздел → элемент**, поиск, дерево навигации, несколько режимов отображения и настройку внешнего вида. + +Стек: HTML, CSS, vanilla JavaScript. Без сборщиков и фреймворков. + +## Возможности + +- **Древовидная навигация** слева в стиле файлового менеджера +- **Каталог карточек** справа с collapsible-разделами +- **Поиск** по названию, тегам, типу сервиса и родительским разделам +- **4 режима отображения**: плитки, список, компактные плитки, компактный список +- **Темы**: светлая / тёмная, 4 пресета акцента + свой градиент +- **Настройки** через popup (иконка ⚙) +- **Изменяемая ширина** бокового меню (drag) +- **Сохранение состояния** в `localStorage` (настройки UI, раскрытие веток) +- **Конфигурация через JSON** — локальный объект или загрузка по URL + +## Быстрый старт + +Панель использует `fetch`, поэтому её нужно открывать через HTTP-сервер, а не напрямую как `file://`. + +```bash +cd ecosystem +npx serve . +``` + +Открой в браузере адрес, который покажет сервер (обычно `http://localhost:3000`). + +## Структура проекта + +``` +ecosystem/ +├── index.html # Разметка страницы +├── styles.css # Стили, темы, адаптив +├── script.js # Логика, рендер, настройки +├── settings.json # Источник данных панели (URL или inline) +├── panel-data.json # Данные навигации (по умолчанию) +├── assets/ # SVG-иконки сервисов +└── README.md +``` + +## Конфигурация + +### `settings.json` + +Указывает, откуда загружать данные панели. + +**Вариант 1 — загрузка из файла или URL:** + +```json +{ + "dataUrl": "./panel-data.json" +} +``` + +Можно указать внешний адрес: + +```json +{ + "dataUrl": "https://example.com/ecosystem/panel-data.json" +} +``` + +**Вариант 2 — inline-объект в том же файле:** + +```json +{ + "dataUrl": "", + "data": { + "title": "Панель экосистемы", + "sections": [] + } +} +``` + +> Если `dataUrl` не пустой, он имеет **приоритет** над полем `data`. + +При ошибке загрузки или невалидном JSON на странице показывается экран с описанием проблемы. + +### `panel-data.json` + +Основной файл с содержимым панели. + +```json +{ + "title": "Панель экосистемы", + "sections": [ + { + "id": "user", + "number": "1", + "title": "Интерфейс пользователя", + "description": "Описание раздела", + "groups": [ + { + "id": "message-filtering", + "number": "1.1", + "title": "Управление фильтрацией", + "description": "Описание подраздела", + "items": [ + { + "number": "1.1.1", + "title": "Редактирование правил", + "type": "rules", + "tags": ["правила", "фильтрация"], + "url": "https://rules.example.com" + } + ] + } + ] + } + ], + "serviceMeta": {} +} +``` + +### Поля данных + +| Уровень | Поля | Описание | +|---------|------|----------| +| **Корень** | `title` | Заголовок панели (sidebar + ``) | +| | `sections` | Массив разделов | +| | `serviceMeta` | Опционально: доп. типы сервисов | +| **section** | `id`, `number`, `title`, `description`, `groups` | Крупный раздел | +| **group** | `id`, `number`, `title`, `description`, `items` | Подраздел | +| **item** | `number`, `title`, `type`, `tags`, `url` | Кликабельный элемент | + +- `type` — ключ из `serviceMeta` (определяет иконку и подпись типа) +- `tags` — используются в поиске и опционально в дереве +- `url` — ссылка перехода; `"#"` означает заглушку (клик не ведёт никуда) + +### Типы сервисов (`serviceMeta`) + +Базовые типы заданы в `script.js`: + +`ceph`, `cloudbeaver`, `config`, `gitea`, `grafana`, `ipmi`, `kubernetes`, `ldap`, `messages`, `opensearch`, `postgresql`, `prometheus`, `rabbitmq`, `redmine`, `rules`, `xpath`, `xmpp` + +В `panel-data.json` можно добавить или переопределить типы: + +```json +{ + "serviceMeta": { + "my-service": { + "label": "My Service", + "icon": "assets/config.svg" + } + } +} +``` + +## Настройки интерфейса (⚙) + +Доступны в popup-окне и сохраняются в `localStorage` (`ecosystem-panel-preferences`): + +| Настройка | Описание | +|-----------|----------| +| **Тема** | Светлая / тёмная | +| **Вид** | Плитки, список, компакт плитки, компакт список | +| **Нумерация** | Показывать номера разделов, подразделов и элементов | +| **Теги дерева** | Поисковые теги рядом с элементами в дереве | +| **2 колонки в дереве** | Элементы папок в 2 колонки (взаимоисключимо с тегами) | +| **Авто раскрывать поиск** | При поиске сворачивать ветки без совпадений | +| **Акцент** | violet, cyan, lime, amber или свой градиент | + +### Свой градиент + +Кнопка **«Свой»** открывает настройки: + +- угол (0–360°) +- цвет «от» и «до» + +Значения сохраняются в `localStorage`. + +## Поведение интерфейса + +### Поиск + +Фильтрует каталог и подсвечивает совпадения в дереве и карточках. Ищет по: + +- названию элемента +- номеру +- тегам +- типу сервиса +- названиям родительских разделов + +### Дерево навигации + +- Клик по **папке** — раскрытие/сворачивание +- Клик по **названию** — прокрутка к элементу в каталоге +- Состояние раскрытия сохраняется отдельно для дерева и контента (`ecosystem-panel-collapse-state`) + +### Адаптив + +- **Touch-устройства** (`pointer: coarse` / `hover: none`): stacked-layout, меню сверху +- **Мышь** (`pointer: fine` + `hover: hover`): sidebar + resizer + каталог в колонках +- **2 колонки в дереве на touch**: в portrait или при ширине от 520px + +### Ширина layout + +- Обычный режим: max **1560px**, sidebar до **560px** +- Режим «2 колонки в дереве»: max **1920px**, sidebar до **760px** + +## localStorage + +| Ключ | Содержимое | +|------|------------| +| `ecosystem-panel-preferences` | Тема, акцент, вид, ширина sidebar, toggle-настройки | +| `ecosystem-panel-collapse-state` | Раскрытые/свернутые ветки дерева и каталога | + +## Разработка + +### Добавить новый элемент + +1. Открой `panel-data.json` +2. Найди нужный `group` в `sections` +3. Добавь объект в `items`: + +```json +{ + "number": "2.5.3", + "title": "Новый сервис", + "type": "gitea", + "tags": ["gitea", "новый"], + "url": "https://gitea.example.com" +} +``` + +4. Обнови страницу + +### Добавить новую иконку + +1. Положи SVG в `assets/` +2. Зарегистрируй тип в `serviceMeta` внутри `panel-data.json` или используй существующий `type` + +### Внешний источник данных + +Укажи URL в `settings.json`: + +```json +{ + "dataUrl": "https://your-server/panel-data.json" +} +``` + +Убедись, что сервер отдаёт корректный `Content-Type: application/json` и разрешает CORS, если домен отличается от панели. + +## Ограничения + +- Требуется HTTP-сервер (`fetch` не работает с `file://`) +- Внешний `dataUrl` должен быть доступен по CORS +- Настройки UI хранятся локально в браузере и не синхронизируются между устройствами + +## Лицензия + +Приватный проект. Используй и изменяй по своему усмотрению. diff --git a/index.html b/index.html index 4884c19..61f57b3 100644 --- a/index.html +++ b/index.html @@ -227,6 +227,15 @@ <!-- Каталог карточек генерируется из ecosystemSections в script.js. --> <section class="catalog cards-view" aria-live="polite" data-catalog-root></section> + <!-- Шаблон ошибки загрузки settings.json / panel data. --> + <template id="config-error-template"> + <div class="empty-state config-error-state"> + <img src="assets/config.svg" alt="" /> + <h2>Не удалось загрузить панель</h2> + <p data-config-error-message></p> + </div> + </template> + <!-- Шаблон состояния, когда поиск не нашел ни одного элемента. --> <template id="empty-template"> <div class="empty-state"> @@ -238,6 +247,6 @@ </div> </main> - <script src="script.js"></script> + <script src="script.js" defer></script> </body> </html> diff --git a/panel-data.json b/panel-data.json new file mode 100644 index 0000000..1093387 --- /dev/null +++ b/panel-data.json @@ -0,0 +1,113 @@ +{ + "title": "Панель экосистемы", + "sections": [ + { + "id": "user", + "number": "1", + "title": "Интерфейс пользователя", + "description": "Рабочие инструменты пользователя для фильтрации и обработки сообщений.", + "groups": [ + { + "id": "message-filtering", + "number": "1.1", + "title": "Управление фильтрацией сообщений", + "description": "Правила, задания, события и статистика фильтрации.", + "items": [ + { "number": "1.1.1", "title": "Редактирование правил", "type": "rules", "tags": ["правила", "фильтрация", "редактор"], "url": "#" }, + { "number": "1.1.2", "title": "Управление заданиями", "type": "redmine", "tags": ["redmine", "задачи", "задания"], "url": "#" }, + { "number": "1.1.3", "title": "Применение правил", "type": "rules", "tags": ["правила", "применение", "фильтрация"], "url": "#" }, + { "number": "1.1.4", "title": "Просмотр статистики", "type": "grafana", "tags": ["grafana", "статистика", "дашборд"], "url": "#" }, + { "number": "1.1.5", "title": "Управление событиями фильтрации", "type": "grafana", "tags": ["grafana", "события", "алерты"], "url": "#" }, + { "number": "1.1.6", "title": "Просмотр журнала событий фильтрации", "type": "grafana", "tags": ["grafana", "журнал", "события"], "url": "#" } + ] + }, + { + "id": "message-processing", + "number": "1.2", + "title": "Обработка сообщений", + "description": "Просмотр, оформление и доступ к хранилищам сообщений.", + "items": [ + { "number": "1.2.1", "title": "Просмотр и оформление", "type": "messages", "tags": ["сообщения", "просмотр", "оформление"], "url": "#" }, + { "number": "1.2.2", "title": "Прямой доступ", "type": "ceph", "tags": ["ceph", "доступ", "объекты"], "url": "#" }, + { "number": "1.2.3", "title": "S3 Хранилище", "type": "ceph", "tags": ["ceph", "s3", "хранилище"], "url": "#" } + ] + } + ] + }, + { + "id": "admin", + "number": "2", + "title": "Интерфейс администратора", + "description": "Администрирование сервисов, кластеров, мониторинга и аппаратной части.", + "groups": [ + { + "id": "service-monitoring", + "number": "2.1", + "title": "Управление и мониторинг сервисов", + "description": "Кластер, модули, события мониторинга и журналы ошибок.", + "items": [ + { "number": "2.1.1", "title": "Управление кластером", "type": "kubernetes", "tags": ["kubernetes", "кластер", "k8s"], "url": "#" }, + { "number": "2.1.2", "title": "Мониторинг состояния модулей", "type": "grafana", "tags": ["grafana", "модули", "мониторинг"], "url": "#" }, + { "number": "2.1.3", "title": "Управление событиями мониторинга", "type": "grafana", "tags": ["grafana", "события", "алерты"], "url": "#" }, + { "number": "2.1.4", "title": "Просмотр журнала событий мониторинга", "type": "grafana", "tags": ["grafana", "журнал", "мониторинг"], "url": "#" }, + { "number": "2.1.5", "title": "Просмотр журналов ошибок", "type": "opensearch", "tags": ["opensearch", "ошибки", "логи"], "url": "#" } + ] + }, + { + "id": "service-configuration", + "number": "2.2", + "title": "Настройка сервисов", + "description": "Пользователи, очереди, xPath, модули и конфигурации.", + "items": [ + { "number": "2.2.1", "title": "Управление пользователями и очередями", "type": "rabbitmq", "tags": ["пользователи", "очереди", "rabbitmq"], "url": "#" }, + { "number": "2.2.2", "title": "Отладка xPath-запросов", "type": "xpath", "tags": ["xpath", "отладка", "запросы"], "url": "#" }, + { "number": "2.2.3", "title": "Конфигурирование модулей", "type": "config", "tags": ["конфигурация", "модули", "настройки"], "url": "#" }, + { "number": "2.2.4", "title": "Просмотр журнала конфигурирования", "type": "gitea", "tags": ["gitea", "журнал", "конфигурация"], "url": "#" }, + { "number": "2.2.5", "title": "Прямой доступ к конфигурации", "type": "gitea", "tags": ["gitea", "конфигурация", "git"], "url": "#" } + ] + }, + { + "id": "hardware", + "number": "2.3", + "title": "Управление и мониторинг аппаратной части", + "description": "Сенсоры, ресурсы и прямые панели серверов.", + "items": [ + { "number": "2.3.1", "title": "Мониторинг показаний сенсоров", "type": "grafana", "tags": ["grafana", "сенсоры", "железо"], "url": "#" }, + { "number": "2.3.2", "title": "Мониторинг потребления ресурсов", "type": "grafana", "tags": ["grafana", "ресурсы", "метрики"], "url": "#" }, + { "number": "2.3.3", "title": "СЕРВЕР01", "type": "ipmi", "tags": ["ipmi", "сервер01", "железо"], "url": "#" }, + { "number": "2.3.4", "title": "СЕРВЕР02", "type": "ipmi", "tags": ["ipmi", "сервер02", "железо"], "url": "#" }, + { "number": "2.3.5", "title": "СЕРВЕР03", "type": "ipmi", "tags": ["ipmi", "сервер03", "железо"], "url": "#" }, + { "number": "2.3.6", "title": "СЕРВЕР04", "type": "ipmi", "tags": ["ipmi", "сервер04", "железо"], "url": "#" }, + { "number": "2.3.7", "title": "СЕРВЕР05", "type": "ipmi", "tags": ["ipmi", "сервер05", "железо"], "url": "#" } + ] + }, + { + "id": "service-panels", + "number": "2.4", + "title": "Настройка отдельных сервисов", + "description": "Панели управления инфраструктурными сервисами.", + "items": [ + { "number": "2.4.1", "title": "Панель управления LDAP Account Manager", "type": "ldap", "tags": ["ldap", "accounts", "пользователи"], "url": "#" }, + { "number": "2.4.2", "title": "Панель управления кластера RabbitMQ", "type": "rabbitmq", "tags": ["rabbitmq", "кластер", "очереди"], "url": "#" }, + { "number": "2.4.3", "title": "Панель управления кластера Ceph", "type": "ceph", "tags": ["ceph", "кластер", "storage"], "url": "#" }, + { "number": "2.4.4", "title": "Панель управления Prometheus", "type": "prometheus", "tags": ["prometheus", "метрики", "мониторинг"], "url": "#" }, + { "number": "2.4.5", "title": "Панель управления XMPP-сервера", "type": "xmpp", "tags": ["xmpp", "чат", "сервер"], "url": "#" }, + { "number": "2.4.6", "title": "Панель управления PostgreSQL", "type": "postgresql", "tags": ["postgresql", "база", "db"], "url": "#" }, + { "number": "2.4.7", "title": "Подключение к БД", "type": "cloudbeaver", "tags": ["cloudbeaver", "database", "sql"], "url": "#" } + ] + }, + { + "id": "packages", + "number": "2.5", + "title": "Образы и пакеты", + "description": "Реестры образов и deb-пакетов.", + "items": [ + { "number": "2.5.1", "title": "Реестр docker-образов", "type": "gitea", "tags": ["gitea", "docker", "registry"], "url": "#" }, + { "number": "2.5.2", "title": "Репозиторий deb-пакетов", "type": "gitea", "tags": ["gitea", "deb", "packages"], "url": "#" } + ] + } + ] + } + ], + "serviceMeta": {} +} diff --git a/script.js b/script.js index eae591a..7e2f450 100644 --- a/script.js +++ b/script.js @@ -10,135 +10,17 @@ const STORAGE_KEY = "ecosystem-panel-preferences"; const COLLAPSE_STORAGE_KEY = "ecosystem-panel-collapse-state"; // ============================================================================ -// Navigation data model +// Panel configuration (settings.json) // ============================================================================ -// Меняй навигацию здесь: разделы -> подразделы -> элементы. -// url можно заменить на реальные адреса, tags помогают поиску. -// -// Структура: -// - section: крупный раздел панели; -// - group: подраздел внутри section; -// - item: конечный кликабельный элемент. -// -// Каждый item имеет: -// - number: человекочитаемый номер; -// - title: название карточки; -// - type: ключ из serviceMeta, который задает иконку и тип сервиса; -// - tags: поисковые теги, которые также можно показывать в дереве; -// - url: ссылка перехода. По умолчанию "#", чтобы заглушки не дергали страницу. -const ecosystemSections = [ - { - id: "user", - number: "1", - title: "Интерфейс пользователя", - description: "Рабочие инструменты пользователя для фильтрации и обработки сообщений.", - groups: [ - { - id: "message-filtering", - number: "1.1", - title: "Управление фильтрацией сообщений", - description: "Правила, задания, события и статистика фильтрации.", - items: [ - createItem("1.1.1", "Редактирование правил", "rules", ["правила", "фильтрация", "редактор"]), - createItem("1.1.2", "Управление заданиями", "redmine", ["redmine", "задачи", "задания"]), - createItem("1.1.3", "Применение правил", "rules", ["правила", "применение", "фильтрация"]), - createItem("1.1.4", "Просмотр статистики", "grafana", ["grafana", "статистика", "дашборд"]), - createItem("1.1.5", "Управление событиями фильтрации", "grafana", ["grafana", "события", "алерты"]), - createItem("1.1.6", "Просмотр журнала событий фильтрации", "grafana", ["grafana", "журнал", "события"]), - ], - }, - { - id: "message-processing", - number: "1.2", - title: "Обработка сообщений", - description: "Просмотр, оформление и доступ к хранилищам сообщений.", - items: [ - createItem("1.2.1", "Просмотр и оформление", "messages", ["сообщения", "просмотр", "оформление"]), - createItem("1.2.2", "Прямой доступ", "ceph", ["ceph", "доступ", "объекты"]), - createItem("1.2.3", "S3 Хранилище", "ceph", ["ceph", "s3", "хранилище"]), - ], - }, - ], - }, - { - id: "admin", - number: "2", - title: "Интерфейс администратора", - description: "Администрирование сервисов, кластеров, мониторинга и аппаратной части.", - groups: [ - { - id: "service-monitoring", - number: "2.1", - title: "Управление и мониторинг сервисов", - description: "Кластер, модули, события мониторинга и журналы ошибок.", - items: [ - createItem("2.1.1", "Управление кластером", "kubernetes", ["kubernetes", "кластер", "k8s"]), - createItem("2.1.2", "Мониторинг состояния модулей", "grafana", ["grafana", "модули", "мониторинг"]), - createItem("2.1.3", "Управление событиями мониторинга", "grafana", ["grafana", "события", "алерты"]), - createItem("2.1.4", "Просмотр журнала событий мониторинга", "grafana", ["grafana", "журнал", "мониторинг"]), - createItem("2.1.5", "Просмотр журналов ошибок", "opensearch", ["opensearch", "ошибки", "логи"]), - ], - }, - { - id: "service-configuration", - number: "2.2", - title: "Настройка сервисов", - description: "Пользователи, очереди, xPath, модули и конфигурации.", - items: [ - createItem("2.2.1", "Управление пользователями и очередями", "rabbitmq", ["пользователи", "очереди", "rabbitmq"]), - createItem("2.2.2", "Отладка xPath-запросов", "xpath", ["xpath", "отладка", "запросы"]), - createItem("2.2.3", "Конфигурирование модулей", "config", ["конфигурация", "модули", "настройки"]), - createItem("2.2.4", "Просмотр журнала конфигурирования", "gitea", ["gitea", "журнал", "конфигурация"]), - createItem("2.2.5", "Прямой доступ к конфигурации", "gitea", ["gitea", "конфигурация", "git"]), - ], - }, - { - id: "hardware", - number: "2.3", - title: "Управление и мониторинг аппаратной части", - description: "Сенсоры, ресурсы и прямые панели серверов.", - items: [ - createItem("2.3.1", "Мониторинг показаний сенсоров", "grafana", ["grafana", "сенсоры", "железо"]), - createItem("2.3.2", "Мониторинг потребления ресурсов", "grafana", ["grafana", "ресурсы", "метрики"]), - createItem("2.3.3", "СЕРВЕР01", "ipmi", ["ipmi", "сервер01", "железо"]), - createItem("2.3.4", "СЕРВЕР02", "ipmi", ["ipmi", "сервер02", "железо"]), - createItem("2.3.5", "СЕРВЕР03", "ipmi", ["ipmi", "сервер03", "железо"]), - createItem("2.3.6", "СЕРВЕР04", "ipmi", ["ipmi", "сервер04", "железо"]), - createItem("2.3.7", "СЕРВЕР05", "ipmi", ["ipmi", "сервер05", "железо"]), - ], - }, - { - id: "service-panels", - number: "2.4", - title: "Настройка отдельных сервисов", - description: "Панели управления инфраструктурными сервисами.", - items: [ - createItem("2.4.1", "Панель управления LDAP Account Manager", "ldap", ["ldap", "accounts", "пользователи"]), - createItem("2.4.2", "Панель управления кластера RabbitMQ", "rabbitmq", ["rabbitmq", "кластер", "очереди"]), - createItem("2.4.3", "Панель управления кластера Ceph", "ceph", ["ceph", "кластер", "storage"]), - createItem("2.4.4", "Панель управления Prometheus", "prometheus", ["prometheus", "метрики", "мониторинг"]), - createItem("2.4.5", "Панель управления XMPP-сервера", "xmpp", ["xmpp", "чат", "сервер"]), - createItem("2.4.6", "Панель управления PostgreSQL", "postgresql", ["postgresql", "база", "db"]), - createItem("2.4.7", "Подключение к БД", "cloudbeaver", ["cloudbeaver", "database", "sql"]), - ], - }, - { - id: "packages", - number: "2.5", - title: "Образы и пакеты", - description: "Реестры образов и deb-пакетов.", - items: [ - createItem("2.5.1", "Реестр docker-образов", "gitea", ["gitea", "docker", "registry"]), - createItem("2.5.2", "Репозиторий deb-пакетов", "gitea", ["gitea", "deb", "packages"]), - ], - }, - ], - }, -]; +// settings.json задает источник данных панели: +// - dataUrl: URL или путь к JSON (fetch), например "./panel-data.json" +// - data: inline-объект с той же структурой, если dataUrl пустой +const SETTINGS_PATH = "./settings.json"; -// Метаданные типов сервисов. -// item.type ссылается на ключ здесь: так карточки получают label и icon. -const serviceMeta = { +let ecosystemSections = []; +let panelTitle = "Панель экосистемы"; + +const defaultServiceMeta = { ceph: { label: "Ceph", icon: "assets/ceph.svg" }, cloudbeaver: { label: "CloudBeaver", icon: "assets/database.svg" }, config: { label: "Config", icon: "assets/config.svg" }, @@ -158,6 +40,8 @@ const serviceMeta = { xmpp: { label: "XMPP", icon: "assets/xmpp.svg" }, }; +let serviceMeta = { ...defaultServiceMeta }; + // ============================================================================ // Default UI preferences // ============================================================================ @@ -210,16 +94,31 @@ const showTreeTagsInput = document.querySelector("[data-show-tree-tags]"); const treeTwoColumnInput = document.querySelector("[data-tree-two-column]"); const autoCollapseSearchInput = document.querySelector("[data-auto-collapse-search]"); const emptyTemplate = document.querySelector("#empty-template"); +const configErrorTemplate = document.querySelector("#config-error-template"); +const pageTitleElement = document.querySelector("#page-title"); // ============================================================================ // Initial render // ============================================================================ -// Сначала строим дерево, затем применяем настройки, затем рендерим каталог. -// Такой порядок нужен, потому что applyPreferences выставляет data-атрибуты -// и CSS-переменные, от которых зависит внешний вид уже созданного DOM. -renderNavigationTree(); -applyPreferences(preferences); -renderCatalog(); +bootstrap(); + +async function bootstrap() { + try { + const panelData = await loadPanelConfig(); + ecosystemSections = panelData.sections; + serviceMeta = panelData.serviceMeta; + panelTitle = panelData.title; + pageTitleElement.textContent = panelTitle; + document.title = panelTitle; + } catch (error) { + showConfigError(error); + return; + } + + renderNavigationTree(); + applyPreferences(preferences); + renderCatalog(); +} // ============================================================================ // Event bindings @@ -388,10 +287,114 @@ sidebarResizer.addEventListener("pointerdown", (event) => { sidebarResizer.addEventListener("pointercancel", handlePointerUp); }); +// ============================================================================ +// Panel config loading +// ============================================================================ +async function loadPanelConfig(settingsPath = SETTINGS_PATH) { + const settings = await fetchJson(settingsPath); + const dataUrl = typeof settings.dataUrl === "string" ? settings.dataUrl.trim() : ""; + + let panelSource = settings.data; + if (dataUrl) { + panelSource = await fetchJson(dataUrl); + } + + if (!panelSource || typeof panelSource !== "object") { + throw new Error("settings.json должен содержать data или dataUrl"); + } + + return normalizePanelData(panelSource); +} + +async function fetchJson(url) { + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Не удалось загрузить ${url}: HTTP ${response.status}`); + } + + return response.json(); +} + +function normalizePanelData(raw) { + const sections = Array.isArray(raw.sections) ? raw.sections.map(normalizeSection) : []; + + if (!sections.length) { + throw new Error("В данных панели нет sections"); + } + + return { + title: typeof raw.title === "string" && raw.title.trim() ? raw.title.trim() : "Панель экосистемы", + sections, + serviceMeta: { + ...defaultServiceMeta, + ...(raw.serviceMeta && typeof raw.serviceMeta === "object" ? raw.serviceMeta : {}), + }, + }; +} + +function normalizeSection(section) { + if (!section?.id || !section?.title) { + throw new Error("Каждый section должен содержать id и title"); + } + + return { + id: String(section.id), + number: String(section.number ?? ""), + title: String(section.title), + description: String(section.description ?? ""), + groups: Array.isArray(section.groups) ? section.groups.map(normalizeGroup) : [], + }; +} + +function normalizeGroup(group) { + if (!group?.id || !group?.title) { + throw new Error("Каждый group должен содержать id и title"); + } + + return { + id: String(group.id), + number: String(group.number ?? ""), + title: String(group.title), + description: String(group.description ?? ""), + items: Array.isArray(group.items) ? group.items.map(normalizeItemFromConfig) : [], + }; +} + +function normalizeItemFromConfig(item) { + if (!item?.title || !item?.type) { + throw new Error("Каждый item должен содержать title и type"); + } + + return { + number: String(item.number ?? ""), + title: String(item.title), + type: String(item.type), + tags: Array.isArray(item.tags) ? item.tags.map(String) : [], + url: typeof item.url === "string" && item.url.trim() ? item.url.trim() : "#", + }; +} + +function showConfigError(error) { + const message = error instanceof Error ? error.message : String(error); + navTree.replaceChildren(); + catalog.replaceChildren(); + + if (configErrorTemplate) { + const node = configErrorTemplate.content.cloneNode(true); + node.querySelector("[data-config-error-message]").textContent = + `${message}. Проверь settings.json и доступность dataUrl.`; + catalog.append(node); + return; + } + + catalog.textContent = message; +} + // ============================================================================ // Data helpers // ============================================================================ -// Упрощает запись item-объектов в ecosystemSections. +// Упрощает запись item-объектов при ручной правке JS (legacy helper). function createItem(number, title, type, tags = [], url = "#") { return { number, title, type, tags, url }; } diff --git a/settings.json b/settings.json new file mode 100644 index 0000000..342d127 --- /dev/null +++ b/settings.json @@ -0,0 +1,4 @@ +{ + "dataUrl": "./panel-data.json", + "data": null +}