Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,10 @@ jobs:
- name: Test
if: ${{ !cancelled() }}
run: pnpm test
# Docs integrity (#102): fail on a dangling docs reference in source or an
# unindexed doc. The checker self-tests first, so the gate is itself gated.
- name: Docs integrity
if: ${{ !cancelled() }}
run: |
pnpm check:docs:test
pnpm check:docs
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@

Дизайнът и решенията живеят в [`docs/`](docs/) — започнете оттам:

- [`docs/architecture.md`](docs/architecture.md) — преглед на системата и карта към решенията.
- [`docs/adr/`](docs/adr/README.md) — Architecture Decision Records (по едно решение на файл).
- [`docs/core-scope.md`](docs/core-scope.md) — доменен модел и речник на данните.
- [`docs/etl.md`](docs/etl.md) — ETL pipeline-ът и емисията от ЦАИС ЕОП.
- [`docs/deploy.md`](docs/deploy.md) — деплой към Cloudflare.
- [`docs/architecture.md`](docs/architecture.md) — архитектурни решения (ADR).
- [`docs/spec/ai-assistant.md`](docs/spec/ai-assistant.md) — спецификация на AI асистента (планиран).
- [`docs/README.md`](docs/README.md) — **пълен индекс** на документацията, вкл. достъпност и стандартите за ревю.

Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/routes/contract.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ export default function Contract({ loaderData }: Route.ComponentProps) {
<span className="muted">не е посочен в данните</span>
),
// Break the gross count down by status/category — surfaces what „Брой оферти" actually
// means (it's the gross submitted count, including rejections — see docs/etl-pipeline.md
// means (it's the gross submitted count, including rejections — see docs/etl.md
// and the staging columns at packages/db/migrations/0000_init.sql:363-365). Each clause
// only appears when the source published a non-zero value, so contracts without any
// rejection/SME data fall back to the original „самите оферти…" footnote.
Expand Down
10 changes: 9 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@

Дизайнът, решенията и спецификациите на платформата за прозрачност на обществените поръчки живеят тук. За преглед на продукта и бърз старт вижте [`README.md`](../README.md) в корена; работните конвенции са в [`AGENTS.md`](../AGENTS.md).

- [`architecture.md`](architecture.md) — архитектурните решения: рендериране (React Router v7 SSR на Workers), сигурност и достъп до D1.
- [`architecture.md`](architecture.md) — преглед на системата (поток на данните, двата Worker-а) и карта към решенията.
- [`adr/`](adr/README.md) — Architecture Decision Records: по едно архитектурно решение на файл, с индекс и шаблон.
- [`core-scope.md`](core-scope.md) — доменният модел и **речникът на данните**: таблици, rollup-и, `value_flag`/`date_flag`, семантиката на `amount_eur`.
- [`etl.md`](etl.md) — ETL pipeline-ът и open-data емисията на ЦАИС ЕОП (`storage.eop.bg`): зареждане, опресняване и производни таблици.
- [`etl-pipeline-state.md`](etl-pipeline-state.md) — анализ на текущото състояние на ETL pipeline-а.
- [`etl-architecture.md`](etl-architecture.md) — целевата ETL архитектура (RFC): предложение за състоянието и реда на изпълнение.
- [`v1-implementation-plan.md`](v1-implementation-plan.md) — precompute слоят и пагинацията (защо rollup-и и keyset вместо per-request GROUP BY / OFFSET).
- [`integrity-gate.md`](integrity-gate.md) — reconciliation gate-ът: hard asserts върху тоталите при import/CI.
- [`anomaly-report.md`](anomaly-report.md) — cross-row аномалии при опресняване: какво `value_flag` не хваща на ниво отделен договор.
- [`deploy.md`](deploy.md) — деплой към Cloudflare: двата Worker-а (`sigma`, `sigma-etl`) и споделеният D1 per environment.
- [`api.md`](api.md) — публичните данни и машинно четими endpoint-и (CSV/JSON/sitemap), query грамата на филтрите и лицензът — за разработчици, които строят върху данните.
- [`accessibility.md`](accessibility.md) — достъпност (WCAG 2.1 AA / EN 301 549): какво покрива платформата и наблюденията за вградената приставка за достъпност.
- [`spec/ai-assistant.md`](spec/ai-assistant.md) — спецификация на разговорния аналитичен слой над СИГМА (BgGPT, текст и глас).
- [`spec/assistant-contracts.md`](spec/assistant-contracts.md) — контрактите BE↔FE за AI асистента (Фаза 1 → Фаза 2).

## Стандарти за ревю

