sdlc-hub/docs/superpowers/plans/2026-10-24-web-modules.md
Andrey Epifancev 52a6c93db8 plan(web-modules): план реализации веба под модули
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 15:38:46 +04:00

33 KiB
Raw Blame History

Веб под модули — план реализации

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: оболочка веба рисует состояние модулей (агент, свежесть, обновление, пакет) по их описанию, навигация сгруппирована по модулям с главной страницей «Обзор», адреса модулей с префиксом модуля, граница ядра и модулей проверяется тестами, дефекты страниц из аудита исправлены.

Architecture: контракт HubModule получает title, status: ModuleStatus (данные) и Summary; ядро (web/src/shell/activity/) держит один опрос GET /agents и один трекер пакетов на все модули (обобщение RefreshProvider модуля jira); блок состояния, «Обзор», верхняя строка узкого экрана — компоненты ядра. Бэкенд добавляет jira_synced_at в проект, module в пакет и batches в ответы POST/PATCH /projects.

Tech Stack: React 19 + TypeScript + Vite, TanStack Query, react-router, Vitest + Testing Library, Playwright; Python 3.13 (uv, pytest).

Spec: openspec/changes/web-modules/ (proposal.md, design.md, specs/web-app, specs/hub-api) — issue jira-hub#98. Аудит и решения — design.md.

