ТИ решаваш! - Документация

Пълно ръководство за използване и внедряване на платформата за подаване на сигнали за замърсяване на въздуха към РИОСВ.

Възможности на платформата

PWA приложение

Инсталирайте на телефона си и използвайте офлайн

Double Opt-In

Защита от спам чрез потвърждение по имейл

Cloudflare Turnstile

Невидима CAPTCHA без досадни пъзели

DOCX генериране

Автоматично създаване на официален документ

Rate Limiting

Ограничение до 1 сигнал на ден на имейл

Serverless

Работи на Cloudflare Pages без сървър

Потребителско ръководство

Научете как да подадете сигнал за замърсяване на въздуха в 4 лесни стъпки.

Попълнете формата

Изберете източника на замърсяване, квартала, датата и часа на инцидента. Отбележете симптомите, които изпитвате (миризма, дразнене на очите и др.).

Въведете данните си

Попълнете вашето име и имейл адрес. Телефонът е опционален, но помага за по-бърза обратна връзка от РИОСВ.

Потвърдете имейла си

След изпращане ще получите имейл с линк за потвърждение. Кликнете на него, за да бъде изпратен сигналът към РИОСВ.

Важно: Проверете папка "Спам" или "Junk" ако не виждате имейла в рамките на няколко минути.

Готово!

След потвърждение, сигналът се изпраща автоматично към РИОСВ – Велико Търново. Ще получите отговор на имейла си.

Защо е нужно потвърждение по имейл?

Системата използва double opt-in механизъм за защита от злоупотреби:

  • Предотвратява изпращане на сигнали от чужд имейл адрес
  • Осигурява, че РИОСВ може да се свърже с вас
  • Защитава системата от автоматизирани атаки

Инсталиране като приложение

Можете да инсталирате "ТИ решаваш!" като приложение на вашия телефон или компютър.

На Android (Chrome)

  1. Отворете signal.tireshavashzavt.org
  2. Натиснете бутона "Инсталирай" в горната част на сайта
  3. Или: меню (⋮) → "Добави към начален екран"

На iPhone (Safari)

  1. Отворете сайта в Safari
  2. Натиснете бутона за споделяне (□↑)
  3. Изберете "Add to Home Screen"
Предимства: Приложението работи офлайн, зарежда се моментално и изглежда като нативно приложение.

Често задавани въпроси

Колко сигнала мога да подам на ден?

Можете да подадете максимум 1 потвърден сигнал на ден от един имейл адрес. Това ограничение защитава системата от злоупотреби.

Не получих имейл за потвърждение. Какво да направя?

Проверете папка "Спам" или "Junk". Ако имейлът не е там, изчакайте 5 минути и опитайте отново. При продължаващи проблеми, свържете се с нас.

Какво става с личните ми данни?

Данните се съхраняват само временно (24 часа) до потвърждение. След изпращане към РИОСВ, данните се изтриват от нашата система. Вижте секция GDPR за повече информация.

Мога ли да редактирам сигнала след изпращане?

Не, след потвърждение сигналът се изпраща директно към РИОСВ. Ако искате да добавите информация, подайте нов сигнал на следващия ден.

Архитектура на системата

Системата е изградена на принципа "serverless" с използване на Cloudflare инфраструктура.

PWA Frontend index.html
Cloudflare Turnstile Anti-bot защита
Cloudflare Workers /api/submit, /api/confirm
Cloudflare KV Временно съхранение
Postal Server SMTP изпращане
РИОСВ Email riosvt-vt@riosvt-vt.org

Детайлна архитектурна диаграма

Архитектура на системата

Пълна архитектурна диаграма на системата

Поток на данните (Double Opt-In)

Поток на данните

Диаграма на потока от данни през системата

Технологичен стек

Компонент Технология Предназначение
Frontend Vanilla HTML/CSS/JS PWA приложение, без framework
Backend Cloudflare Workers Serverless API endpoints
Database Cloudflare KV Key-Value store за pending signals
Hosting Cloudflare Pages Static hosting + Functions
Anti-bot Cloudflare Turnstile CAPTCHA без пъзели
Email Postal Server Self-hosted SMTP
DOCX docx.js Генериране на Word документи

Сигурност и защита

Защита от спам

  • Cloudflare Turnstile - невидима CAPTCHA, блокира ботове
  • Double opt-in - изисква потвърждение от реален имейл
  • Rate limiting - максимум 3 заявки и 1 потвърден сигнал на ден
  • Token expiration - линковете изтичат след 24 часа

