PRO+ DEPLOYMENT Інструкція встановлення на субдомен ← Wizard

Розгортання newsomo Pro+ на субдомені

Покрокова інструкція: від реєстрації DNS до робочого Workbench на analyst.newsomo.com. Приблизно 4-6 годин роботи · одноразово.

📚 Зміст

  1. Архітектура: що куди ставимо
  2. DNS — субдомен у Cloudflare
  3. Netlify: фронтенд деплой
  4. Hetzner: розширення бекенду
  5. Auth: Google OAuth для аналітиків
  6. Admin role: підтвердження статусу
  7. AI engine: Claude API + embeddings
  8. Cron tasks: scheduled pipeline
  9. Monitoring + alerts
  10. Cost estimation
  11. Launch checklist
1

Архітектура: що куди ставимо

Огляд за 60 секунд
10 хв

Pro+ використовує існуючу інфраструктуру newsomo + один новий субдомен. Без окремих серверів — все на тому ж Hetzner CCX13, Netlify CDN та SQLite.

┌──────────────────────────────────────────────────────────────┐ │ analyst.newsomo.com │ │ │ │ ┌─────────────┐ ┌──────────────────────┐ │ │ │ Netlify │ ──API──▶│ Hetzner CCX13 │ │ │ │ (фронт) │ │ Flask + SQLite │ │ │ │ │ │ 157.180.68.160 │ │ │ │ • wizard │ │ │ │ │ │ • workbench│ │ /api/projects/* │ │ │ │ • capabil. │ │ /api/codings/* │ │ │ │ • SSL auto │ │ /api/sources/* │ │ │ └─────────────┘ │ /api/taxonomy/* │ │ │ │ │ │ │ Google OAuth ──────────▶│ /auth/google │ │ │ │ │ │ │ ┌─────────────────────┐ │ Existing newsomo: │ │ │ │ Resend (emails) │◀─│ RSS collector │ │ │ │ • welcome │ │ Claude haiku │ │ │ │ • report ready │ │ trafilatura │ │ │ │ • critical alert │ │ embeddings │ │ │ └─────────────────────┘ └──────────────────────┘ │ └──────────────────────────────────────────────────────────────┘
💡
Чому так просто: бекенд Pro+ — це 5 нових API endpoints поверх вже працюючого newsomo (220 RSS, Claude haiku, embeddings). Фронтенд — 5 static HTML файлів які ви вже бачили в outputs. Жодних мікросервісів, окремих БД, Kubernetes.
2

DNS — субдомен у Cloudflare

analyst.newsomo.com → Netlify
15 хв
  1. Зайти у Cloudflare dashboard → domain newsomo.com → DNS
  2. Add record: CNAME analyst → newsomo.netlify.app (або ваш Netlify subdomain)
  3. Proxy status: 🟠 DNS only (НЕ proxied — Netlify сам робить SSL)
  4. TTL: Auto
  5. Save → чекаємо 1-3 хв propagation
  6. Перевірка: dig analyst.newsomo.com або nslookup analyst.newsomo.com
⚠️
Не вмикайте Cloudflare proxy на цьому записі — інакше Netlify не зможе видати Let's Encrypt сертифікат. Якщо потім хочете Cloudflare CDN — підключіть через Netlify edge functions.

Опціонально: SPF/DMARC для transactional email

Якщо плануєте відправляти повідомлення з analyst@newsomo.com через Resend:

# Add TXT records у Cloudflare DNS
TXT @  v=spf1 include:_spf.resend.com ~all
TXT resend._domainkey  k=rsa; p=MIGfMA0...  # від Resend dashboard
TXT _dmarc  v=DMARC1; p=quarantine; rua=mailto:postmaster@newsomo.com
3

Netlify: фронтенд деплой

5 HTML файлів → CDN глобально
20 хв

Drag-drop варіант (швидкий)

  1. Зайти на app.netlify.com
  2. Створити новий site → "Deploy manually"
  3. Перетягнути папку з файлами:
    • newsomo_project_wizard.html
    • newsomo_analyst_workbench.html
    • newsomo_workbench_grid.html
    • newsomo_workbench_full.html
    • newsomo_ai_capabilities.html
    • index.html (login page — створіть або редирект на wizard)
  4. Site → Domain settings → Add custom domain → analyst.newsomo.com
  5. SSL → Netlify autoматично провізує Let's Encrypt cert (5 хв)

CLI варіант (для CI/CD)

# Один раз
npm install -g netlify-cli
netlify login

# У папці з HTML
netlify init
netlify deploy --prod --dir=.

# Майбутні оновлення
netlify deploy --prod

netlify.toml (обов'язково)

[build]
  publish = "."

# Redirect index.html → wizard
[[redirects]]
  from = "/"
  to = "/newsomo_project_wizard.html"
  status = 200

# API proxy до Hetzner backend
[[redirects]]
  from = "/api/*"
  to = "https://api.newsomo.com/:splat"
  status = 200
  force = true

# Security headers
[[headers]]
  for = "/*"
  [headers.values]
    X-Frame-Options = "DENY"
    X-Content-Type-Options = "nosniff"
    Referrer-Policy = "strict-origin-when-cross-origin"
    Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload"
Перевірка: відкрийте https://analyst.newsomo.com/ у браузері — має редиректнути на wizard. SSL замочок зелений.
4

Hetzner: розширення бекенду

5 нових API routes поверх існуючого newsomo
60 хв

Бекенд Pro+ — це один blueprint у Flask з 5 routes, що додається до існуючого newsomo. Жодних окремих процесів.

1. SSH у Hetzner і додавання нового модуля

ssh newsomo@157.180.68.160
cd /home/newsomo

# Створити новий blueprint файл
touch newsomo/api/analyst.py
nano newsomo/api/analyst.py

2. Структура нових таблиць (SQLite migration)

CREATE TABLE projects (
  id INTEGER PRIMARY KEY,
  fingerprint TEXT UNIQUE NOT NULL,           -- minfin-a691c2f
  name TEXT, client TEXT, analyst_email TEXT,
  start_date DATE, end_date DATE,
  taxonomy_json TEXT,                       -- {fixed:[], theses:[], speakers:[]}
  matrix_json TEXT,                          -- AI/Auto/Human per field
  sources_json TEXT,                         -- selected RSS cats, custom, csv, etc
  groups_json TEXT,
  status TEXT DEFAULT 'active',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE project_articles (
  id INTEGER PRIMARY KEY,
  project_id INTEGER REFERENCES projects(id),
  news_id INTEGER REFERENCES news(id),       -- посилання на newsomo news
  ai_preview_json TEXT,                      -- {minfin_type, theses, speakers}
  coding_json TEXT,                           -- final analyst answers
  is_completed BOOLEAN DEFAULT 0,
  completed_at TIMESTAMP
);

CREATE TABLE coding_decisions (
  id INTEGER PRIMARY KEY,
  project_id INTEGER, news_id INTEGER,
  field TEXT,                                 -- minfin/marchenko/thesis_X/speaker
  ai_value TEXT, human_value TEXT,
  decision TEXT,                              -- accept/reject/override/add
  ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE analyst_users (
  id INTEGER PRIMARY KEY,
  email TEXT UNIQUE, google_sub TEXT,
  role TEXT DEFAULT 'pending',        -- pending/analyst/admin
  approved_at TIMESTAMP, approved_by TEXT,
  pro_plus_active BOOLEAN DEFAULT 0,
  trial_ends_at TIMESTAMP
);

3. analyst.py blueprint (skeleton)

from flask import Blueprint, request, jsonify
from newsomo.db import get_conn
from newsomo.ai import claude_classify, embed_text, find_similar

bp = Blueprint('analyst', __name__, url_prefix='/api')

@bp.route('/projects', methods=['POST'])
def create_project():
    payload = request.json
    conn = get_conn()
    conn.execute("INSERT INTO projects (...) VALUES (...)", ...)
    # Trigger background: enroll matching articles, generate AI preview
    enqueue_project_init(payload['fingerprint'])
    return jsonify({'ok': True, 'fingerprint': ...})

@bp.route('/projects/<fp>/articles')
def list_articles(fp):
    # Returns 15-300 articles already filtered + AI preview attached
    ...

@bp.route('/codings/<article_id>', methods=['POST'])
def save_coding(article_id):
    payload = request.json  # {minfin, marchenko, theses, speakers, ...}
    save_decisions(article_id, payload)  # log into coding_decisions
    return jsonify({'ok': True})

@bp.route('/projects/<fp>/export')
def export_xlsx(fp):
    # Generate Excel з 113 колонками точно як ТЗ
    return send_file(generate_xlsx(fp), as_attachment=True)

4. Запуск (systemd unit вже існує)

sudo systemctl restart newsomo
sudo systemctl status newsomo
tail -f /var/log/newsomo/app.log

5. nginx proxy для api.newsomo.com

server {
    listen 443 ssl http2;
    server_name api.newsomo.com;
    ssl_certificate /etc/letsencrypt/live/newsomo.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/newsomo.com/privkey.pem;

    add_header Access-Control-Allow-Origin "https://analyst.newsomo.com";
    add_header Access-Control-Allow-Credentials "true";
    add_header Access-Control-Allow-Headers "Content-Type, Authorization";
    add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";

    location / {
        proxy_pass http://127.0.0.1:5000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        client_max_body_size 50M;  # для CSV upload
    }
}
⚠️
CORS: точний Access-Control-Allow-Origin для analyst.newsomo.com — не *, бо auth cookies.
5

Auth: Google OAuth для аналітиків

Заходять через Gmail, ви підтверджуєте статус
30 хв

1. Google Cloud Console

  1. Перейти на console.cloud.google.com → новий проєкт "newsomo-analyst"
  2. API & Services → OAuth consent screen → Configure:
    • User type: External
    • App name: newsomo Pro+
    • Support email: marikovski@gmail.com
    • Authorized domains: newsomo.com
    • Scopes: email, profile, openid
  3. Credentials → Create OAuth Client ID:
    • Type: Web application
    • Authorized JS origins: https://analyst.newsomo.com
    • Authorized redirect URIs: https://api.newsomo.com/auth/google/callback
  4. Скопіювати Client ID + Client Secret

2. Flask backend: auth endpoint

from authlib.integrations.flask_client import OAuth

oauth = OAuth(app)
oauth.register(
    name='google',
    client_id=os.environ['GOOGLE_CLIENT_ID'],
    client_secret=os.environ['GOOGLE_CLIENT_SECRET'],
    server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
    client_kwargs={'scope': 'openid email profile'},
)

@app.route('/auth/google')
def google_login():
    redirect_uri = 'https://api.newsomo.com/auth/google/callback'
    return oauth.google.authorize_redirect(redirect_uri)

@app.route('/auth/google/callback')
def google_callback():
    token = oauth.google.authorize_access_token()
    user = token['userinfo']

    # Upsert у analyst_users
    conn.execute("INSERT OR REPLACE INTO analyst_users (email, google_sub, role) VALUES (?, ?, 'pending')",
                 (user['email'], user['sub']))

    # Set httpOnly cookie з session token
    response = redirect('https://analyst.newsomo.com/')
    response.set_cookie('session', generate_jwt(user), httponly=True, secure=True, samesite='Lax')
    return response

3. Frontend: login кнопка

У index.html на analyst.newsomo.com:

<a href="https://api.newsomo.com/auth/google" class="btn-google">
  <img src="/google-icon.svg"> Sign in with Google
</a>

4. Перші 5 beta-аналітиків — швидкий whitelist

# SQL — після того як вони увійшли через Google перший раз
UPDATE analyst_users SET
  role = 'analyst',
  pro_plus_active = 1,
  trial_ends_at = datetime('now', '+60 days'),
  approved_at = datetime('now'),
  approved_by = 'marikovski@gmail.com'
WHERE email IN (
  'analyst1@agency.ua',
  'analyst2@pr.com',
  'analyst3@minfin.gov.ua',
  -- 5 beta partners
);
6

Admin role: підтвердження статусу

Ваша панель для approve/reject аналітиків
45 хв

Окремий route /admin на subdomain analyst.newsomo.com/admin, доступний лише role=admin.

Admin endpoints

GET  /api/admin/users/pending           → список нових login’ів
POST /api/admin/users/<email>/approve   → {role: 'analyst', trial_days: 14}
POST /api/admin/users/<email>/reject    → ban + notification email
GET  /api/admin/projects               → всі активні проєкти + analyst
POST /api/admin/projects/<fp>/disable  → заморозити (наприклад quota)
GET  /api/admin/metrics                → MRR, total codings, AI agreement

Admin UI (HTML)

Простий dashboard на /admin/index.html з:

  • Таблиця "Очікують підтвердження": gmail, дата реєстрації, кнопки [✓ Approve] [✗ Reject]
  • Таблиця "Активні аналітики": gmail, скільки проєктів, скільки кодувань за останні 30 днів, статус trial/paid
  • Метрики проєктів: загалом створено, активних, paused
  • AI metrics: total decisions, accept rate, override rate per analyst
💡
На старті (перші 50 аналітиків) — підтверджуєте вручну. При 100+ — додаєте автозатвердження для платних кастомерів (Stripe webhook).
7

AI engine: Claude API + embeddings

Класифікація + multi-label + NER
90 хв

Environment variables

# /home/newsomo/.env
ANTHROPIC_API_KEY=sk-ant-...
GOOGLE_CLIENT_ID=...apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-...
RESEND_API_KEY=re_...
JWT_SECRET=$(openssl rand -hex 32)

1. Active/Passive classifier (Claude few-shot)

from anthropic import Anthropic
client = Anthropic()

def classify_communication(text, entity='Мінфін', examples=[]):
    """Returns 'активна' | 'пасивна' | None."""
    few_shot = "\n".join([
        f"Приклад: {e['text'][:300]}\nВідповідь: {e['label']}\n"
        for e in examples[:8]
    ])
    msg = client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=20,
        messages=[{
            "role": "user",
            "content": f"""Тебе питають: чи комунікація {entity} у тексті 'активна' (сам сказав/зробив) чи 'пасивна' (про нього говорять інші)?

{few_shot}

Тепер класифікуй:
{text[:1500]}

Відповідь одним словом: активна, пасивна, або none"""
        }]
    )
    return msg.content[0].text.strip().lower()

2. Multi-label thesis classifier (embeddings)

from sentence_transformers import SentenceTransformer
import numpy as np

model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')

def top_theses(text, theses_with_embeddings, k=5):
    """Returns top-K most similar theses."""
    text_emb = model.encode(text[:2000])
    sims = [
        (thesis, np.dot(text_emb, emb) / (np.linalg.norm(text_emb) * np.linalg.norm(emb)))
        for thesis, emb in theses_with_embeddings
    ]
    sims.sort(key=lambda x: -x[1])
    return [(t, float(s)) for t, s in sims[:k] if s > 0.45]

3. NER через spaCy + fuzzy match

import spacy
from rapidfuzz import fuzz

nlp = spacy.load('uk_core_news_md')

def detect_speakers(text, known_speakers):
    """Returns list of matched speakers from DB."""
    doc = nlp(text[:3000])
    found = []
    for ent in doc.ents:
        if ent.label_ != 'PER': continue
        for speaker in known_speakers:
            name = speaker.split(',')[0].strip()
            if fuzz.ratio(ent.text.lower(), name.lower()) > 80:
                found.append(speaker)
                break
    return list(set(found))

4. Learning loop (cron each hour)

def retrain_thesis_centroids():
    """Recompute thesis centroids based on accepted articles."""
    for project in active_projects():
        for thesis in project.taxonomy.theses:
            accepted_articles = get_accepted_for(project.id, thesis)
            if len(accepted_articles) >= 3:
                embs = [model.encode(a.text[:2000]) for a in accepted_articles]
                centroid = np.mean(embs, axis=0)
                save_centroid(project.id, thesis, centroid)
    log("Retrained thesis centroids")
8

Cron tasks: scheduled pipeline

Що працює само у фоні
20 хв
# crontab -e як newsomo user

# Existing (вже працюють):
*/15 * * * * cd /home/newsomo && .venv/bin/python scripts/rss_collect.py
*/15 * * * * cd /home/newsomo && .venv/bin/python scripts/translate_pending.py
0 6 * * * cd /home/newsomo && .venv/bin/python scripts/promote_temp_sources.py

