From 822e072418e725fd46a6bf87f9d2cff95f0460ce Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 24 Aug 2026 14:32:09 +0600 Subject: [PATCH] [2026-08-24] eagle: personal/plans/ohotnichy-bilety-interaktiv.md personal/projects/personal-os/eagle-dashboard.md --- personal/plans/ohotnichy-bilety-interaktiv.md | 80 +++++++++++++++++++ .../projects/personal-os/eagle-dashboard.md | 15 ++++ 2 files changed, 95 insertions(+) create mode 100644 personal/plans/ohotnichy-bilety-interaktiv.md diff --git a/personal/plans/ohotnichy-bilety-interaktiv.md b/personal/plans/ohotnichy-bilety-interaktiv.md new file mode 100644 index 00000000..953df556 --- /dev/null +++ b/personal/plans/ohotnichy-bilety-interaktiv.md @@ -0,0 +1,80 @@ +# Интерактивные билеты охотника (a1-tir.ru/bilety) + +Проект: скачать страницу билетов для экзамена на гражданское оружие и превратить в интерактивный тренажёр. + +**Возможности:** номер правильного ответа скрыт · ответы кликабельны · выбранный неправильный → красный, правильный → зелёный. + +**Файлы (исходники в `~/Downloads/`):** +- `bilety.html` — скачанная + расширенная страница (инъекция CSS/JS + метаданные) +- `bilety.js` — enhancer: парсер вопросов + click-логика +- `bilety.css` — стили подсветки (зелёный/красный) +- `a1-tir_bilety_raw.html` — оригинальная скачанная копия (667 КБ) + +--- + +## Статус +- ✅ Страница скачана и переработана в интерактив. +- ⏸️ **Доставка в Eagle Dashboard Pages не завершена** — упирается в права: `/Library/WebServer/Documents/` root-owned, `sudo cp` требует пароль из неинтерактивного шелла. Нужно либо получить sudo-пароль от Alex, либо он запустит вручную: + ```bash + sudo cp ~/Downloads/bilety.html ~/Downloads/bilety.js ~/Downloads/bilety.css /Library/WebServer/Documents/ + ``` + После копирования страница появится в `http://localhost:8880/?tab=pages` под именем `bilety` (title/purpose/contains уже заполнены). Рестарт дашборда не нужен. + +--- + +## Источник и структура (Tilda) + +Страница — Tilda-лендинг, весь исходный HTML на одной строке (`a1-tir_bilety_raw.html`, строка 228 содержит контент). + +Экзамен разложен по **4 блокам** `data-record-type="106"` (Tilda text-блок `div.field-text.t-text`): +- `rec465410787` — «Правовая подготовка», вопросы 1.1–1.49 +- `rec1120437831` — без заголовка (продолжение правовой), 1.50–1.93 +- `rec1120438216` — «Огневая подготовка», 2.1–2.41 +- `rec465410788` — без заголовка (правила охоты), 58–103 · **вопросы из нескольких ``** +- Итого ~180 вопросов. + +### Структура одного вопроса в DOM (проверено по сырым данным) +Каждый блок — один большой div; внутри `childNodes`: +- `
` — разделители (шум) +- `Вопрос` — текст вопроса; может занимать **несколько подряд** `` (напр. вопрос 58); внутри возможен вложенный `` (consultantplus) +- шапка раздела — `

Заголовок

` (strong вложен в p → не ловится как вопрос) +- текстовые узлы ответов: `"1. Текст"`, `"2. ..."`, `"3. ..."` (бывают ведущие пробелы) +- `N` — **номер правильного ответа** (чистое число) +- примечание — `Примечание: …` (НЕ номер, сохранить) +- шум: ` `, ` `, пустой `` с `` + +Контентных ``/`` в блоках вопросов нет — только текст. + +--- + +## Решение — чисто клиентский JS (без библиотек/backend) + +`bilety.js` на `DOMContentLoaded` обрабатывает каждый `[data-record-type="106"] .t-text`: + +**Парсер `parseQuiz` (конечный автомат по childNodes):** +- `` с непустым текстом, не ` `: если у текущего вопроса ещё нет ответов и фаза `question` → **мержим** текст (многострочный вопрос); иначе → **новый вопрос**. +- текстовый узел, матчащий `^\d+[\.\s:]` и при отсутствии заданного `correct` → ответ `{n, text}` (фаза → `answers`); иначе — продолжение последнего ответа. +- `` с чистым числом → `question.correct = N` (только в фазе `answers`, чтобы не спутать с «Примечание»); с текстом → `question.note = innerHTML` (сохранить); пустой/` ` → шум. +- `