Global Constraints

  • Предусловие: ветка перебазирована на main после слияния sdlc-rename (#97): пакет src/sdlc_hub/, бренд «SDLC Hub», ключи sdlc-hub-*, readMigrated в web/src/api/legacyStorage.ts. Без этого план не начинать.
  • Адреса: «Обзор» / (под /app), jira — /jira/board, /jira/issues, /jira/issues/:key, /jira/feed; wiki — /wiki; прежние /board, /issues, /issues/:key, /feed — перенаправление с сохранением search, replace.
  • Тексты (точно): названия модулей «Jira», «Вики»; «Обзор»; агент — «агент на связи» / «агент офлайн» / «обновите агента»; свежесть — «данные на ДД.ММ ЧЧ:ММ · все проекты» / «данные на ДД.ММ ЧЧ:ММ · проект», «обновления ещё не было»; действия — «Обновить из Jira», «Обновить вики»; подсказки значков — «Обновить из Jira — все проекты», «Обновить вики — проект <метка>»; заголовок вкладки «<страница> · <метка> · SDLC Hub»; «Последние пакеты проекта»; «Команды хаба»; «Привязать корень вики»; «нужно целое число».
  • События пакета — прежние тексты refreshText (пакет #N в очереди/выполняется…, выполнен, не выполнен: …, отменён, …: <причина> — опрос остановлен, «<действие>» снова доступна, хаб не отвечает — пакет не поставлен); POLL_MS = 3000, MAX_POLL_ERRORS = 5, DONE_VISIBLE_MS = 10_000; подхват и GET /agents — раз в 60_000 мс.
  • Primary-кнопок обновления нет: значок в строке модуля, variant="secondary" в шапках страниц.
  • Цвет не единственный признак: точка агента и просрочка — с текстом (title/скрытый текст).
  • AURA (скилл aura-design): цвета только из styles/tokens.css, компоненты из web/src/ui/.
  • Зелёный — весь набор: uv run pytest -q, cd web && npm run lint && npm test; перед финальным ревью ещё cd web && npm run e2e.
  • Живой хаб (:8790) и его данные не трогать; e2e — только на тестовом хабе (web/scripts/test-hub.sh, :8791).
  • Язык комментариев, текстов, коммитов — русский; коммиты заканчиваются Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>.

Review Focus

  1. Пакет, поставленный из MCP или автообновлением, во время открытой страницы: строка модуля должна его показать не позже чем через 60 с и не блокировать кнопку обновления — Task 3, picks up batch of any author и foreign batch does not block refresh.
  2. Смена проекта во время идущего пакета вики: строка «Вики» нового проекта не показывает чужой пакет, опрос старого не течёт в новую строку — Task 3, project switch drops project-scoped batch.
  3. Проект без корня вики на всех поверхностях (панель развёрнута/свёрнута, телефон, «Обзор»): нет пустой строки «Вики», нет запроса /wiki/status из оболочки — Task 4 no wiki row without root, Task 5 overview card without root.
  4. Прежний адрес карточки из истории браузера (/app/issues/KEY без проекта): перенаправление и окно поверх списка, закрытие окна возвращает на /jira/issues с текущим проектом — Task 4, legacy card address.
  5. Пересечение классов CSS, которое уже ломало «Вики»: тест-страж должен падать на div.wiki-подобном конфликте и на дубле класса между модулем и ядром — Task 2, css classes do not collide.

Task 1: API для блока состояния

Files:

  • Modify: src/sdlc_hub/modules/jira/project_fields.py (view: jira_synced_at), src/sdlc_hub/core/queue.py (batch_view: module; list_batches(conn, limit=20, project=None, module=None)), src/sdlc_hub/core/api.py (h_batches_get: ?module=, неизвестный — 404 not_found; POST /projects и PATCH /projects/{label}: batches; GET / и GET /links → Location: /app/)
  • Test: tests/test_project_fields.py, tests/test_queue_core.py, tests/test_api.py, tests/test_web_app.py, tests/test_wiki_projects.py

Interfaces:

  • Produces: GET /projects[].jira_synced_at: str | null — finished_at последнего sync_run полного обновления (тот же запрос, что last_sync в modules/jira/mirror.py снимка доски — вынести в функцию last_full_sync(conn) -> dict | None модуля jira и использовать в обоих местах); batch_view(...)["module"]: str | None — registry.module_of(<тип первой команды по seq>).name; POST /projects → {project, batch_id, batches: [{"module": str, "id": int}]}; PATCH → {project, batch_ids, batches}.

  • Step 1: Тесты

    • test_projects_jira_synced_at: до полного обновления GET /projects → у проекта jira_synced_at is None; после завершённого полного refresh с sync_run.finished_at = "2026-10-24T11:07:00" → это значение у каждого проекта.
    • test_batch_module: пакет refresh → module == "jira"; пакет wiki_sync → "wiki"; пакет без команд → None (в list_batches и batch_view).
    • test_batches_module_filter: GET /batches?module=wiki&project=AWPCA → только пакеты wiki проекта; GET /batches?module=nope → 404.
    • test_create_project_batches: POST /projects {label, name, wiki_root: "DOC-001234"} → batches == [{"module": "jira", "id": <refresh>}, {"module": "wiki", "id": <wiki_sync>}], batch_id == <refresh>; PATCH /projects/AWPCA {wiki_root} → batches == [{"module": "wiki", "id": batch_ids[0]}].
    • test_root_redirect (в test_web_app.py, заменить прежний): GET / и GET /links → 302, Location: /app/.
  • Step 2: uv run pytest tests/test_project_fields.py tests/test_queue_core.py tests/test_api.py tests/test_web_app.py tests/test_wiki_projects.py -q — FAIL.

  • Step 3: реализация по Interfaces. В ядре (queue.py, api.py) — без названий модулей (граница tests/test_registry.py): модуль берётся из registry.module_of.

  • Step 4: uv run pytest -q — весь набор PASS (в том числе test_registry.py, эталоны MCP: batch_id в MCP не меняется).

  • Step 5: Commit — feat(web-modules): API — свежесть Jira в проекте, модуль пакета, пакеты нового проекта по модулям.

Task 2: Граница ядра и модулей в вебе

Files:

  • Create: web/src/ui/format.ts (+ format.test.ts), web/src/modules/jira/api/types.ts, web/src/boundary.test.ts
  • Move: web/src/api/closed.ts (+ тест) → web/src/modules/jira/api/closed.ts
  • Modify: web/src/api/types.ts (остаются Batch (+ module: string | null), BatchCommand, BatchStatus, AgentView, CreatedBatch, Project, CreatedProject, UpdatedProject, HubSettings без словарей jira; fact_status удалён везде, в том числе web/src/test/issueFixtures.ts и комментарий modules/jira/issues/card.ts), импорты типов в модулях; modules/jira/board/model/util.ts (fmtStamp переезжает в ui/format.ts), modules/wiki/PageView.tsx, modules/wiki/SyncStatus.tsx; modules/jira/issues/wiki/wiki.css и разметка, использующая className="wiki" в модуле jira → jira-markup; IssueTable.tsx и IssueCard.tsx (даты через fmtDate); web/src/modules/index.ts или modules.test.tsx (уникальность)

Interfaces:

  • Produces: fmtStamp(iso: string | null): string — «ДД.ММ ЧЧ:ММ» (как сейчас в board/model/util.ts); fmtDate(iso: string | null, now?: Date): string — «ДД.ММ», год не текущий — «ДД.ММ.ГГГГ», null — «—»; AgentView = поля GET /agents: {module: string; online: boolean; last_seen_at: string | null; agent_version: number | null; warning: string | null; update_hint: string | null; current_command_id: number | null; release: {version: number} | null}.

  • Step 1: Тесты

    • format.test.ts: fmtDate("2026-10-19", new Date("2026-10-24")) → "19.10"; fmtDate("2025-12-31", new Date("2026-10-24")) → "31.12.2025"; fmtDate(null) → "—"; fmtStamp("2026-10-24T11:07:00") → "24.10 11:07".
    • boundary.test.ts (читает файлы через import.meta.glob(..., { query: "?raw", eager: true })):
      • core does not import modules: файлы src/{shell,ui,api,projects,auth,styles}/**/*.{ts,tsx} (кроме тестов) не импортируют modules/ иначе как modules/index.ts и modules/types.ts;
      • module does not import other module: файл modules/<a>/** не импортирует modules/<b>/;
      • css classes do not collide: множество имён классов (/\.([a-zA-Z_][\w-]*)/g по селекторам, без значений свойств) в modules/<a>/**/*.css не пересекается с modules/<b>/**/*.css и с src/{shell,ui,styles,projects,auth}/**/*.css; сообщение — имя класса и файлы;
      • core has no module terms: в файлах ядра нет fact_status, closed_statuses, kind_labels, link_types, BoardIssue, IssueRow.
    • modules.test.tsx: names unique и nav paths unique (T7.6) — сообщение с дублем; NotFound — ключ ссылки уникален при дубле (T7.5).
  • Step 2: cd web && npx vitest run src/ui/format.test.ts src/boundary.test.ts src/modules/modules.test.tsx — FAIL (пересечение .wiki, импорт wiki → jira, термины в api/types.ts).

  • Step 3: перенос типов и closed.ts, ui/format.ts, jira-markup, даты в таблице и карточке, удаление fact_status.

  • Step 4: cd web && npm run lint && npm test — PASS; на странице «Вики» тестового хаба заголовок h1 — 20 px (проверить в e2e Task 8).

  • Step 5: Commit — refactor(web-modules): граница ядра и модулей в вебе, форматтеры дат в ядре, без fact_status.

Task 3: Контракт состояния модуля и трекер пакетов в ядре

Files:

  • Create: web/src/shell/activity/ActivityProvider.tsx, web/src/shell/activity/activity.ts (контекст и хуки), web/src/shell/activity/events.ts (перенос refreshStatus.ts: ModuleEvent, eventText(e, actionLabel), константы), web/src/shell/activity/activity.test.tsx (перенос и обобщение modules/jira/refresh/refresh.test.tsx), web/src/modules/jira/status.ts, web/src/modules/wiki/status.ts
  • Modify: web/src/modules/types.ts (ModuleStatus, HubModule.title, status, Summary; удалить SidebarStatus, TopbarNotice, useOnProjectCreated), web/src/modules/index.ts, web/src/modules/jira/index.tsx, web/src/modules/wiki/index.tsx, web/src/App.tsx (провайдер ядра), web/src/modules/wiki/api/queries.ts (useWikiSync не нужен оболочке; страница вики берёт действие из трекера)
  • Delete: web/src/modules/jira/refresh/ (RefreshProvider.tsx, refresh.ts, refreshStatus.ts, DataStatus.tsx, SidebarStatus.tsx, их тесты) после переноса тестов; web/src/modules/wiki/SyncStatus.tsx (+ тест)

Interfaces:

  • Consumes: Batch.module, AgentView (Task 2), GET /batches?module=&project=&limit=, jira_synced_at (Task 1).

  • Produces:

    • ModuleStatus — как в design.md («Контракт веб-модуля»): agent?: string; scope: "all" | "project"; freshness(p: Project): {available: boolean; at: string | null; error: string | null}; refresh?: {label: string; start(project: string): Promise<{id: number; estimate_seconds?: number}>; isOwn(b: Batch): boolean}; invalidate(qc: QueryClient, project: string | null): void.
    • useModuleActivity(name: string): {agent: AgentView | null; event: ModuleEvent | null; busy: boolean; running: Batch | null; start(): Promise<void>; track(id: number): void} — busy: постановка в пути или отслеживаемый пакет удовлетворяет refresh.isOwn.
    • useAgents(): AgentView[]; useActivitySummary(): "error" | "busy" | "online" | "offline" (для сводной точки).
    • ActivityProvider({children, project, pollMs?}) — project — текущая метка (из ProjectContext).
    • jira: status = {agent: "jira", scope: "all", freshness: p => ({available: true, at: p.jira_synced_at ?? null, error: null}), refresh: {label: "Обновить из Jira", start: () => api.post("/refresh", {full: true, author: "viewer"}), isOwn: b => b.commands?.[0]?.type === "refresh"}, invalidate: board-все, issues, issue, changes}; wiki: {agent: "wiki", scope: "project", freshness: p => ({available: !!p.wiki_root, at: p.wiki_synced_at ?? null, error: p.wiki_sync_error ?? null}), refresh: {label: "Обновить вики", start: project => api.post("/wiki/sync", {project, author: "viewer"}), isOwn: b => b.commands?.[0]?.type === "wiki_sync"}, invalidate: ["wiki", project] tree/page/search}.
  • Правила трекера: подхват — при монтировании, при смене проекта, раз в 60 с и после start: GET /batches?module=<m>&limit=20 (+&project=<метка> для scope: "project"), первый незавершённый (queued/running, самый старый) — если ничего не отслеживается; при смене проекта отслеживаемый пакет модуля с scope: "project" сбрасывается без события. start: агент офлайн по useAgents() → agentOffline: true в событии; ответ 409 (ApiError.status === 409) — сразу подхват без события ошибки. Если отслеживается чужой пакет (не isOwn), start ставит пакет и переключает отслеживание на него. По завершении пакета — событие, module.status.invalidate(qc, project), перечитывание ["projects"].

  • Step 1: Тесты activity.test.tsx (мок fetch как в прежнем refresh.test.tsx, фейковые таймеры) — перенести все сценарии прежнего файла на модуль jira (M-2, M-3, M-4, T10.2, T4.1, 401, batch-done гаснет, переход между страницами), заменив «подхват viewer+refresh» новыми:

    • picks up batch of any author: GET /batches?module=jira отдаёт пакет автора hub в running → event.kind === "batch", опрос GET /batches/{id};
    • foreign batch does not block refresh: отслеживается пакет jira с commands[0].type = "ensure_fields" → busy === false; start() → один POST /refresh, event.id — новый пакет;
    • project switch drops project-scoped batch: отслеживается wiki_sync проекта AWPCA, project → STOCKS_DRRK → event === null, опрос пакета AWPCA остановлен; jira-пакет не сброшен;
    • wiki 409 tracks running batch: POST /wiki/sync → 409 → подхват идущего пакета, события ошибки нет;
    • agent offline from /agents: GET /agents → wiki online: false; start() для wiki → событие с agentOffline: true;
    • invalidates module data only: завершение пакета wiki инвалидирует ["wiki", "AWPCA", …] и ["projects"], не ["board"];
    • one agents poll: два модуля → один запрос GET /agents за интервал.
  • Step 2: cd web && npx vitest run src/shell/activity — FAIL.

  • Step 3: реализация. Состояние — на модуль (Record<name, {active, event, starting, failures}>), опросы пакетов — useQueries по активным; логика остановки — перенос из RefreshProvider без изменений.

  • Step 4: cd web && npm run lint && npm test — PASS (временные заглушки оболочки, если Task 4 ещё не сделан: Sidebar может рендерить старый блок через useModuleActivity — главное, чтобы набор был зелёным).

  • Step 5: Commit — feat(web-modules): состояние модуля в контракте, трекер пакетов и агенты в ядре.

Task 4: Оболочка — группы, блок состояния, узкий экран, адреса

Files:

  • Create: web/src/shell/ModuleStatusBlock.tsx (+ тест), web/src/shell/useDocumentTitle.ts
  • Modify: web/src/shell/Sidebar.tsx (+ Sidebar.test.tsx), web/src/shell/Layout.tsx, web/src/shell/NotFound.tsx, web/src/shell/shell.css, web/src/modules/jira/index.tsx (пути /jira/…, перенаправления прежних путей), web/src/modules/jira/issues/modal.ts и overlay.tsx (/jira/issues, закрытие окна — с текущим проектом), все ссылки на /board, /issues, /feed внутри модулей
  • Test: web/src/shell/*.test.tsx, web/src/projects/projects.test.tsx

Interfaces:

  • Consumes: useModuleActivity, useAgents, useActivitySummary (Task 3); HubModule.title, status.

  • Produces: NavItem.title?: string (название страницы для вкладки; по умолчанию label); useDocumentTitle(page: string | null, project: string | null); ModuleStatusBlock({project, variant: "expanded" | "collapsed"}); оболочка: пункт «Обзор» (to: "/") — первым, затем группы MODULES с заголовком title; смена проекта вне раздела → /.

  • Step 1: Тесты

    • ModuleStatusBlock.test.tsx: две строки «Jira» и «Вики» с точкой и текстом агента («агент на связи»/«агент офлайн»/«обновите агента», warning в title); «данные на 24.10 11:07 · все проекты» и «данные на 24.10 11:05 · проект»; значок с aria-label «Обновить из Jira — все проекты» и «Обновить вики — проект AWPCA»; jira_synced_at: null → «обновления ещё не было»; wiki_sync_error: "403 Forbidden" → текст ошибки в строке «Вики» (тон ошибки, с текстом); no wiki row without root — проект без wiki_root → одна строка, запросов /wiki/status и /board нет; collapsed — по значку на модуль, подробности в title.
    • Sidebar.test.tsx: заголовки групп «Jira», «Вики»; «Обзор» первым; на 390 px верхняя строка — «Меню», метка проекта, сводная точка (aria-label с состоянием); drawer is modal — фокус в панели, Escape во вложенном меню закрывает только меню (M-1 #73), Escape после — панель, фокус на «Меню»; метка проекта свёрнутой панели целиком в title (M-4).
    • Layout/projects.test.tsx: legacy card address — /issues/AURORA-1?project=AWPCA → /jira/issues/AURORA-1?project=AWPCA, окно поверх списка, закрытие → /jira/issues?project=AWPCA; /board?project=X → /jira/board?project=X; смена проекта на «Вики» у проекта без вики → остаётся «Вики» (страница с формой привязки), на неизвестном адресе → /; заголовок вкладки «Вики · AWPCA · SDLC Hub», «Обзор · AWPCA · SDLC Hub».
  • Step 2: cd web && npx vitest run src/shell src/projects — FAIL.

  • Step 3: реализация. Панель прокручивается сама, если не влезает (M-5 #73); удалить мёртвый --topbar-h (M-6), устаревшие комментарии про верхнюю панель (M-7 #73, С19 аудита); событие пакета на узком экране — строкой под верхней строкой (замена TopbarNotice), на развёрнутой панели — в строке модуля; role="status" — постоянный контейнер (T10.1 #94).

  • Step 4: cd web && npm run lint && npm test — PASS.

  • Step 5: Commit — feat(web-modules): боковая панель по модулям, блок состояния, узкий экран, адреса модулей.

Task 5: Страница «Обзор»

Files:

  • Create: web/src/shell/OverviewPage.tsx (+ OverviewPage.test.tsx), web/src/modules/jira/Summary.tsx, web/src/modules/wiki/Summary.tsx
  • Modify: web/src/shell/pages.tsx (маршрут / → «Обзор» под ProjectGate), web/src/modules/jira/index.tsx, web/src/modules/wiki/index.tsx (Summary), web/src/api/queries.ts (useRecentBatches(project) → GET /batches?project=&limit=10)

Interfaces:

  • Consumes: ModuleStatusBlock нет — карточка модуля «Обзора» рисует ядро из useModuleActivity + status.freshness + useAgents (версия agent_version), кнопка variant="secondary"; Summary({project: Project}).

  • Produces: jira Summary: « задач · <открытых> открытых · <просроченных> просрочено» (по useBoard(project): свои задачи neighbor = false; открытые — не closed по modules/jira/api/closed.ts; просроченные — открытые с duedate < сегодня), ссылки «Доска связей», «Задачи», «Лента»; wiki Summary: «<wiki_pages> страниц», поле поиска (Enter → /wiki?project=<метка>&q=<строка>; страница «Вики» читает q в поле поиска), ссылка «Вики»; без корня — объяснение и ссылка «Привязать корень вики» на /wiki?project=<метка>.

  • Step 1: Тесты OverviewPage.test.tsx: / с проектом AWPCA (корень есть) — две карточки «Jira», «Вики» с агентом и версией («v14», «v1»), свежестью, кнопками «Обновить из Jira», «Обновить вики» (обе secondary); overview card without root — sfm_kedr → карточка «Вики» с «Привязать корень вики»; «Последние пакеты проекта» — строки #<id>, модуль, название, автор, статус, время; поиск по вики Enter → адрес /wiki?project=AWPCA&q=согласов, на «Вики» строка поиска заполнена и показаны результаты.

  • Step 2: cd web && npx vitest run src/shell/OverviewPage.test.tsx — FAIL.

  • Step 3: реализация (сетка карточек: 2 колонки ≥ 1024 px, 1 колонка уже; компоненты Panel, PageHeader, Button).

  • Step 4: cd web && npm run lint && npm test — PASS.

  • Step 5: Commit — feat(web-modules): страница «Обзор» со сводками модулей и последними пакетами.

Task 6: Окно «Новый проект» по модулям

Files:

  • Modify: web/src/projects/NewProjectDialog.tsx (+ тест), web/src/projects/projects.css, web/src/api/queries.ts (useCreateProject, useUpdateProject — тип ответа с batches), web/src/api/types.ts (CreatedProject.batches, UpdatedProject.batches: {module: string; id: number}[]), web/src/shell/Layout.tsx (onCreated → track каждого пакета своим модулем), web/src/modules/wiki/RootForm.tsx (пакет из batches → track модуля wiki)

Interfaces:

  • Consumes: useModuleActivity(name).track (Task 3), HubModule.title, projectFields.

  • Step 1: Тесты NewProjectDialog.test.tsx: группы fieldset с legend «Jira» и «Вики»; подсказки — текст под полем (aria-describedby), placeholder пустой; подсказка метки «метка задач в Jira и имя проекта в хабе»; 51.5 в «Номер доски Jira» → «нужно целое число», запроса нет (T4.1); ответ batches с двумя пакетами → track jira и track wiki вызваны со своими id (M-14, T5.3 #68); setExtra — функциональная форма (T4.2: два быстрых ввода в разные поля не теряют первое); FIELDS собирается при отрисовке, не при импорте (T4.5); useUpdateProject и показ 422 у поля корня — тесты (T4.4).

  • Step 2: cd web && npx vitest run src/projects src/modules/wiki — FAIL.

  • Step 3: реализация.

  • Step 4: cd web && npm run lint && npm test — PASS.

  • Step 5: Commit — feat(web-modules): «Новый проект» — поля по модулям, подсказки, пакеты каждому модулю.

Task 7: Страницы модулей — исправления по аудиту

Files:

  • Modify: web/src/modules/wiki/WikiPage.tsx, Tree.tsx, Search.tsx, wiki.css (+ тесты); web/src/modules/jira/board/components/BoardHeader.tsx, web/src/modules/jira/issues/IssuesPage.tsx, IssueTable.tsx, issues.css, IssueCard.tsx, web/src/modules/jira/feed/FeedPage.tsx; web/src/projects/ProjectGate.tsx; web/src/styles/ (глобальное scrollbar-gutter: stable для прокручиваемой области страницы)

  • Step 1: Тесты

    • «Вики»: шапка — h1 «Вики», подзаголовок «<wiki_root_title> · страниц · обновлено ДД.ММ ЧЧ:ММ», кнопка «Обновить вики» secondary из useModuleActivity("wiki"); без page — открыт корень и выделен в дереве; узлы-предки выбранной страницы раскрыты, после очистки поиска раскрытие прежнее (T10.2, T10.6); aria-setsize/aria-posinset у узлов (T10.2); порядок заголовков h1 → h2 (T10.3); на 390 px (matchMedia мок) страница выше дерева; параметр q заполняет поиск.
    • Доска, «Задачи», «Лента»: в шапке вторичная кнопка «Обновить из Jira» (useModuleActivity("jira")), нет primary; под шапкой доски нет пустого блока .board-src без содержимого (N-1), мёртвые пропсы label/title удалены (N-2 #68).
    • «Задачи»: просроченный срок — с текстом «просрочено» для экранного чтения (visually-hidden) и title; срок через fmtDate.
    • Карточка: раздел «Команды хаба» вместо «Команды моста».
    • ProjectGate: «проект NOPE не найден» и подсказка «выберите проект в списке «Проект» боковой панели (на узком экране — в «Меню»)».
  • Step 2: cd web && npx vitest run src/modules src/projects — FAIL.

  • Step 3: реализация. Горизонтальная прокрутка «Задач» на 1440 px: таблица прокручивается внутри своей панели (overflow-x: auto у обёртки таблицы, ширина обёртки не больше контейнера страницы).

  • Step 4: cd web && npm run lint && npm test — PASS.

  • Step 5: Commit — fix(web-modules): страницы модулей — шапки, «Вики» с корнем, просрочка, прокрутка «Задач».

Task 8: e2e, документация, спека

Files:

  • Modify: web/e2e/*.spec.ts (новые адреса, «Обзор», блок состояния, перенаправления), web/e2e/hub.ts (фикстура пакетов, если нужна), ARCHITECTURE.md (раздел «Веб»: контракт HubModule с status/Summary, трекер в shell/activity/, адреса), CLAUDE.md (адреса веба, «Обзор»), README.md (адреса веба)

  • Test: web/e2e/layout.spec.ts, web/e2e/screens.spec.ts, новый web/e2e/overview.spec.ts

  • Step 1: Тесты e2e (тестовый хаб): overview.spec.ts — /app/ → /app/?project=…, две карточки, список пакетов; layout.spec.ts — на 1440 px у «Задач» document.documentElement.scrollWidth === window.innerWidth; на «Вики» getComputedStyle(h1).fontSize === "20px"; /app/board → /app/jira/board; на 390 px — метка проекта в верхней строке, «Меню» модально; screens.spec.ts — скриншоты «Обзора» и панели в обеих темах (как прочие экраны).

  • Step 2: cd web && npm run e2e — новые тесты падают до правок e2e-хелперов, затем PASS.

  • Step 3: документация.

  • Step 4: uv run pytest -q, cd web && npm run lint && npm test && npm run e2e, openspec validate web-modules --strict — PASS.

  • Step 5: Commit — docs(web-modules): ARCHITECTURE, CLAUDE.md, README, e2e под модули.