Защита на данните

  • HTTPS only - целият трафик е криптиран
  • No cookies - не се използват бисквитки за проследяване
  • Minimal data - съхраняваме само необходимото
  • Auto-delete - данните се изтриват след 24 часа

Token Security

Confirmation токените се генерират с crypto.randomUUID(), което осигурява:

  • 122 бита ентропия
  • Криптографски сигурен random
  • Невъзможност за отгатване
Важно за production: Уверете се, че всички secrets (TURNSTILE_SECRET_KEY, POSTAL_API_KEY) са конфигурирани през Cloudflare Dashboard, а не в кода!

Валидация на входните данни

Системата използва стриктна валидация на всички полета, както на frontend, така и на backend.

Поле Правила за валидация Пример за грешка
Име
  • Само кирилица (български букви)
  • 3-100 символа
  • Минимум 2 думи (име + фамилия)
  • Позволени: интервали, тирета
Моля, въведете името си на кирилица
Имейл
  • Валиден RFC 5322 формат
  • Само латиница, цифри, точки, тирета
  • Валиден домейн с TLD
Моля, въведете валиден имейл адрес
Телефон
  • Само цифри (опционално)
  • Автоматично добавяне на +359
Телефонът може да съдържа само цифри
Текстови полета
  • Sanitization срещу XSS
  • Премахване на < > тагове
  • Премахване на javascript: URLs
  • Премахване на event handlers
Опасното съдържание се премахва автоматично

Защо е нужна кирилица за името?

Сигналите се изпращат към българска държавна институция (РИОСВ). Използването на кирилица:
  • Осигурява коректна идентификация на подателя
  • Предотвратява злоупотреби с фалшиви имена
  • Съответства на изискванията за официална кореспонденция

Код за валидация (пример)

// Валидация на име - само кирилица
function validateName(name) {
  const cyrillicPattern = /^[А-Яа-яЁёЍѝ\s\-]+$/;
  if (!cyrillicPattern.test(name)) {
    return { valid: false, error: 'Моля, въведете името си на кирилица' };
  }
  const words = name.split(/\s+/).filter(w => w.length > 0);
  if (words.length < 2) {
    return { valid: false, error: 'Моля, въведете име и фамилия' };
  }
  return { valid: true };
}

// Sanitize на входни данни
function sanitizeInput(input) {
  return input
    .replace(/[<>]/g, '')           // Премахва HTML тагове
    .replace(/javascript:/gi, '')    // Премахва javascript: URLs
    .replace(/on\w+\s*=/gi, '')      // Премахва event handlers
    .replace(/[\x00-\x1F]/g, '')     // Премахва control chars
    .trim();
}

Статистически панел "Гласът на Велико Търново"

Интерактивен публичен панел, който визуализира анонимизирани данни от сигналите и мотивира гражданите да участват.

Вижте статистиката на живо: /stats/

Концепция: "Заедно правим разлика"

Панелът е проектиран с фокус върху социалното доказателство и поведенческата психология, за да:

  • Валидира преживяванията - показва, че проблемът е реален и споделен
  • Създава чувство за общност - "Не си сам, 312 души като теб..."
  • Мотивира за действие - gamification елементи и progress bars
  • Демонстрира momentum - "Тази седмица +18% спрямо миналата"

Секции на панела

1. Hero секция - Емоционален импакт

Hero секция - Емоционален импакт

2. Времева динамика - "Вълната расте"

Седмична динамика на сигналите

3. Карта на кварталите - "Горещи точки"

Разпределение по квартали

4. Симптоми - "Споделени преживявания"

Статистика на симптомите

5. Gamification - "Общността расте"

Gamification - постижения и цели

6. Live feed - "Случва се сега"

Live feed - последни сигнали

Психологически принципи

Принцип Реализация Ефект
Социално доказателство "312 граждани", "623 души споделят" Намалява съмнението, че проблемът е реален
Колективна идентичност "ЗАЕДНО", "Не си сам" Създава чувство за общност и принадлежност
Прогрес и momentum "+18% тази седмица", progress bars Показва, че движението расте и има смисъл
Gamification Постижения, цели, нива Подсъзнателна награда за участие
FOMO Live feed в реално време "Другите действат, а аз?"
Валидация "78% изпитват същото" Потвърждава, че преживяването е реално

Технически детайли

API Endpoint

GET /api/public-stats - Връща агрегирана анонимизирана статистика:

{
  "totals": {
    "signals": 847,
    "respondents": 312
  },
  "districts": [
    { "district": "Чолаковци", "count": 189 },
    { "district": "Бузлуджа", "count": 156 }
  ],
  "sources": [...],
  "symptoms": [...],
  "daily": [
    { "date": "2025-01-15", "count": 23 }
  ],
  "weeklyChange": 18,
  "recent": [
    { "time": "преди 3 мин", "district": "Чолаковци", "symptom": "миризма на лепило" }
  ],
  "milestones": {
    "achieved": [{ "target": 100, "name": "Първата искра", "icon": "🔥" }],
    "next": { "target": 1000, "name": "Не може да ни игнорират", "icon": "🎯" },
    "progress": 84,
    "remaining": 153
  }
}

Milestones (Постижения)

Цел Име Икона
100Първата искра🔥
250Гласът се чува📢
500Вълна на промяната🌊
750Силата на общността💪
1000Не може да ни игнорират🎯
2000Движение за промяна🚀
5000Гласът на града🏆

Съхранение на данни

GDPR съвместимо: Статистиката използва само анонимизирани данни от D1 базата. Email адресите се хешират с SHA-256 за проследяване на уникални респонденти без съхранение на лични данни.
-- D1 таблица за анонимизирани данни
CREATE TABLE anonymous_signals (
  id TEXT PRIMARY KEY,
  timestamp TEXT NOT NULL,
  source TEXT NOT NULL,
  district TEXT NOT NULL,
  symptoms TEXT NOT NULL,      -- JSON array
  email_hash TEXT NOT NULL,    -- SHA-256 hash
  created_at INTEGER NOT NULL
);

Автоматично обновяване

  • Данните се кешират за 60 секунди (Cache-Control header)
  • Фронтенд панелът се обновява автоматично на всяка минута
  • Live feed показва последните 10 сигнала

GDPR съответствие

GDPR жизнен цикъл на данните

Жизнен цикъл на личните данни в системата

Какви данни събираме

Данни Цел Съхранение
Име Идентификация на подателя 24 часа в KV, след това към РИОСВ
Имейл Потвърждение + обратна връзка 24 часа в KV
Телефон (опционално) Допълнителен контакт 24 часа в KV
IP адрес Turnstile верификация Не се съхранява

Правно основание

Обработката на данни се извършва на основание:

  • Член 6(1)(a) GDPR - Съгласие (чрез изпращане на формата)
  • Член 6(1)(e) GDPR - Обществен интерес (сигнали за замърсяване)

Права на субектите

Право на достъп

Можете да поискате копие на вашите данни

Право на изтриване

Данните се изтриват автоматично след 24 часа

Право на възражение

Не потвърждавайте имейла и данните ще бъдат изтрити

Забележка: След потвърждение, данните се предават на РИОСВ и излизат от нашата система. За достъп до тези данни, обърнете се към РИОСВ.

Съхранение в Postal SMTP сървър

Изпратените имейли се обработват през Postal SMTP сървър със следните политики за съхранение:

Параметър Стойност Описание
Raw message данни 30 дни Пълното съдържание на имейла (headers + attachments)
Метаданни 60 дни Информация за доставка и статус (без съдържание)
Важно: След изтичане на 30-дневния период, съдържанието на имейлите (включително DOCX прикачените файлове) се изтрива автоматично от Postal сървъра.

Изисквания за инсталация

Необходими акаунти

  • Cloudflare акаунт (безплатен) - за Pages, Workers, KV, Turnstile
  • Postal Server - self-hosted или друг SMTP доставчик
  • GitHub акаунт (опционално) - за автоматичен deploy

Локални инструменти

# Node.js 18+
node --version

# npm или yarn
npm --version

# Wrangler CLI
npm install -g wrangler
wrangler --version

Cloudflare услуги

Услуга План Лимити (безплатен)
Pages Free Unlimited sites, 500 builds/month
Workers Free 100,000 requests/day
KV Free 100,000 reads/day, 1,000 writes/day
Turnstile Free Unlimited

Инсталация стъпка по стъпка

Клонирайте репозиторито

git clone https://github.com/your-org/signal2riosv_vt.git
cd signal2riosv_vt

Инсталирайте зависимостите

npm install

Влезте в Cloudflare

wrangler login

Това ще отвори браузър за автентикация.

Създайте KV namespace

# Създайте namespace
wrangler kv:namespace create PENDING_SIGNALS