# Pro+ нові:
# Кожні 15 хв: знайти нові статті для активних проєктів і згенерувати AI preview
*/15 * * * * cd /home/newsomo && .venv/bin/python scripts/project_ai_preview.py >> logs/preview.log 2>&1

# Кожну годину: перерахувати thesis centroids на основі прийнятих рішень
0 * * * * cd /home/newsomo && .venv/bin/python scripts/retrain_centroids.py >> logs/learn.log 2>&1

# Щодня о 09:00: відправити weekly/daily reports клієнтам
0 9 * * * cd /home/newsomo && .venv/bin/python scripts/send_scheduled_reports.py

# Щодня о 03:00: backup БД у S3-сумісне сховище
0 3 * * * cd /home/newsomo && .venv/bin/sh scripts/backup_db.sh

# Раз на тиждень: deactivate expired trials, send reminders
0 10 * * 1 cd /home/newsomo && .venv/bin/python scripts/trial_lifecycle.py
9

Monitoring + alerts

Що зламається → ви дізнаєтесь
30 хв

UptimeRobot (безкоштовно до 50 monitors)

  • https://newsomo.com — main site, 5 min interval
  • https://analyst.newsomo.com — Pro+ frontend
  • https://api.newsomo.com/health — backend healthcheck endpoint
  • https://api.newsomo.com/api/projects/healthz — DB connectivity check

