ТИ решаваш! - Документация
Пълно ръководство за използване и внедряване на платформата за подаване на сигнали за замърсяване на въздуха към РИОСВ.
Възможности на платформата
PWA приложение
Инсталирайте на телефона си и използвайте офлайн
Double Opt-In
Защита от спам чрез потвърждение по имейл
Cloudflare Turnstile
Невидима CAPTCHA без досадни пъзели
DOCX генериране
Автоматично създаване на официален документ
Rate Limiting
Ограничение до 1 сигнал на ден на имейл
Serverless
Работи на Cloudflare Pages без сървър
Потребителско ръководство
Научете как да подадете сигнал за замърсяване на въздуха в 4 лесни стъпки.
Попълнете формата
Изберете източника на замърсяване, квартала, датата и часа на инцидента. Отбележете симптомите, които изпитвате (миризма, дразнене на очите и др.).
Въведете данните си
Попълнете вашето име и имейл адрес. Телефонът е опционален, но помага за по-бърза обратна връзка от РИОСВ.
Потвърдете имейла си
След изпращане ще получите имейл с линк за потвърждение. Кликнете на него, за да бъде изпратен сигналът към РИОСВ.
Готово!
След потвърждение, сигналът се изпраща автоматично към РИОСВ – Велико Търново. Ще получите отговор на имейла си.
Защо е нужно потвърждение по имейл?
Системата използва double opt-in механизъм за защита от злоупотреби:
- Предотвратява изпращане на сигнали от чужд имейл адрес
- Осигурява, че РИОСВ може да се свърже с вас
- Защитава системата от автоматизирани атаки
Инсталиране като приложение
Можете да инсталирате "ТИ решаваш!" като приложение на вашия телефон или компютър.
На Android (Chrome)
- Отворете signal.tireshavashzavt.org
- Натиснете бутона "Инсталирай" в горната част на сайта
- Или: меню (⋮) → "Добави към начален екран"
На iPhone (Safari)
- Отворете сайта в Safari
- Натиснете бутона за споделяне (□↑)
- Изберете "Add to Home Screen"
Често задавани въпроси
Колко сигнала мога да подам на ден?
Можете да подадете максимум 1 потвърден сигнал на ден от един имейл адрес. Това ограничение защитава системата от злоупотреби.
Не получих имейл за потвърждение. Какво да направя?
Проверете папка "Спам" или "Junk". Ако имейлът не е там, изчакайте 5 минути и опитайте отново. При продължаващи проблеми, свържете се с нас.
Какво става с личните ми данни?
Данните се съхраняват само временно (24 часа) до потвърждение. След изпращане към РИОСВ, данните се изтриват от нашата система. Вижте секция GDPR за повече информация.
Мога ли да редактирам сигнала след изпращане?
Не, след потвърждение сигналът се изпраща директно към РИОСВ. Ако искате да добавите информация, подайте нов сигнал на следващия ден.
Архитектура на системата
Системата е изградена на принципа "serverless" с използване на Cloudflare инфраструктура.
Детайлна архитектурна диаграма
Пълна архитектурна диаграма на системата
Поток на данните (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 без пъзели |
| 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
- Невъзможност за отгатване
Валидация на входните данни
Системата използва стриктна валидация на всички полета, както на frontend, така и на backend.
| Поле | Правила за валидация | Пример за грешка |
|---|---|---|
| Име |
|
Моля, въведете името си на кирилица |
| Имейл |
|
Моля, въведете валиден имейл адрес |
| Телефон |
|
Телефонът може да съдържа само цифри |
| Текстови полета |
|
Опасното съдържание се премахва автоматично |
Защо е нужна кирилица за името?
- Осигурява коректна идентификация на подателя
- Предотвратява злоупотреби с фалшиви имена
- Съответства на изискванията за официална кореспонденция
Код за валидация (пример)
// Валидация на име - само кирилица
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();
}
Статистически панел "Гласът на Велико Търново"
Интерактивен публичен панел, който визуализира анонимизирани данни от сигналите и мотивира гражданите да участват.
Концепция: "Заедно правим разлика"
Панелът е проектиран с фокус върху социалното доказателство и поведенческата психология, за да:
- Валидира преживяванията - показва, че проблемът е реален и споделен
- Създава чувство за общност - "Не си сам, 312 души като теб..."
- Мотивира за действие - gamification елементи и progress bars
- Демонстрира momentum - "Тази седмица +18% спрямо миналата"
Секции на панела
1. Hero секция - Емоционален импакт
2. Времева динамика - "Вълната расте"
3. Карта на кварталите - "Горещи точки"
4. Симптоми - "Споделени преживявания"
5. Gamification - "Общността расте"
6. 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 | Гласът на града | 🏆 |
Съхранение на данни
-- 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 съответствие
Жизнен цикъл на личните данни в системата
Какви данни събираме
| Данни | Цел | Съхранение |
|---|---|---|
| Име | Идентификация на подателя | 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 дни | Информация за доставка и статус (без съдържание) |
Изисквания за инсталация
Необходими акаунти
- 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
- Отидете на Cloudflare Turnstile Dashboard
- Създайте нов site
- Копирайте Site Key в
index.html - Запазете 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
- Свържете GitHub репозиторито с Cloudflare Pages
- Настройте build settings:
# Build command: (оставете празно) # Build output directory: / # Root directory: /
Всеки push към main branch ще деплойне автоматично.
Обновяване на версия
- Променете версията в
index.html(comment + footer) - Увеличете cache версията в
sw.js - 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 страница: "Вече имате потвърден сигнал за днес."