Expand Down
81 changes: 81 additions & 0 deletions docs/adr/0001-rendering-and-security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# ADR-0001 — Стратегия за рендиране на front-end-а и модел на сигурност

- **Статус:** Прието
- **Дата:** 2026-05-21
- **Обхват:** Итерация 1 — публичният, read-only explorer на обществените поръчки (АОП). Документът отбелязва и пътя напред към по-широката платформа (оценка на риска, аномалии, картелен анализ — roadmap).

> **Актуализация (2026-05-21):** Решението за фреймуърк по-долу беше променено от SvelteKit на **React Router v7 върху Cloudflare Workers** след претегляне на екосистемата, набирането на хора и контрибутори, primitives за достъпност и екосистемата за визуализации/AI. Решенията за рендиране (§2) и за сигурност (§3) са независими от фреймуърка и остават непроменени; обновени бяха само техните специфични за фреймуърка механики.

## Контекст

Итерация 1 е **публичен, read-only** отчетен и визуализационен слой върху ~190 хил. реда договори от АОП в Cloudflare D1, насочен към граждани, журналисти и НПО, с интерфейс на български. Уеб приложението първоначално беше скелетирано върху **SvelteKit**; този ADR преразглежда този избор. Сървърната среда е **Cloudflare Workers**, които четат D1. Данните пристигат като **периодични bulk зареждания** (`scripts/import.mjs`), а не като жива емисия; в тази итерация **няма публичен write път и няма автентикация**.

Три въпроса стояха в основата на решението:

1. Трябва ли front-end-ът да премине от Svelte към React?
2. Трябва ли страниците да се рендират на сървъра (SSR), да се пре-рендират (статично), или да са класическо клиентско SPA срещу API?
3. Променят ли отговора атаките през SSR / „hydration“?

## Решение

### 1. React с React Router v7 (framework mode) върху Cloudflare Workers

Избран пред първоначалния SvelteKit скелет след претегляне на дълготрайните фактори:

- **Екосистема и преизползване** — най-голямата екосистема от компоненти/библиотеки и най-богатите опции точно за повърхностите, върху които СИГМА стъпва: мрежови графи на връзки (`@xyflow/react`), таблици за големи данни (TanStack Table / AG Grid), графики (visx / Recharts), а за по-късния асистент — AI SDK (`@ai-sdk/react`) и зрели UI-та за асистенти.
- **Набиране на хора и open-source контрибутори** — значително по-голям talent pool; релевантно за вероятно OSS гражданска платформа („Лицензът ще бъде определен преди публичното пускане“).
- **Primitives за достъпност** — React Aria / Radix са сред най-добрите в класа си за изричната цел **WCAG 2.2 AA**.
- **Дълготрайност** — най-защитимият дългосрочен избор за дълголетна платформа от обществена полза.

**React Router v7 (framework mode), а не Next.js** — той върви чисто върху **Cloudflare Workers** (през `@cloudflare/vite-plugin`) и запазва модела SSR-на-edge и хибридното рендиране от §2. Приети компромиси спрямо SvelteKit: по-голям клиентски runtime (смекчен от SSR + кеширане) и липса на first-party CF *Pages* адаптер — RR7 се деплойва като Worker, което освен това уеднаквява `apps/web` с останалите `apps/*`.

### 2. Рендиране: хибридно, избрано според повърхността

По подразбиране **SSR + edge кеширане**; пре-рендиране само за реално статичните страници; клиентско рендиране само за интерактивните „острови“. **Не** чисто SPA за публично съдържание (SEO/споделяемост + цена на първото изрисуване); **не** build-time пре-рендиране на целия корпус (десетки хиляди страници за компании/институции, а търсенето изобщо не може да се пре-рендира).

| Повърхност | Стратегия | Защо |
| --- | --- | --- |
| Начало, За проекта, **методология / „как работят червените флагове“**, документация за отворените данни | Пре-рендиране (статично) | Рядко се променят → надеждно + безплатно |
| Профил на компания (по ЕИК), страница на институция, детайл на поръчка/обособена позиция | **SSR + edge кеш** (`s-maxage` + `stale-while-revalidate`), purge при презареждане на набора данни | Голям обем, променя се само при bulk презареждане → скорост като при статика + устойчивост на DDoS, актуалност след презареждане |
| Explorer: търсене / филтри / класации („най-големи бенефициенти“) | SSR, кратък кеш, варира според заявката | Безкрайно URL пространство; задвижван от SQL |
| Интерактивни визуализации (граф на мрежа/потоци, карта на България, сортируеми таблици) | Клиентски рендиран **остров**, хидриран върху SSR-ната страница + първоначални данни | Нуждае се от клиентска интерактивност; SSR на обвивката заради SEO/първо изрисуване |