Alert channels: Telegram bot + email marikovski@gmail.com

Healthcheck endpoints (Flask)

@app.route('/health')
def health():
    return jsonify({'status': 'ok', 'ts': datetime.utcnow().isoformat()})

@app.route('/api/projects/healthz')
def healthz():
    try:
        conn = get_conn()
        n = conn.execute("SELECT COUNT(*) FROM projects WHERE status='active'").fetchone()[0]
        return jsonify({'status': 'ok', 'active_projects': n})
    except Exception as e:
        return jsonify({'status': 'error', 'msg': str(e)}), 500

Telegram alert bot (для критичних подій)

# у Flask error handler
@app.errorhandler(500)
def server_error(e):
    requests.post(
        f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage",
        data={
            'chat_id': ADMIN_CHAT_ID,
            'text': f"🚨 newsomo Pro+ 500 error\n{request.path}\n{str(e)[:200]}",
            'parse_mode': 'HTML'
        }
    )
    return 'Internal error', 500

Logging — лог-файли локально + ротація

# /etc/logrotate.d/newsomo
/home/newsomo/logs/*.log {
    daily
    rotate 30
    compress
    delaycompress
    notifempty
    create 0640 newsomo newsomo
}
10

Cost estimation

Скільки реально коштує запустити та підтримувати
5 хв

One-time costs (запуск)

АртиклОписСума
Google OAuth app verificationБезкоштовно (Anthropic standard)$0
SSL certLet's Encrypt через Netlify$0
Анти-bot, captchaCloudflare Turnstile (free tier 1M/міс)$0
UptimeRobot50 monitors free$0
Час девелопменту~73 години (за PLATFORM_ARCHITECTURE.md)~$1,800 (alas, self)
РазомOne-time$0 cash + ваш час

Monthly recurring (при 25 Pro+ юзерах)

СервісПлан$/міс
Hetzner CCX133 vCPU · 8GB · 80GB SSD (вже є)$15
NetlifyFree tier (100GB bandwidth)$0
Cloudflare DNSFree$0
Anthropic Claude~$0.001/article × 25 users × 200 articles/міс~$50
Resend email3000 emails free, 0.0001/email далі~$3
S3 backupsAWS S3 ~5GB$1
Stripe processing2.9% + $0.30 per tx (25 users × $30)~$30
РазомЗа місяць~$99/міс
💰
Break-even: з 25 Pro+ × $30 = $750 MRR → margin $651/міс (87% gross). Дуже здорова unit-economy.
11

Launch checklist

День Х — все має бути 🟢
пройти 30 хв

Pre-launch (за тиждень до D-day)

АртикулПеревірка
DNS: dig analyst.newsomo.com резолвиться
SSL: замочок зелений, валідний до 2026-09-XX
Frontend: всі 5 HTML файлів завантажуються
Backend: /health повертає 200
DB migrations: всі 4 нові таблиці створені
Google OAuth: можна залогінитись з тестового gmail
Admin panel: бачите своїх pending users
5 cron tasks додані, запустилися хоча б раз без помилок
Email send: тестовий welcome email прийшов
UptimeRobot моніторить 4 endpoints
Telegram alert bot працює (тестове повідомлення)

День X (запуск)

  1. Створюєте тестовий проєкт через wizard (з Мінфін шаблону)
  2. Перевіряєте що newsomo засуєне статті за 30 хв
  3. Кодуєте 3-5 статей у Workbench → перевіряєте що зберігаються
  4. Експортуєте → .xlsx має точно 113 колонок як Мінфін-приклад кодування.xlsx
  5. Запрошуєте перших 5 beta-аналітиків (whitelist + персональний email)
  6. Моніторите перші 24 години через Telegram alerts

Post-launch (перший тиждень)

  • Щодня: дивитесь UptimeRobot + admin panel: скільки нових signup, скільки активних кодувань
  • Щодня: дивитесь Telegram alerts: чи є 500-ки, чи AI працює стабільно
  • Кожні 2 дні: 15-хвилинні calls з beta-аналітиками: що працює, що ні, що бракує
  • В кінці тижня: ретроспектива → backlog для v42.1
🚨
Rollback план: якщо щось не так — Netlify зберігає всі deploys. Один клік "Rollback to previous" → старий фронт працює. Backend має git checkout v41.18 && sudo systemctl restart newsomo — Pro+ blueprint просто не реєструється, основний newsomo продовжує працювати.

🗓️ Timeline до запуску

Day 1
Setup: DNS + Netlify + SSL → analyst.newsomo.com відкривається з wizard frontend (4 год)
Day 2-3
Backend: DB migrations + 5 API endpoints + nginx CORS (16 год)
Day 4
Auth: Google OAuth + JWT cookies + admin endpoint (8 год)
Day 5-6
AI engine: Claude classifier + thesis embeddings + NER + learning loop (16 год)
Day 7
Admin panel + email: approve/reject + Resend notifications (8 год)
Day 8-9
Integration testing: end-to-end через wizard → Workbench → export (12 год)
Day 10
Launch: 5 beta аналітиків + monitoring (4 год)

Total: ~68 годин · 10 робочих днів · $99/міс recurring
~$1.2K інвестиції часу → $9-30K річний MRR за рік