` при `cur === null` и нет вопросов → `headerHTML` (заголовок раздела сохраняем). +- фильтр: оставляем только вопросы с `correct !== null`. + +**Рендер `renderQuestion`:** карточка `.whale-q` → `.whale-q-text` (вопрос, `textContent`, жирный) + `.whale-answers` с кнопками `.whale-a` (`data-n` = номер ответа) + опционально `.whale-note`. + +**Click-логика:** первый клик блокируется (`data-locked`). Если `n === correct` → класс `correct` (зелёный); иначе класс `wrong` (красный) **и** правильный ответ получает `correct` (чтобы показать верный). Управляется `data-correct` на карточке. + +**Инъекция в страницу:** +- `` перед `` +- `` перед `` +- мета для дашборда в ``: `eagle-test-purpose`, `eagle-test-contains`. + +**Дизайн .whale-a:** карточки-кнопки (flex column), `#f5f5f5`, hover `#ececec`; `.correct`=`#d4edda`/`#28a745`, `.wrong`=`#f8d7da`/`#dc3545` (с `!important`). + +--- + +## Pitfall: локальное открытие `file://` +Страница Tilda использует `sessionStorage` для анимации появления `.t-records` — при открытии локально это работает. Просто открыть `bilety.html` двойным кликом достаточно для проверки функционала. + +--- + +## Связанное +- Доставка в дашборд — механика Pages: `personal/projects/personal-os/eagle-dashboard.md`. diff --git a/personal/projects/personal-os/eagle-dashboard.md b/personal/projects/personal-os/eagle-dashboard.md index 2102dece..8e782794 100644 --- a/personal/projects/personal-os/eagle-dashboard.md +++ b/personal/projects/personal-os/eagle-dashboard.md @@ -149,6 +149,21 @@ PID-file-based — survives Dashboard restarts without crashing. **Pages** — URL input + metadata table for `/Library/WebServer/Documents/*.html` test pages. Each row shows filename, title, purpose, contents, modified date, and opens the page in a new browser tab. +### Как добавить страницу в Pages (2026-08-24, проверено) + +Механика — просто копия `*.html` файла в webroot `/Library/WebServer/Documents/`. Дашборд читает каталог **живьём** через `GET /api/pages` → `_read_page_metadata()` — рестарт дашборда и `launchctl kickstart` **не нужны**. Страница отдаётся по `/local-pages/` (mount `app.mount("/local-pages", StaticFiles(...))`). + +Метаданные парсятся из HTML: +| Поле дашборда | Источник | +|---|---| +| `title` | `...` | +| `purpose` | ``, fallback — `` | +| `contains` | `` | + +**Pitfall — относительные `href`/`src`:** если страница ссылается на свои `.js`/`.css` относительными путями (как `bilety.html` → `bilety.js`, `bilety.css`), эти файлы тоже обязаны лежать рядом в `/Library/WebServer/Documents/`, иначе запрос уйдёт на `/local-pages/bilety.js` и не найдётся. + +**Pitfall — права (блокер):** `/Library/WebServer/Documents/` принадлежит `root:wheel`, права `drwxr-xr-x`. `admin` писать туда **не может** без sudo. NOPASSWD-правил для этого пути в sudoers **нет** (`admin` имеет только `(ALL) ALL` с паролем + спец. pmset/launchctl NOPASSWD). Из неинтерактивного шелла Hermes `sudo cp` встаёт на запрос пароля → **требуется пароль пользователя или ручная команда**: `sudo cp ~/Downloads/bilety.* /Library/WebServer/Documents/`. + **Files** — FileBrowser iframe at `http://localhost:8181`. **Crons** (2-я вкладка) — управление cron jobs из 3 источников: