Хаб это два в одном: сайт https://openpalliative.github.io/ и общая платформа для всех шести сайтов проекта. Платформа живёт в папке opsite/, пять репо инструментов держат её копию
| Репо | Адрес на Pages | Порт локально | Роль |
|---|---|---|---|
| openpalliative.github.io | / |
8700 | хаб и платформа opsite |
| pal-norms | /pal-norms/ |
8701 | производитель: нормы, сроки, календарь |
| pal-devices-data | /pal-devices-data/ |
8702 | производитель: изделия, потребитель норм |
| obrashchenie-kit | /obrashchenie-kit/ |
8703 | потребитель: заявления и жалобы |
| home-care-kit | /home-care-kit/ |
8704 | потребитель: листы для дома |
| pal-directory | /pal-directory/ |
8705 | производитель: регионы и номера |
Все сайты статические. Сервера, базы, аналитики и запросов к чужим адресам в рантайме нет. Каждый репо собирается сам и публикуется своим pages.yml на свой путь. Хаб публикуется в корень и не содержит папок инструментов: GitHub Pages отдаёт /pal-norms/ из репо pal-norms
Ссылки внутри сайта относительные, поэтому тот же _site/ работает на Pages, на зеркале и под любым префиксом. Ссылки в соседние репо строит xurl() от корня. Исключение 404.html: в нём пути от site.base, потому что Pages отдаёт его на любом вложенном адресе
opsite это сборщик, проверки и общий UI. Зависимости сборки: jinja2, pyyaml, jsonschema, markdown-it-py, всё через uv. Node нужен только для тестов
| Команда | Что делает |
|---|---|
uv run python -m opsite build |
собирает _site/ из content/, templates/, data/, vendor/ и site_hooks.py репо. --org-root задаёт префикс ссылок в соседние репо |
uv run python -m opsite check |
линт текста в исходниках и в собранном HTML, CSP, ссылки, сеть (ни одного внешнего адреса в рантайме), бюджеты первой загрузки, SEO, дрейф копии opsite/, свежесть снимков, эталонные страницы, MANIFEST.sha256. --ci пишет аннотации GitHub, --org ../_site проверяет межрепные ссылки в общей сборке |
uv run python -m opsite lint |
только линт исходников |
uv run python -m opsite serve |
отдаёт _site/ по тому же пути, что на Pages, на порту из site.yaml. --dir отдаёт готовую папку как есть |
uv run python -m opsite sync-data |
забирает снимки соседей в vendor/, см. раздел 4 |
uv run python -m opsite manifest |
только в хабе: копирует content/now.yaml в opsite/now.yaml, собирает opsite/situations.json из content/situations.yaml, пишет ui/js/tools.js, при изменениях поднимает opsite/VERSION и переписывает opsite/MANIFEST.sha256 |
opsite/templates/op/: каркас страницы и макросы (op.now,op.draft,op.region,op.row,op.steps,op.date_fieldи другие)opsite/ui/:css/(токены, база, компоненты, печать),js/(модули без сборки),fonts/(Onest и OFL),icons/(Lucide и ISC),brand/(знак, иконки сайта, картинки Open Graph)opsite/schema/: JSON Schema дляsite.yaml,deps.yaml, конверта датасета,index.json,entrypoints,now,situationsи фронтматтера страницopsite/lint/: правила текста.words.yaml(как не пишем и как пишем),stoplist.yaml(домены, на которые не ссылаемся),territories.yaml(территории, данных по которым нет),drugs.txt(названия препаратов, которых нет на листах). Ошибки: длинное тире, эмодзи, упоминания генераторов текста, номер акта литералом вне цитаты, доза, препарат, домен из стоп-листа, территория, инлайн-скрипт, атрибутstyle, обработчикon*=opsite/tools.yaml: пять инструментов (репо, префикс, название, иконка). Из него строятся подвал,/tools/и ключи, которые чистит «Удалить всё»opsite/now.yamlиopsite/situations.json: копии изcontent/хаба, чтобы шапка иop.nowво всех репо были одинаковымиopsite/tests/: общий спек Playwrightpages.spec.jsи фикстуры (сторож сети, axe, нарушения CSP, ошибки консоли),test_platform.pyopsite/VERSIONиopsite/MANIFEST.sha256: версия платформы и sha256 каждого файла
- Правка
opsite/делается только в хабе - В хабе
uv run python -m opsite manifest: новые хеши, при изменениях новаяVERSION sh scripts/sync-ui-allпроходит по пяти соседним папкам и в каждой запускаетscripts/sync-ui. В одном репо:sh scripts/sync-ui, хаб ищется в../openpalliative.github.ioили вOP_HUBsync-uiкопируетopsite/черезrsync --deleteи сверяет копию сMANIFEST.sha256. Если у копии и у хаба однаVERSION, а хеши разные, копирование отказывается: значит, кто-то правил копию или забылmanifestв хабеopsite checkв репо инструмента предупреждает о дрейфе копии
В репо инструментов opsite/ не правят и не присылают pull request с изменениями в нём
Производители публикуют наборы в /<repo>/data/v1/: index.json со списком наборов, версией, датой данных и sha256, рядом каждый набор в конверте (items, source, data_date, verified у записей) и его схема
Потребитель описывает, что ему нужно, в deps.yaml: репо, мажорная версия и наборы. opsite sync-data:
--from local(по умолчанию) читает../<repo>/_site/data/v1/,--from pagesчитаетhttps://openpalliative.github.io/<repo>/data/v1/- мажор из
index.jsonдолжен совпасть сmajorвdeps.yaml, иначе ошибка - каждый файл сверяется с sha256 из
index.jsonи проверяется по схеме - если содержимое не изменилось, ничего не трогает. Иначе пишет файлы в
vendor/<repo>/v1/и обновляетdeps.yaml(version,data_date,content_sha256,synced_at) - если источник недоступен, оставляет старый снимок и предупреждает. С
--strictвыходит с ошибкой
Хаб берёт entrypoints у всех пяти инструментов и у pal-directory ещё regions, region-lines и federal-lines. В рантайме сайт читает только свои файлы и vendor/, к соседям и к Pages не ходит
В CI это делает sync-data.yml раз в неделю: sync-data --from pages, сборка, check --ci и pull request sync/data с меткой review:data, на ветке которого запускается checks.yml. Хаб синхронизируется последним, когда сайты соседей уже опубликованы
content/situations.yaml: 10 ситуаций (night-pain,discharge,refused,device,conclusion,dying,who-to-call,emergency-sheet,turning-18,prepare). У каждой заголовок, подсказка, иконка, флагurgent, роли номеров региона и три блока шагов: «Сегодня», «Потом», «Если не получилось»- шаг ссылается на страницу хаба (
repo: hub) или на вход инструмента (repoиentry).site_hooks.pyнаходит вход вvendor/<repo>/v1/entrypoints.jsonи берёт из него путь и отметки (maturity,offline,printable). Если входа нет, шаг показан как «Готовим», без ссылки - к ссылке в инструмент добавляется
?back=<id ситуации>, чтобы инструмент мог вернуть человека в его ситуацию site_hooks.pages()отдаёт сборщику страницыs/<id>/по шаблонуtemplates/situation.html.j2site_hooks.data_sources()собирает таблицу «откуда данные» изvendor/*/v1/index.json: набор, версия, дата, число проверенных и непроверенных записей
- регионы и номера берутся только из снимков pal-directory в
vendor/pal-directory/v1/. В pal-directory они взяты из таблицы горячих линий Росздравнадзора, у каждого номера источник, дата иverified - при сборке хаб пишет
region-index.json(название, столица, сокращения для поиска региона) иregion-lines/<iso>.jsonна каждый регион. Записи соhidden: trueиorigin: syntheticв эти файлы не попадают никогда - выбранный регион хранится в
localStorageпод ключомop:regionи общий для всех сайтов, потому что origin один. Без региона показаны номера, которые работают по всей стране, изcontent/now.yaml - проверенные номера показываются крупно, непроверенные обычным размером со статусом «Не проверено». Если проверенных нет, хаб пишет об этом и предлагает федеральные номера. Всегда есть ссылка на карточку региона в pal-directory
Переменная окружения сборки pal-directory, в CI vars.PD_DATA_MODE:
real: реальные номера из таблицы Росздравнадзора с источником и датой. Это режим по умолчанию и режим для Pagesdemo: вместоregion-linesсинтетика с номерами вида8 (000) ...безtel:и плашка «Пример на выдуманных данных». Только для тестов
Хаб от режима не зависит: синтетические строки он отбрасывает при сборке, а без номеров показывает ссылку на карточку региона
Чтобы несколько человек могли работать в одном репо, не мешая друг другу:
| Зона | Что |
|---|---|
| данные | data/, pipeline/, raw/, тесты данных |
| страницы | templates/, assets/, content/, lib/, e2e и скриншоты |
| инфраструктура | .github/, README.md, docs/, лицензии, файлы сообщества, scripts/ для проверок |
| общий UI | opsite/, только в хабе |
sh scripts/init-repo REPO PREFIX PORT "Название" "Что делает" [--pwa] [--draft legal|medical|both|none] копирует scripts/repo-template/ в соседнюю папку и подставляет значения. Существующие файлы не трогает. В шаблоне:
site.yaml,pyproject.toml,package.json,playwright.config.js,deps.yaml, версии Node и Python.github/workflows/:checks.yml,pages.yml,release.yml,sync-data.yml,watch.yml. Ненужные удалите:watch.ymlнужен только производителям,sync-data.ymlтолько потребителям.github/: шаблоны issue и pull request,CODEOWNERS,dependabot.ymlCONTRIBUTING.md,SECURITY.md,CODE_OF_CONDUCT.mdscripts/:gate.sh,sync-ui,sync-data,serve-one,e2e-browsers,tools/install- тесты: unit, e2e,
pages.json
В конце init-repo запускает sync-ui, так что копия opsite/ появляется сразу
Все экшены закреплены по 40-символьному SHA с комментарием версии. У workflow permissions: {}, права выдаются на уровне job по минимуму. У каждого job есть timeout-minutes. checkout везде с persist-credentials: false, pull request создаёт peter-evans/create-pull-request своим токеном. Кэш uv и pnpm включён только в checks.yml, там, где ничего не публикуется
| Workflow | Когда | Что |
|---|---|---|
checks.yml |
pull request, push в main, вручную |
actionlint, pytest, сборка, check --ci, unit, e2e в chromium, firefox и webkit, скриншоты. Повторный push в тот же pull request отменяет прошлый прогон |
pages.yml |
push в main, вручную |
build с правом только на чтение собирает и проверяет _site/, deploy с pages: write и id-token: write публикует |
sync-data.yml |
вторник, вручную | новые снимки соседей, pull request с меткой review:data, запуск checks.yml на его ветке, в конце PUT .../workflows/sync-data.yml/enable, чтобы GitHub не выключил расписание через 60 дней без активности |
watch.yml |
понедельник, вручную, только у производителей | наблюдатель читает официальные источники. В шаблоне служебные снимки уходят pull request с меткой facts, остальное issue, у производителей детали в их ARCHITECTURE.md. Раннер задаётся vars.WATCH_RUNNER (JSON-строка), по умолчанию ubuntu-24.04 |
release.yml |
тег v*, у инструментов |
ворота, архив сайта и отдельный архив данных, SHA256SUMS, релиз на GitHub |
Проверка локально: sh scripts/actionlint (actionlint 1.7.12 и shellcheck 0.10.0, бинарники ставит scripts/tools/install с проверкой sha256) и uvx zizmor --offline .github/workflows
pnpm test:unit: модулиopsite/ui/jspnpm test:e2e: Playwright в headless chromium поtests/e2e/и общемуopsite/tests/pages.spec.js. Проектыdesktop,mobile,nojspnpm test:e2e:browsers: то же в chromium, firefox и webkit. Браузер выбирает переменнаяOP_BROWSER, отчёты в_reports/pw-<браузер>/pnpm shots: скриншоты в_reports/shots/pnpm test:e2e:all:playwright.all.config.jsподнимает общую сборку на 8080 (scripts/serve-all --no-build) и обходитsitemap.xmlвсех шести сайтов: ответ страницы, одинh1, нет горизонтальной прокрутки, axe, сторож сети, ссылки внутри сайта и в соседние репо, скриншоты в_reports/shots/all/sh scripts/gate.sh: все ворота подряд, итог в_reports/gate.json