# Копирайте ID-то и го добавете в wrangler.toml
# [[kv_namespaces]]
# binding = "PENDING_SIGNALS"
# id = "your-namespace-id"

Конфигурирайте Turnstile

  1. Отидете на Cloudflare Turnstile Dashboard
  2. Създайте нов site
  3. Копирайте Site Key в index.html
  4. Запазете Secret Key за следващата стъпка

Добавете secrets

# Turnstile secret key
wrangler pages secret put TURNSTILE_SECRET_KEY

# Postal API key
wrangler pages secret put POSTAL_API_KEY

# Stats key (за /api/stats endpoint)
wrangler pages secret put STATS_KEY

Редактирайте wrangler.toml

name = "signal-riosv"
pages_build_output_dir = "."

[[kv_namespaces]]
binding = "PENDING_SIGNALS"
id = "your-namespace-id"

[vars]
RIOSV_EMAIL = "riosvt-vt@riosvt-vt.org"
FROM_EMAIL = "noreply@yourdomain.com"
FROM_NAME = "ТИ решаваш!"
POSTAL_API_URL = "https://postal.yourdomain.com"

Deploy!

wrangler pages deploy .

Конфигурация

Променливи на средата

Променлива Тип Описание
RIOSV_EMAIL var Имейл на РИОСВ за получаване на сигнали
FROM_EMAIL var Имейл адрес за изпращане
FROM_NAME var Име на изпращача
POSTAL_API_URL var URL на Postal API
TURNSTILE_SECRET_KEY secret Turnstile secret за верификация
POSTAL_API_KEY secret API ключ за Postal
STATS_KEY secret Ключ за достъп до статистики

Rate Limiting

Настройките за rate limiting се намират в functions/api/submit.js:

// Rate limiting constants
const MAX_PENDING_PER_DAY = 3;    // Непотвърдени заявки
const MAX_CONFIRMED_PER_DAY = 1;  // Потвърдени сигнали

Turnstile Site Key

Намерете в index.html и заменете:

<div class="cf-turnstile"
     data-sitekey="YOUR_SITE_KEY"
     data-callback="onTurnstileSuccess">
</div>

Deployment стратегии

Ръчен deploy с Wrangler

# Deploy към production
wrangler pages deploy .

# Preview deploy (за тестване)
wrangler pages deploy . --branch=preview

Автоматичен deploy от GitHub

  1. Свържете GitHub репозиторито с Cloudflare Pages
  2. Настройте build settings:
# Build command: (оставете празно)
# Build output directory: /
# Root directory: /

Всеки push към main branch ще деплойне автоматично.

Обновяване на версия

  1. Променете версията в index.html (comment + footer)
  2. Увеличете cache версията в sw.js
  3. Deploy
# index.html
<!-- version 0.3.7 -->

# sw.js
const CACHE_NAME = 'ti-reshavash-v14';

POST /api/submit

Изпраща сигнал и генерира confirmation имейл.

Request

{
  "turnstileToken": "string (required)",
  "signalData": {
    "source": "string",
    "customSource": "string (optional)",
    "district": "string",
    "whenVal": "string (ISO date)",
    "address": "string (optional)",
    "subject": "string (optional)",
    "name": "string (required)",
    "email": "string (required)",
    "phone": "string (optional)",
    "desc": "string (optional)",
    "checks": ["string array"]
  },
  "docxBase64": "string (base64 encoded DOCX)",
  "letterText": "string (plain text of letter)"
}

Response - Success (200)

{
  "success": true,
  "message": "Изпратихме имейл за потвърждение на user@example.com"
}

Response - Rate Limited (429)

{
  "success": false,
  "error": "Достигнахте максималния брой заявки за деня (3).",
  "rateLimited": true
}

Response - Turnstile Failed (400)

{
  "success": false,
  "error": "Turnstile верификацията е неуспешна (timeout-or-duplicate)."
}

GET /api/confirm/:token

Потвърждава сигнала и го изпраща към РИОСВ.

Parameters

:token UUID token от confirmation имейла

Response - Success (200)

HTML страница с потвърждение:

<!DOCTYPE html>
<html>
  <body>
    <h1>Сигналът е изпратен!</h1>
    <p>Ще получите отговор на: user@example.com</p>
  </body>
</html>

Response - Token Expired (404)

HTML страница с грешка и линк за нов сигнал.

Response - Rate Limited (429)

HTML страница: "Вече имате потвърден сигнал за днес."