Тъй като наборът данни е периодична снимка (а не жив), кеш TTL-ите може да са дълги, а едно презареждане просто **purge-ва** кеша — почти всички предимства на статиката, без да се изброява корпусът при build.

### 3. Модел на сигурност

Приоритети за публичен сайт за прозрачност: **интегритет ≈ наличност ≫ конфиденциалност** — данните са публични по дизайн; тяхната *достоверност* и *uptime* са продуктът.

**Контроли в итерация 1:**

- **Read път да остане read-only.** Ingestion-ът е офлайн (`scripts/import.mjs`); няма публичен write endpoint. Това се запазва — без публична мутация, без админ в итерация 1.
- **Edge модел + кеширане като поглъщане на DDoS.** Cloudflare поглъща L3/4; кешираният HTML означава, че атакуващ / scrape трафик удря кеша, а не D1.
- **WAF + rate limiting** върху задвижвания от SQL explorer/търсене и върху всеки export на отворени данни; ограничаване на `limit` / пагинация / размер на export-а, така че нито една заявка да не сканира цялата таблица.
- **Строг CSP + security headers** — генериране на per-request nonce в `entry.server.tsx`, подаван към `<Scripts nonce>` / `<ScrollRestoration nonce>` на React Router и към header-а `Content-Security-Policy` (за да са разрешени hydration скриптовете на фреймуърка без `unsafe-inline`). Добавяне на HSTS, `X-Content-Type-Options`, `Referrer-Policy`.
- **D1 prepared statements само с bound параметри**; никога конкатениране на SQL чрез низове.

**По въпроса за „hydration атаките“** — реалните класове и как този дизайн ги адресира:

- **Изтичане между потребители през кеширан персонализиран SSR** — неприложимо в итерация 1 (без автентикация; всяка страница е анонимна и идентична → безопасна за кеширане). Остава предотвратено и по-късно, като се кешират *само* анонимни публични страници и никога автентикираните.
- **Свръх-сериализиране на тайни в hydration payload-а** — смекчено чрез типизираните response DTO-та в `packages/api-contract` (отдават проекции на същностите, никога сурови редове) и чрез държане на код, носещ credentials, извън уеб/read пътя. DTO-тата да остават без вътрешни-само полета.
- **XSS през сериализирано състояние / рендирани данни** — разчита се на авто-escaping-а на React/JSX; **никога `dangerouslySetInnerHTML`** върху текст от АОП (институция / компания / предмет са от външен източник); строгият CSP е резервната защита.

**Отложено за по-късни фази** (повърхностите на по-широката платформа):

- **Workflow-и за възложители/участници + админ** → Cloudflare Access (SSO + MFA) за админ; строго разделяне на публичен/персонализиран кеш; изолация на write пътя в отделен worker.
- **AI асистент за обществени поръчки** → маршрутизиране през **AI Gateway** (лимити за rate/разход, кеширане, логване); read-only, параметризирани инструменти; заземяване на всяко твърдение в изчислени данни (риск от клевета); никакво кеширане; рендиране на изхода като текст. Спецификация — [`spec/ai-assistant.md`](../spec/ai-assistant.md).

## Последствия / scaffold follow-up-и

- Конфигуриране на пре-рендирането в `react-router.config.ts` (`prerender` пътища) за статичните информационни route-ове.
- Задаване на `Cache-Control` (`s-maxage` + `stale-while-revalidate`) през `headers` export-ите на route-овете при SSR data route-овете; иницииране на cache purge (по URL/таг) в края на скрипта за зареждане на данните.
- Задаване на CSP + security headers в `entry.server.tsx` (или Worker middleware) с per-request nonce върху `<Scripts>`.
- Добавяне на Cloudflare rate-limit правило + WAF managed ruleset за explorer/export endpoint-ите (infra конфигурация, проследявана отделно).
- Премахване или auth-gate на отворения `POST /etl/run` от скелета в `apps/etl` преди какъвто и да е деплой.

## Свързани документи

- [`etl.md`](../etl.md) — ETL pipeline-ът и емисията от ЦАИС ЕОП.
- [`deploy.md`](../deploy.md) — деплой към Cloudflare и оперативна сигурност.
- [`spec/ai-assistant.md`](../spec/ai-assistant.md) — спецификация на планирания AI асистент.
- [`README.md`](../../README.md) на хранилището и [`AGENTS.md`](../../AGENTS.md) — общ преглед и работни конвенции.
Loading