feat: migrate prod docs to OVH VPS + UTC→Warsaw timezone in all templates
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Production moved from on-prem VM 249 (10.22.68.249) to OVH VPS (57.128.200.27, inpi-vps-waw01). Updated ALL documentation, slash commands, memory files, architecture docs, and deploy procedures. Added |local_time Jinja filter (UTC→Europe/Warsaw) and converted 155 .strftime() calls across 71 templates so timestamps display in Polish timezone regardless of server timezone. Also includes: created_by_id tracking, abort import fix, ICS calendar fix for missing end times, Pros Poland data cleanup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3df362f44e
commit
110d971dca
@ -2,7 +2,7 @@
|
||||
|
||||
## Status: WDROŻONE ✅
|
||||
|
||||
Aplikacja Flask została pomyślnie wdrożona na NORDABIZ-01.
|
||||
Aplikacja Flask została pomyślnie wdrożona na OVH VPS inpi-vps-waw01.
|
||||
|
||||
## Ukończone kroki
|
||||
|
||||
@ -16,12 +16,12 @@ Aplikacja Flask została pomyślnie wdrożona na NORDABIZ-01.
|
||||
|
||||
## Dostęp
|
||||
|
||||
- **LAN:** http://10.22.68.249 ✅
|
||||
- **LAN:** http://57.128.200.27 ✅
|
||||
- **WAN:** https://nordabiznes.pl (maintenance mode w NPM)
|
||||
|
||||
## Ważne informacje
|
||||
|
||||
- **VM:** NORDABIZ-01 (ID 249, IP 10.22.68.249)
|
||||
- **VM:** OVH VPS inpi-vps-waw01 (ID 249, IP 57.128.200.27)
|
||||
- **Baza:** PostgreSQL `nordabiz` (80 firm)
|
||||
- **Port aplikacji:** 5000
|
||||
- **Venv:** /var/www/nordabiznes/venv/
|
||||
|
||||
@ -110,7 +110,7 @@ docker exec nordabiz-postgres psql -U nordabiz_app -d nordabiz -c "SELECT slug,
|
||||
|
||||
## Uwagi:
|
||||
- DEV: PostgreSQL via Docker na localhost:5433
|
||||
- PROD: PostgreSQL na 10.22.68.249:5432
|
||||
- PROD: PostgreSQL na 57.128.200.27:5432
|
||||
- Dla produkcji: po przetestowaniu lokalnie, wdróż przez `/deploy`
|
||||
- Nowe firmy powinny pochodzić z oficjalnej listy członków Norda Biznes
|
||||
- Zawsze weryfikuj NIP przed dodaniem
|
||||
|
||||
@ -12,7 +12,7 @@ Opcjonalny argument określa typ backupu:
|
||||
|
||||
## System automatycznych backupów
|
||||
|
||||
### Harmonogram (cron na NORDABIZ-01)
|
||||
### Harmonogram (cron na OVH VPS inpi-vps-waw01)
|
||||
|
||||
| Typ | Częstotliwość | Godzina | Retencja | Lokalizacja |
|
||||
|-----|---------------|---------|----------|-------------|
|
||||
@ -25,16 +25,16 @@ Opcjonalny argument określa typ backupu:
|
||||
|
||||
```bash
|
||||
# Ostatnie backupy hourly
|
||||
ssh maciejpi@10.22.68.249 "ls -lt /var/backups/nordabiz/hourly/ | head -5"
|
||||
ssh maciejpi@57.128.200.27 "ls -lt /var/backups/nordabiz/hourly/ | head -5"
|
||||
|
||||
# Ostatnie backupy daily
|
||||
ssh maciejpi@10.22.68.249 "ls -lt /var/backups/nordabiz/daily/ | head -5"
|
||||
ssh maciejpi@57.128.200.27 "ls -lt /var/backups/nordabiz/daily/ | head -5"
|
||||
|
||||
# Sprawdź offsite (PBS)
|
||||
ssh maciejpi@10.22.68.127 "ls -lt /backup/nordabiz/daily/ | head -5"
|
||||
|
||||
# Rozmiar backupów
|
||||
ssh maciejpi@10.22.68.249 "du -sh /var/backups/nordabiz/*"
|
||||
ssh maciejpi@57.128.200.27 "du -sh /var/backups/nordabiz/*"
|
||||
```
|
||||
|
||||
## Kroki do wykonania:
|
||||
@ -48,26 +48,26 @@ docker exec nordabiz-postgres pg_dump -U nordabiz_app nordabiz > "backups/dev_$(
|
||||
### 2. Backup produkcyjnej bazy PostgreSQL
|
||||
Eksport do lokalnego:
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "sudo -u postgres pg_dump nordabiz" > "backups/prod_$(date +%Y%m%d_%H%M%S).sql"
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres pg_dump nordabiz" > "backups/prod_$(date +%Y%m%d_%H%M%S).sql"
|
||||
```
|
||||
|
||||
Lub backup na serwerze:
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "sudo -u postgres pg_dump -Fc nordabiz > /tmp/backup_$(date +%Y%m%d_%H%M%S).dump"
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres pg_dump -Fc nordabiz > /tmp/backup_$(date +%Y%m%d_%H%M%S).dump"
|
||||
```
|
||||
|
||||
### 3. Backup plików konfiguracyjnych
|
||||
```bash
|
||||
mkdir -p backups/config_$(date +%Y%m%d)
|
||||
cp .env backups/config_$(date +%Y%m%d)/dev.env
|
||||
ssh maciejpi@10.22.68.249 "cat /var/www/nordabiznes/.env" > backups/config_$(date +%Y%m%d)/prod.env
|
||||
ssh maciejpi@10.22.68.249 "cat /etc/nginx/sites-available/nordabiznes" > backups/config_$(date +%Y%m%d)/nginx.conf
|
||||
ssh maciejpi@57.128.200.27 "cat /var/www/nordabiznes/.env" > backups/config_$(date +%Y%m%d)/prod.env
|
||||
ssh maciejpi@57.128.200.27 "cat /etc/nginx/sites-available/nordabiznes" > backups/config_$(date +%Y%m%d)/nginx.conf
|
||||
```
|
||||
|
||||
### 4. Snapshot VM w Proxmox
|
||||
Użyj skill `proxmox-manager`:
|
||||
```
|
||||
Utwórz snapshot VM NORDABIZ-01 (ID: 249) z opisem "Backup przed [operacja]"
|
||||
Utwórz snapshot VM OVH VPS inpi-vps-waw01 (OVH VPS) z opisem "Backup przed [operacja]"
|
||||
```
|
||||
|
||||
Lub ręcznie:
|
||||
@ -83,14 +83,14 @@ ls -la backups/*.sql 2>/dev/null
|
||||
|
||||
Snapshoty VM (użyj skill proxmox-manager):
|
||||
```
|
||||
Pokaż snapshoty VM 249
|
||||
Pokaż snapshoty OVH VPS
|
||||
```
|
||||
|
||||
### 6. Przywracanie z backupu
|
||||
|
||||
#### Szybkie przywracanie (skrypt DR)
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "sudo /var/www/nordabiznes/scripts/dr-restore.sh /var/backups/nordabiz/hourly/nordabiz_YYYYMMDD_HH.dump"
|
||||
ssh maciejpi@57.128.200.27 "sudo /var/www/nordabiznes/scripts/dr-restore.sh /var/backups/nordabiz/hourly/nordabiz_YYYYMMDD_HH.dump"
|
||||
```
|
||||
|
||||
#### DEV (Docker PostgreSQL):
|
||||
@ -100,13 +100,13 @@ docker exec -i nordabiz-postgres psql -U nordabiz_app -d nordabiz < backups/dev_
|
||||
|
||||
#### PROD (PostgreSQL):
|
||||
```bash
|
||||
cat backups/prod_YYYYMMDD_HHMMSS.sql | ssh maciejpi@10.22.68.249 "sudo -u postgres psql nordabiz"
|
||||
cat backups/prod_YYYYMMDD_HHMMSS.sql | ssh maciejpi@57.128.200.27 "sudo -u postgres psql nordabiz"
|
||||
```
|
||||
|
||||
#### Rollback VM:
|
||||
Użyj skill `proxmox-manager`:
|
||||
```
|
||||
Przywróć VM 249 ze snapshotu backup_YYYYMMDD
|
||||
Przywróć OVH VPS ze snapshotu backup_YYYYMMDD
|
||||
```
|
||||
|
||||
## Konfiguracja cron (na serwerze PROD)
|
||||
@ -162,6 +162,6 @@ sudo /var/www/nordabiznes/scripts/dr-restore.sh /path/to/backup.dump
|
||||
- Snapshoty VM są najszybsze do rollbacku
|
||||
- PostgreSQL dump jest przenośny między środowiskami
|
||||
- DEV używa Docker PostgreSQL na localhost:5433
|
||||
- PROD używa PostgreSQL na 10.22.68.249:5432
|
||||
- PROD używa PostgreSQL na 57.128.200.27:5432
|
||||
|
||||
Data aktualizacji: 2026-02-02
|
||||
|
||||
@ -99,4 +99,4 @@ Podsumuj w czytelnej formie:
|
||||
- Monitoruj tokeny dla kontroli limitów
|
||||
- Wysokie latency może wskazywać na problemy z API
|
||||
- DEV: `docker exec nordabiz-postgres psql -U nordabiz_app -d nordabiz`
|
||||
- PROD: `ssh maciejpi@10.22.68.249 "sudo -u postgres psql -d nordabiz"`
|
||||
- PROD: `ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz"`
|
||||
|
||||
@ -127,4 +127,4 @@ Wygeneruj raport w formacie markdown z tabelami:
|
||||
- Priorytet: email > telefon > www > opis
|
||||
- Weryfikuj dane przez oficjalne źródła (CEIDG, KRS)
|
||||
- DEV: `docker exec nordabiz-postgres psql -U nordabiz_app -d nordabiz`
|
||||
- PROD: `ssh maciejpi@10.22.68.249 "sudo -u postgres psql -d nordabiz"`
|
||||
- PROD: `ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz"`
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Deploy NordaBiz to Production
|
||||
|
||||
Wykonaj deployment projektu NordaBiz na serwer produkcyjny NORDABIZ-01.
|
||||
Wykonaj deployment projektu NordaBiz na serwer produkcyjny OVH VPS inpi-vps-waw01.
|
||||
|
||||
## Kroki do wykonania:
|
||||
|
||||
@ -10,7 +10,7 @@ Wykonaj deployment projektu NordaBiz na serwer produkcyjny NORDABIZ-01.
|
||||
- Sprawdź czy lokalna aplikacja działa: `curl http://localhost:5000/health` lub `curl http://localhost:5001/health`
|
||||
|
||||
### 2. Połączenie z serwerem
|
||||
- SSH do NORDABIZ-01: `ssh root@10.22.68.249`
|
||||
- SSH do OVH VPS inpi-vps-waw01: `ssh maciejpi@57.128.200.27`
|
||||
- Przejdź do katalogu: `cd /var/www/nordabiznes`
|
||||
|
||||
### 3. Deployment
|
||||
|
||||
@ -15,54 +15,54 @@ Opcjonalny argument określa typ logów lub liczbę linii, np.:
|
||||
Połącz się z serwerem i pobierz logi:
|
||||
|
||||
```bash
|
||||
ssh root@10.22.68.249 "journalctl -u nordabiznes -n 50 --no-pager"
|
||||
ssh maciejpi@57.128.200.27 "journalctl -u nordabiznes -n 50 --no-pager"
|
||||
```
|
||||
|
||||
Dla więcej linii:
|
||||
```bash
|
||||
ssh root@10.22.68.249 "journalctl -u nordabiznes -n 100 --no-pager"
|
||||
ssh maciejpi@57.128.200.27 "journalctl -u nordabiznes -n 100 --no-pager"
|
||||
```
|
||||
|
||||
Tylko błędy:
|
||||
```bash
|
||||
ssh root@10.22.68.249 "journalctl -u nordabiznes -p err -n 50 --no-pager"
|
||||
ssh maciejpi@57.128.200.27 "journalctl -u nordabiznes -p err -n 50 --no-pager"
|
||||
```
|
||||
|
||||
### 2. Logi Nginx (access)
|
||||
```bash
|
||||
ssh root@10.22.68.249 "tail -50 /var/log/nginx/access.log"
|
||||
ssh maciejpi@57.128.200.27 "tail -50 /var/log/nginx/access.log"
|
||||
```
|
||||
|
||||
### 3. Logi Nginx (error)
|
||||
```bash
|
||||
ssh root@10.22.68.249 "tail -50 /var/log/nginx/error.log"
|
||||
ssh maciejpi@57.128.200.27 "tail -50 /var/log/nginx/error.log"
|
||||
```
|
||||
|
||||
### 4. Logi w czasie rzeczywistym (follow)
|
||||
```bash
|
||||
ssh root@10.22.68.249 "journalctl -u nordabiznes -f"
|
||||
ssh maciejpi@57.128.200.27 "journalctl -u nordabiznes -f"
|
||||
```
|
||||
(Ctrl+C aby przerwać)
|
||||
|
||||
### 5. Logi z określonego czasu
|
||||
Ostatnia godzina:
|
||||
```bash
|
||||
ssh root@10.22.68.249 "journalctl -u nordabiznes --since '1 hour ago' --no-pager"
|
||||
ssh maciejpi@57.128.200.27 "journalctl -u nordabiznes --since '1 hour ago' --no-pager"
|
||||
```
|
||||
|
||||
Dzisiaj:
|
||||
```bash
|
||||
ssh root@10.22.68.249 "journalctl -u nordabiznes --since today --no-pager"
|
||||
ssh maciejpi@57.128.200.27 "journalctl -u nordabiznes --since today --no-pager"
|
||||
```
|
||||
|
||||
### 6. Szukanie wzorca
|
||||
```bash
|
||||
ssh root@10.22.68.249 "journalctl -u nordabiznes --no-pager | grep -i 'error\|exception\|failed'"
|
||||
ssh maciejpi@57.128.200.27 "journalctl -u nordabiznes --no-pager | grep -i 'error\|exception\|failed'"
|
||||
```
|
||||
|
||||
### 7. Status usługi
|
||||
```bash
|
||||
ssh root@10.22.68.249 "systemctl status nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "systemctl status nordabiznes"
|
||||
```
|
||||
|
||||
## Analiza logów:
|
||||
@ -74,7 +74,7 @@ Po pobraniu logów przeanalizuj je pod kątem:
|
||||
- Nieautoryzowanych prób dostępu
|
||||
|
||||
## Uwagi:
|
||||
- Serwer: NORDABIZ-01 (VM 249, IP 10.22.68.249)
|
||||
- Serwer: OVH VPS inpi-vps-waw01 (OVH VPS, IP 57.128.200.27)
|
||||
- Usługa systemd: `nordabiznes`
|
||||
- Logi rotują automatycznie
|
||||
- Dla alertów rozważ integrację z Zabbix (skill: monitoring-manager)
|
||||
|
||||
@ -18,7 +18,7 @@ Sprawdź aktualny status projektu NordaBiz - lokalnie i na produkcji.
|
||||
- Test SSL: `curl -vI https://nordabiznes.pl 2>&1 | grep -E "(SSL|expire|subject)"`
|
||||
|
||||
### 3. Status VM (użyj skill proxmox-manager)
|
||||
- VM: NORDABIZ-01 (ID: 249)
|
||||
- VM: OVH VPS inpi-vps-waw01 (OVH VPS)
|
||||
- Sprawdź: CPU, RAM, uptime, snapshoty
|
||||
|
||||
### 4. Statystyki bazy danych (DEV via Docker)
|
||||
@ -33,7 +33,7 @@ SELECT 'Wiadomości chat: ' || COUNT(*) FROM ai_chat_messages;
|
||||
|
||||
### 5. Statystyki bazy danych (PROD)
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "sudo -u postgres psql -d nordabiz -c \"
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz -c \"
|
||||
SELECT 'Firmy: ' || COUNT(*) FROM companies;
|
||||
SELECT 'Użytkownicy: ' || COUNT(*) FROM users;
|
||||
SELECT 'Konwersacje chat: ' || COUNT(*) FROM ai_chat_conversations;
|
||||
@ -52,11 +52,11 @@ Podsumuj status w formie tabeli:
|
||||
| Lokalna aplikacja | OK/ERROR | port |
|
||||
| Docker PostgreSQL | OK/ERROR | localhost:5433 |
|
||||
| Produkcja | OK/ERROR | response time |
|
||||
| VM NORDABIZ-01 | OK/ERROR | uptime |
|
||||
| VM OVH VPS inpi-vps-waw01 | OK/ERROR | uptime |
|
||||
| Baza danych DEV | OK/ERROR | liczba rekordów |
|
||||
| Baza danych PROD | OK/ERROR | liczba rekordów |
|
||||
| Git | CLEAN/DIRTY | branch |
|
||||
|
||||
## Uwagi:
|
||||
- DEV: PostgreSQL via Docker na localhost:5433
|
||||
- PROD: PostgreSQL na 10.22.68.249:5432
|
||||
- PROD: PostgreSQL na 57.128.200.27:5432
|
||||
|
||||
67
AGENTS.md
Normal file
67
AGENTS.md
Normal file
@ -0,0 +1,67 @@
|
||||
# NordaBiz — Agent Instructions
|
||||
|
||||
Platform: katalog firm i networking dla Izby Gospodarczej Norda Biznes (Wejherowo).
|
||||
Production: https://nordabiznes.pl | Status: LIVE
|
||||
|
||||
## Stack
|
||||
|
||||
- **Backend:** Flask 3.0, SQLAlchemy 2.0, Python 3.9+, PostgreSQL
|
||||
- **Frontend:** HTML5, CSS3, Vanilla JS, Jinja2
|
||||
- **AI:** Google Gemini 3 Flash (free tier) — moduł NordaGPT
|
||||
- **Security:** Flask-Login, Flask-WTF (CSRF), Flask-Limiter
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
app.py # Main Flask app (routes, auth, API)
|
||||
database.py # SQLAlchemy models (Company, User, Chat, Forum...)
|
||||
gemini_service.py # Google Gemini AI integration
|
||||
nordabiz_chat.py # AI chat engine with company context
|
||||
search_service.py # Unified SearchService (synonyms, FTS, fuzzy)
|
||||
blueprints/ # 17 Flask blueprints (modular routes)
|
||||
templates/ # Jinja2 templates
|
||||
static/ # CSS, JS, images
|
||||
database/ # SQL schemas, migrations
|
||||
scripts/ # Python/Node.js utilities
|
||||
tests/ # Unit + integration tests
|
||||
```
|
||||
|
||||
## Key Conventions
|
||||
|
||||
- **Slug format:** kebab-case, e.g. `pixlab-sp-z-o-o`
|
||||
- **NIP:** 10 digits, no dashes | **REGON:** 9 or 14 digits | **KRS:** 10 digits (companies only)
|
||||
- **Categories:** `IT`, `Construction`, `Services`, `Production`, `Trade`, `Other`
|
||||
- **Data quality levels:** `basic`, `enhanced`, `complete`
|
||||
|
||||
## Database
|
||||
|
||||
- **Dev:** PostgreSQL via Docker (`localhost:5433/nordabiz`)
|
||||
- **Prod:** PostgreSQL on 57.128.200.27:5432 (OVH VPS inpi-vps-waw01, localhost from server)
|
||||
- After creating tables: `GRANT ALL ON TABLE ... TO nordabiz_app`
|
||||
- After creating sequences: `GRANT USAGE, SELECT ON SEQUENCE ... TO nordabiz_app`
|
||||
|
||||
## Running Locally
|
||||
|
||||
```bash
|
||||
docker compose up -d # Start PostgreSQL
|
||||
python3 app.py # Start Flask (port 5000 or 5001)
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
pytest tests/ -v # All tests
|
||||
pytest tests/unit/ -v # Unit tests (no DB)
|
||||
pytest tests/integration/ -v # Integration tests (with DB)
|
||||
```
|
||||
|
||||
## Jinja2 Templates — IMPORTANT
|
||||
|
||||
`{% block extra_js %}` in `base.html` is INSIDE a `<script>` tag — do NOT add your own `<script>` tags inside it.
|
||||
|
||||
## Code Style
|
||||
|
||||
- Polish language for user-facing strings, English for code/comments
|
||||
- No unnecessary abstractions — keep it simple
|
||||
- Security first: never hardcode API keys, always use `.env`
|
||||
- Validate at system boundaries only
|
||||
@ -168,7 +168,7 @@ Checked deployment architecture documentation against documented server IPs, por
|
||||
|
||||
| Item | Documented Value | Verified in Docs | Status |
|
||||
|------|------------------|------------------|--------|
|
||||
| NORDABIZ-01 IP | 10.22.68.249 | ✅ Found | VERIFIED |
|
||||
| NORDABIZ-01 IP | 57.128.200.27 | ✅ Found | VERIFIED |
|
||||
| NORDABIZ-01 VM ID | 249 | ✅ Found | VERIFIED |
|
||||
| R11-REVPROXY-01 IP | 10.22.68.250 | ✅ Found | VERIFIED |
|
||||
| R11-REVPROXY-01 VM ID | 119 | ✅ Found | VERIFIED |
|
||||
|
||||
60
CLAUDE.md
60
CLAUDE.md
@ -67,24 +67,23 @@ nordabiz/
|
||||
- **Domena:** staging.nordabiznes.pl (NPM Proxy Host ID: 44)
|
||||
- **Weryfikacja:** `curl -I https://staging.nordabiznes.pl/health`
|
||||
|
||||
### Production
|
||||
- **Serwer:** NORDABIZ-01 (VM 249, IP 10.22.68.249)
|
||||
- **Baza:** PostgreSQL na 10.22.68.249:5432
|
||||
- **Reverse Proxy:** NPM na R11-REVPROXY-01 (VM 119, IP 10.22.68.250)
|
||||
### Production (OVH VPS)
|
||||
- **Serwer:** inpi-vps-waw01 (OVH VPS, IP 57.128.200.27)
|
||||
- **Baza:** PostgreSQL na 57.128.200.27:5432
|
||||
- **SSL/Proxy:** nginx na VPS (bezpośrednio, bez NPM)
|
||||
- **Domena:** nordabiznes.pl (DNS w OVH, SSL Let's Encrypt)
|
||||
- **SSH:** `ssh maciejpi@57.128.200.27` (ZAWSZE jako maciejpi!)
|
||||
- **Ścieżka:** `/var/www/nordabiznes` | Restart: `sudo systemctl restart nordabiznes`
|
||||
- **Weryfikacja:** `curl -I https://nordabiznes.pl/health`
|
||||
|
||||
### NPM Proxy Configuration (KRYTYCZNE!)
|
||||
**⚠️ Różnice OVH VPS vs stary on-prem:**
|
||||
- **Brak .git repo** na VPS — deploy przez rsync, NIE git pull
|
||||
- **`.env` jest root-owned** — skrypty wymagają sudo do odczytu
|
||||
- **Migracje** wymagają `sudo -u postgres psql` (app user nie ma ALTER TABLE)
|
||||
|
||||
**Proxy Host ID:** 27 | **Forward Port:** 5000 (NIE 80!)
|
||||
### NPM Proxy Configuration (tylko staging)
|
||||
|
||||
```
|
||||
NPM (10.22.68.250) → Backend (10.22.68.249:5000) ✓
|
||||
NPM (10.22.68.250) → Backend (10.22.68.249:80) ✗ (pętla przekierowań!)
|
||||
```
|
||||
|
||||
Na serwerze .249 nginx na porcie 80 przekierowuje na HTTPS. Flask/Gunicorn na porcie 5000. **ZAWSZE** sprawdź port po edycji NPM!
|
||||
|
||||
**Weryfikacja:** `curl -I https://nordabiznes.pl/health` | **Incydent:** `docs/INCIDENT_REPORT_20260102.md`
|
||||
NPM dotyczy teraz **tylko staging** (Proxy Host ID: 44 dla staging.nordabiznes.pl). Produkcja nie korzysta z NPM — SSL obsługuje nginx bezpośrednio na OVH VPS.
|
||||
|
||||
## Git & Deployment
|
||||
|
||||
@ -103,40 +102,41 @@ Na serwerze .249 nginx na porcie 80 przekierowuje na HTTPS. Flask/Gunicorn na po
|
||||
# 1. DEV: Push do obu repozytoriów
|
||||
git push origin master && git push inpi master
|
||||
|
||||
# 2. STAGING: Wdrożenie i test
|
||||
# 2. STAGING: Wdrożenie i test (on-prem, bez zmian)
|
||||
ssh maciejpi@10.22.68.248 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl reload nordabiznes"
|
||||
# ⚠️ OBOWIĄZKOWO: Test manualny nowej funkcjonalności na staging!
|
||||
|
||||
# 3. PROD: Pull zmiany (DOPIERO PO WERYFIKACJI STAGING!)
|
||||
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && sudo -u www-data git pull"
|
||||
# 3. PROD (OVH VPS): Rsync zmienionych plików (DOPIERO PO WERYFIKACJI STAGING!)
|
||||
rsync -avz -e ssh --rsync-path="sudo rsync" <files> maciejpi@57.128.200.27:/var/www/nordabiznes/
|
||||
|
||||
# 4. PROD: Migracje SQL (jeśli są)
|
||||
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && /var/www/nordabiznes/venv/bin/python3 scripts/run_migration.py database/migrations/XXX_nazwa.sql"
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres psql nordabiz -f /var/www/nordabiznes/database/migrations/XXX_nazwa.sql"
|
||||
|
||||
# 5. PROD: Restart + weryfikacja
|
||||
ssh maciejpi@10.22.68.249 "sudo systemctl reload nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl restart nordabiznes"
|
||||
curl -sI https://nordabiznes.pl/health | head -3
|
||||
```
|
||||
|
||||
**⚠️ UWAGI KRYTYCZNE:**
|
||||
1. **Migracje SQL** - NIE używaj `psql` bezpośrednio. Użyj `scripts/run_migration.py`
|
||||
2. **Uprawnienia logów** - `sudo chown -R maciejpi:maciejpi /var/log/nordabiznes/`
|
||||
3. **502 po restarcie** - Poczekaj 3-5 sekund i sprawdź ponownie
|
||||
4. **Git pull** - Używaj `sudo -u www-data git pull` (www-data ma klucze SSH)
|
||||
1. **Brak .git na VPS** - Deploy TYLKO przez rsync, NIE git pull
|
||||
2. **Migracje SQL** - Używaj `sudo -u postgres psql` (app user nie ma ALTER TABLE)
|
||||
3. **`.env` jest root-owned** - Skrypty wymagają sudo do odczytu
|
||||
4. **502 po restarcie** - Poczekaj 3-5 sekund i sprawdź ponownie
|
||||
5. **SSH timeout** - NIE oznacza że komenda nie została wykonana! Sprawdź `ps aux | grep <skrypt>`
|
||||
6. **Staging nadal on-prem** - Git pull na .248 działa jak wcześniej (www-data ma klucze SSH)
|
||||
|
||||
**Skrypty Python z dostępem do bazy (WAŻNE!):**
|
||||
|
||||
`.env` NIE jest wczytywany przez `source .env` w kontekście SSH!
|
||||
`.env` na OVH VPS jest root-owned — wymaga sudo do odczytu!
|
||||
|
||||
```bash
|
||||
# ✅ PRAWIDŁOWO:
|
||||
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && \
|
||||
DATABASE_URL=\$(grep DATABASE_URL .env | cut -d'=' -f2) \
|
||||
ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && \
|
||||
sudo DATABASE_URL=\$(sudo grep DATABASE_URL .env | cut -d'=' -f2) \
|
||||
/var/www/nordabiznes/venv/bin/python3 skrypt.py"
|
||||
|
||||
# ❌ BŁĘDNIE:
|
||||
ssh maciejpi@10.22.68.249 "source .env && python3 skrypt.py"
|
||||
ssh maciejpi@57.128.200.27 "source .env && python3 skrypt.py"
|
||||
```
|
||||
|
||||
## Konwencje danych
|
||||
@ -157,8 +157,10 @@ ssh maciejpi@10.22.68.249 "source .env && python3 skrypt.py"
|
||||
|
||||
### Deployment
|
||||
- Przed wdrożeniem: `python -m py_compile app.py`
|
||||
- SSH: `ssh maciejpi@10.22.68.249` (ZAWSZE jako maciejpi!)
|
||||
- Ścieżka: `/var/www/nordabiznes` | Restart: `sudo systemctl reload nordabiznes`
|
||||
- SSH prod: `ssh maciejpi@57.128.200.27` (OVH VPS, ZAWSZE jako maciejpi!)
|
||||
- SSH staging: `ssh maciejpi@10.22.68.248` (on-prem VM 248)
|
||||
- Ścieżka: `/var/www/nordabiznes` | Restart prod: `sudo systemctl restart nordabiznes`
|
||||
- Deploy prod: rsync (brak .git na VPS) | Deploy staging: git pull
|
||||
- **ZAWSZE** aktualizuj `release_notes` w app.py
|
||||
|
||||
### Szablony Jinja2 - WAŻNE!
|
||||
|
||||
@ -68,9 +68,9 @@ grep -r "NordaBiz2025Secure" --include="*.txt" .
|
||||
```
|
||||
./view_maturity_results.sh:# export PGPASSWORD='your_database_password'
|
||||
./view_maturity_results.sh: echo " export PGPASSWORD='your_database_password'"
|
||||
./view_maturity_results.sh:ssh root@10.22.68.249 "PGPASSWORD=\"$PGPASSWORD\" psql -h localhost -U nordabiz_app -d nordabiz -c \"
|
||||
./view_maturity_results.sh:ssh root@10.22.68.249 "PGPASSWORD=\"$PGPASSWORD\" psql -h localhost -U nordabiz_app -d nordabiz -c \"
|
||||
./view_maturity_results.sh:ssh root@10.22.68.249 "PGPASSWORD=\"$PGPASSWORD\" psql -h localhost -U nordabiz_app -d nordabiz -c \"
|
||||
./view_maturity_results.sh:ssh root@57.128.200.27 "PGPASSWORD=\"$PGPASSWORD\" psql -h localhost -U nordabiz_app -d nordabiz -c \"
|
||||
./view_maturity_results.sh:ssh root@57.128.200.27 "PGPASSWORD=\"$PGPASSWORD\" psql -h localhost -U nordabiz_app -d nordabiz -c \"
|
||||
./view_maturity_results.sh:ssh root@57.128.200.27 "PGPASSWORD=\"$PGPASSWORD\" psql -h localhost -U nordabiz_app -d nordabiz -c \"
|
||||
```
|
||||
|
||||
**Analysis:**
|
||||
|
||||
@ -100,14 +100,14 @@ graph LR
|
||||
|
||||
**Issue:** Line break example showed only a single node without connections:
|
||||
```mermaid
|
||||
A[Flask App<br/>10.22.68.249<br/>Port 5000]
|
||||
A[Flask App<br/>57.128.200.27<br/>Port 5000]
|
||||
```
|
||||
|
||||
**Fix:** Added proper graph structure with connections:
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Flask App<br/>10.22.68.249<br/>Port 5000]
|
||||
B[PostgreSQL<br/>10.22.68.249<br/>Port 5432]
|
||||
A[Flask App<br/>57.128.200.27<br/>Port 5000]
|
||||
B[PostgreSQL<br/>57.128.200.27<br/>Port 5432]
|
||||
A --> B
|
||||
```
|
||||
|
||||
@ -188,7 +188,7 @@ Spot-checked key diagrams for accuracy:
|
||||
- ✅ Identifies one-to-many, many-to-many, one-to-one
|
||||
|
||||
4. **Network Topology** (`07-network-topology.md`)
|
||||
- ✅ Correct IP addresses (10.22.68.249, .250, .180)
|
||||
- ✅ Correct IP addresses (57.128.200.27, .250, .180)
|
||||
- ✅ Correct ports (5000 for Flask, 5432 for PostgreSQL)
|
||||
- ✅ Shows Fortigate NAT configuration
|
||||
|
||||
|
||||
80
README.md
80
README.md
@ -271,8 +271,8 @@ Norda Biznes Partner is a **Flask-powered web platform** built with PostgreSQL,
|
||||
|
||||
#### 🌍 Multi-Environment Deployment
|
||||
- **Development:** PostgreSQL via Docker (localhost:5433)
|
||||
- **Production:** NORDABIZ-01 server (VM 249, 10.22.68.249)
|
||||
- **Reverse proxy:** NPM on R11-REVPROXY-01
|
||||
- **Production:** OVH VPS (57.128.200.27, inpi-vps-waw01)
|
||||
- **Staging reverse proxy:** NPM on R11-REVPROXY-01
|
||||
- **SSL/TLS:** Let's Encrypt with auto-renewal
|
||||
- **Domain:** nordabiznes.pl (DNS in OVH)
|
||||
- **WSGI:** Gunicorn production server
|
||||
@ -859,9 +859,9 @@ The application is live in production at **https://nordabiznes.pl** since Novemb
|
||||
|
||||
| Component | Details |
|
||||
|-----------|---------|
|
||||
| **Server** | NORDABIZ-01 (VM 249, IP: 10.22.68.249) |
|
||||
| **Database** | PostgreSQL 15 on 10.22.68.249:5432 |
|
||||
| **Reverse Proxy** | Nginx Proxy Manager on R11-REVPROXY-01 (VM 119, IP: 10.22.68.250) |
|
||||
| **Server** | OVH VPS inpi-vps-waw01 (IP: 57.128.200.27) |
|
||||
| **Database** | PostgreSQL 15 on 57.128.200.27 (localhost:5432) |
|
||||
| **Reverse Proxy** | Not used for production (direct DNS). NPM (10.22.68.250) serves staging only |
|
||||
| **Domain** | nordabiznes.pl (DNS managed via OVH) |
|
||||
| **SSL/TLS** | Let's Encrypt with automatic renewal |
|
||||
| **WSGI Server** | Gunicorn |
|
||||
@ -876,19 +876,23 @@ The project maintains two Git remotes for redundancy and deployment:
|
||||
| Remote | Repository | Purpose |
|
||||
|--------|------------|---------|
|
||||
| **origin** (GitHub) | `git@github.com:pienczyn/nordabiz.git` | Cloud backup, public access, CI/CD ready |
|
||||
| **inpi** (Gitea) | `git@10.22.68.180:maciejpi/nordabiz.git` | Internal backup, deployment source |
|
||||
| **inpi** (Gitea) | `git@10.22.68.180:maciejpi/nordabiz.git` | Internal backup |
|
||||
|
||||
**Gitea Access:** https://10.22.68.180:3000/ (requires HTTPS)
|
||||
|
||||
### Deployment Workflow
|
||||
|
||||
```
|
||||
┌─────────────┐ git push ┌─────────────┐ git pull ┌─────────────┐
|
||||
│ Development │ ────────────► │ Gitea │ ◄──────────── │ Production │
|
||||
│ (Local) │ │ (INPI) │ │ Server │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
│
|
||||
└──── git push ────► GitHub (cloud backup)
|
||||
┌─────────────┐ git push ┌─────────────┐
|
||||
│ Development │ ────────────► │ GitHub │ (cloud backup)
|
||||
│ (Local) │ └─────────────┘
|
||||
└──────┬──────┘
|
||||
│ rsync
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ OVH VPS │ (57.128.200.27)
|
||||
│ Production │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
#### Deployment Steps
|
||||
@ -896,24 +900,19 @@ The project maintains two Git remotes for redundancy and deployment:
|
||||
**1. Push Changes from Development:**
|
||||
|
||||
```bash
|
||||
# Push to both remotes (from local development)
|
||||
# Push to GitHub (and optionally Gitea)
|
||||
git push origin master && git push inpi master
|
||||
```
|
||||
|
||||
**2. Deploy to Production:**
|
||||
**2. Deploy to Production (via rsync — no .git on server):**
|
||||
|
||||
```bash
|
||||
# SSH to production server
|
||||
ssh maciejpi@10.22.68.249
|
||||
# Rsync to production server (from local dev machine)
|
||||
rsync -avz --exclude='.git' --exclude='venv' --exclude='.env' \
|
||||
./ maciejpi@57.128.200.27:/var/www/nordabiznes/
|
||||
|
||||
# Navigate to application directory
|
||||
cd /var/www/nordabiznes
|
||||
|
||||
# Pull latest changes (as www-data user)
|
||||
sudo -u www-data git pull
|
||||
|
||||
# Restart the application service
|
||||
sudo systemctl restart nordabiznes
|
||||
# SSH to restart
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl reload nordabiznes"
|
||||
|
||||
# Verify deployment
|
||||
curl -I https://nordabiznes.pl/health
|
||||
@ -934,13 +933,9 @@ Before deploying to production, always:
|
||||
|
||||
### Production Configuration
|
||||
|
||||
**Critical NPM Proxy Configuration:**
|
||||
**Network Configuration:**
|
||||
|
||||
```
|
||||
Nginx Proxy Manager (10.22.68.250) → Backend (10.22.68.249:5000) ✓
|
||||
```
|
||||
|
||||
**⚠️ IMPORTANT:** The NPM proxy must forward to port **5000** (Gunicorn), NOT port 80 (nginx). Forwarding to port 80 causes redirect loops.
|
||||
Production traffic goes directly to OVH VPS (57.128.200.27) — DNS A record points there. NPM (10.22.68.250) is used only for staging (staging.nordabiznes.pl).
|
||||
|
||||
**Environment Variables:**
|
||||
|
||||
@ -973,7 +968,7 @@ sudo systemctl start nordabiznes
|
||||
|
||||
**Database Backup:**
|
||||
|
||||
Production server uses Proxmox Backup Server for automated VM snapshots and disaster recovery.
|
||||
Production server (OVH VPS) uses local pg_dump backups with offsite sync to PBS (10.22.68.127).
|
||||
|
||||
**Health Check:**
|
||||
|
||||
@ -987,7 +982,7 @@ The application provides a health endpoint for monitoring:
|
||||
|
||||
```bash
|
||||
# Always SSH as maciejpi user (NOT root!)
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
```
|
||||
|
||||
**Test Accounts:**
|
||||
@ -1401,11 +1396,9 @@ python verify_all_companies_data.py # Check data quality
|
||||
# Development: Push to both remotes
|
||||
git push origin master && git push inpi master
|
||||
|
||||
# Production: Deploy to server
|
||||
ssh maciejpi@10.22.68.249
|
||||
cd /var/www/nordabiznes
|
||||
sudo -u www-data git pull
|
||||
sudo systemctl restart nordabiznes
|
||||
# Production: Deploy to server (rsync, no .git on server)
|
||||
rsync -avz --exclude='.git' --exclude='venv' --exclude='.env' ./ maciejpi@57.128.200.27:/var/www/nordabiznes/
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl reload nordabiznes"
|
||||
curl -I https://nordabiznes.pl/health # Verify deployment
|
||||
```
|
||||
|
||||
@ -1661,8 +1654,8 @@ python run_ai_quality_tests.py -v
|
||||
**Problem:** nordabiznes.pl not loading
|
||||
|
||||
```bash
|
||||
# SSH to production server
|
||||
ssh maciejpi@10.22.68.249
|
||||
# SSH to production server (OVH VPS)
|
||||
ssh maciejpi@57.128.200.27
|
||||
|
||||
# Check service status
|
||||
sudo systemctl status nordabiznes
|
||||
@ -1720,12 +1713,8 @@ sudo systemctl restart postgresql
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Always use www-data for application operations
|
||||
sudo -u www-data git pull
|
||||
sudo -u www-data python3 verify_all_companies_data.py
|
||||
|
||||
# SSH as maciejpi (NOT root!)
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
```
|
||||
|
||||
### API & External Service Issues
|
||||
@ -1847,8 +1836,7 @@ Additional documentation and resources for developers.
|
||||
|
||||
### Production Environment
|
||||
|
||||
- **Server:** NORDABIZ-01 (VM 249, IP: 10.22.68.249)
|
||||
- **Reverse Proxy:** R11-REVPROXY-01 (VM 119, IP: 10.22.68.250)
|
||||
- **Server:** OVH VPS inpi-vps-waw01 (IP: 57.128.200.27)
|
||||
- **URL:** https://nordabiznes.pl
|
||||
- **Status:** LIVE since 2025-11-23
|
||||
|
||||
|
||||
17
app.py
17
app.py
@ -218,6 +218,19 @@ app.config['SESSION_COOKIE_HTTPONLY'] = True
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
||||
|
||||
# Template filters
|
||||
from zoneinfo import ZoneInfo
|
||||
_WARSAW_TZ = ZoneInfo('Europe/Warsaw')
|
||||
_UTC_TZ = ZoneInfo('UTC')
|
||||
|
||||
@app.template_filter('local_time')
|
||||
def local_time_filter(dt, fmt='%d.%m.%Y %H:%M'):
|
||||
"""Convert naive UTC datetime to Europe/Warsaw and format."""
|
||||
if not dt:
|
||||
return ''
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=_UTC_TZ)
|
||||
return dt.astimezone(_WARSAW_TZ).strftime(fmt)
|
||||
|
||||
@app.template_filter('ensure_url')
|
||||
def ensure_url_filter(url):
|
||||
"""Ensure URL has http:// or https:// scheme"""
|
||||
@ -1436,7 +1449,7 @@ def send_error_notification(error, request_info):
|
||||
{traceback_str}
|
||||
|
||||
{'='*50}
|
||||
🔧 Sprawdź logi: ssh maciejpi@10.22.68.249 "sudo journalctl -u nordabiznes --since '10 minutes ago'"
|
||||
🔧 Sprawdź logi: ssh maciejpi@57.128.200.27 "sudo journalctl -u nordabiznes --since '10 minutes ago'"
|
||||
"""
|
||||
|
||||
body_html = f"""<!DOCTYPE html>
|
||||
@ -1466,7 +1479,7 @@ def send_error_notification(error, request_info):
|
||||
</div>
|
||||
<div style="margin-top: 20px; padding: 15px; background: #1e3a5f; border-radius: 8px;">
|
||||
<div style="color: #60a5fa;">🔧 <strong>Sprawdź logi:</strong></div>
|
||||
<code style="display: block; margin-top: 10px; color: #34d399; word-break: break-all;">ssh maciejpi@10.22.68.249 "sudo journalctl -u nordabiznes --since '10 minutes ago'"</code>
|
||||
<code style="display: block; margin-top: 10px; color: #34d399; word-break: break-all;">ssh maciejpi@57.128.200.27 "sudo journalctl -u nordabiznes --since '10 minutes ago'"</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -443,7 +443,7 @@ def admin_status():
|
||||
# ===== SERVERS PING =====
|
||||
servers_status = []
|
||||
servers_to_ping = [
|
||||
('NORDABIZ-01', '10.22.68.249'),
|
||||
('NORDABIZ-01', '57.128.200.27'),
|
||||
('R11-REVPROXY-01', '10.22.68.250'),
|
||||
('R11-DNS-01', '10.22.68.171'),
|
||||
('R11-GIT-INPI', '10.22.68.180'),
|
||||
@ -507,7 +507,7 @@ def admin_status():
|
||||
{'name': 'systemd', 'version': '255', 'icon': '⚙️', 'category': 'Service Manager'},
|
||||
],
|
||||
'servers': [
|
||||
{'name': 'NORDABIZ-01', 'ip': '10.22.68.249', 'icon': '🖥️', 'role': 'App Server (VM 249)'},
|
||||
{'name': 'NORDABIZ-01', 'ip': '57.128.200.27', 'icon': '🖥️', 'role': 'App Server (VM 249)'},
|
||||
{'name': 'R11-REVPROXY-01', 'ip': '10.22.68.250', 'icon': '🔀', 'role': 'Reverse Proxy (VM 119)'},
|
||||
{'name': 'R11-DNS-01', 'ip': '10.22.68.171', 'icon': '📡', 'role': 'DNS Server (VM 122)'},
|
||||
{'name': 'R11-GIT-INPI', 'ip': '10.22.68.180', 'icon': '📦', 'role': 'Git Server (VM 180)'},
|
||||
|
||||
@ -12,19 +12,19 @@ database/
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Instalacja na NORDABIZ-01
|
||||
## 🚀 Instalacja na OVH VPS (inpi-vps-waw01)
|
||||
|
||||
### Krok 1: Instalacja PostgreSQL
|
||||
|
||||
```bash
|
||||
# SSH do serwera
|
||||
ssh root@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
|
||||
# Aktualizacja pakietów
|
||||
apt update
|
||||
sudo apt update
|
||||
|
||||
# Instalacja PostgreSQL 15
|
||||
apt install -y postgresql-15 postgresql-contrib-15
|
||||
sudo apt install -y postgresql-15 postgresql-contrib-15
|
||||
|
||||
# Sprawdzenie statusu
|
||||
systemctl status postgresql
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
# Norda Biznes - Deployment Checklist
|
||||
|
||||
**Version:** 1.0
|
||||
**Last Updated:** 2026-01-02
|
||||
**Environment:** Production (NORDABIZ-01, IP: 10.22.68.249)
|
||||
**Last Updated:** 2026-04-04
|
||||
**Environment:** Production (OVH VPS inpi-vps-waw01, IP: 57.128.200.27)
|
||||
**Audience:** DevOps, SysAdmins
|
||||
|
||||
---
|
||||
@ -75,23 +75,23 @@ This checklist ensures safe, repeatable deployments to production with minimal r
|
||||
- [ ] Secrets stored securely (LastPass, 1Password, vault)
|
||||
|
||||
### Access & Permissions
|
||||
- [ ] SSH access to NORDABIZ-01 verified
|
||||
- [ ] SSH access to OVH VPS verified
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "echo OK"
|
||||
ssh maciejpi@57.128.200.27 "echo OK"
|
||||
```
|
||||
- [ ] PostgreSQL credentials verified (not displayed)
|
||||
```bash
|
||||
psql -h 10.22.68.249 -U nordabiz_app -d nordabiz -c "SELECT version();"
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz -c 'SELECT version();'"
|
||||
```
|
||||
- [ ] www-data user can execute deployment scripts
|
||||
- [ ] maciejpi user can execute deployment scripts
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "sudo -l | grep -E 'systemctl|psql'"
|
||||
ssh maciejpi@57.128.200.27 "sudo -l | grep -E 'systemctl|psql'"
|
||||
```
|
||||
|
||||
### Backup Location
|
||||
- [ ] Backup destination has adequate free space
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "df -h /var/backups"
|
||||
ssh maciejpi@57.128.200.27 "df -h /var/backups"
|
||||
# Minimum 2GB free recommended
|
||||
```
|
||||
- [ ] Backup location is accessible and writable
|
||||
@ -103,12 +103,12 @@ This checklist ensures safe, repeatable deployments to production with minimal r
|
||||
### Application Status
|
||||
- [ ] Current application is running and healthy
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "sudo systemctl status nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl status nordabiznes"
|
||||
# Status: active (running)
|
||||
```
|
||||
- [ ] Application logs show no recent errors
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "sudo tail -50 /var/log/nordabiznes/*.log | grep -i error"
|
||||
ssh maciejpi@57.128.200.27 "sudo tail -50 /var/log/nordabiznes/*.log | grep -i error"
|
||||
# Should be empty or only non-critical errors
|
||||
```
|
||||
- [ ] Health check endpoint responding
|
||||
@ -120,25 +120,25 @@ This checklist ensures safe, repeatable deployments to production with minimal r
|
||||
### Database Status
|
||||
- [ ] PostgreSQL is running
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "sudo systemctl status postgresql"
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl status postgresql"
|
||||
# Status: active (running)
|
||||
```
|
||||
- [ ] Database is accessible
|
||||
```bash
|
||||
psql -h 10.22.68.249 -U nordabiz_app -d nordabiz -c "SELECT NOW();"
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz -c 'SELECT NOW();'"
|
||||
```
|
||||
- [ ] No long-running transactions
|
||||
```bash
|
||||
psql -h 10.22.68.249 -U nordabiz_app -d nordabiz -c "
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz -c \"
|
||||
SELECT pid, usename, state, query
|
||||
FROM pg_stat_activity
|
||||
WHERE state != 'idle' AND duration > interval '5 minutes';"
|
||||
WHERE state != 'idle' AND duration > interval '5 minutes';\""
|
||||
# Should be empty
|
||||
```
|
||||
- [ ] Database size recorded
|
||||
```bash
|
||||
psql -h 10.22.68.249 -U nordabiz_app -d nordabiz -c "
|
||||
SELECT pg_size_pretty(pg_database_size('nordabiz'));"
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz -c \"
|
||||
SELECT pg_size_pretty(pg_database_size('nordabiz'));\""
|
||||
# Record this value
|
||||
```
|
||||
|
||||
@ -148,7 +148,7 @@ This checklist ensures safe, repeatable deployments to production with minimal r
|
||||
- Best deployment time: off-peak (11:00-12:00, 14:00-17:00)
|
||||
- [ ] No ongoing data imports or batch jobs
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "ps aux | grep -i 'python.*import'"
|
||||
ssh maciejpi@57.128.200.27 "ps aux | grep -i 'python.*import'"
|
||||
# Should be empty
|
||||
```
|
||||
|
||||
@ -165,7 +165,7 @@ This checklist ensures safe, repeatable deployments to production with minimal r
|
||||
- [ ] Full database backup
|
||||
```bash
|
||||
BACKUP_FILE="$HOME/backup_before_deployment_$(date +%Y%m%d_%H%M%S).sql"
|
||||
ssh maciejpi@10.22.68.249 "sudo -u www-data pg_dump -U nordabiz_app -d nordabiz" > "$BACKUP_FILE"
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres pg_dump -d nordabiz" > "$BACKUP_FILE"
|
||||
# Verify backup was created
|
||||
ls -lh "$BACKUP_FILE"
|
||||
# Minimum size: >5MB (should contain all schema and data)
|
||||
@ -180,10 +180,10 @@ This checklist ensures safe, repeatable deployments to production with minimal r
|
||||
- [ ] Backup can be restored (test on separate database)
|
||||
```bash
|
||||
# Optional: Create test database and restore
|
||||
psql -h 10.22.68.249 -U nordabiz_app -c "CREATE DATABASE nordabiz_test;"
|
||||
psql -h 10.22.68.249 -U nordabiz_app -d nordabiz_test < "$BACKUP_FILE"
|
||||
psql -h 10.22.68.249 -U nordabiz_app -d nordabiz_test -c "SELECT COUNT(*) FROM companies;"
|
||||
# Then drop test database: DROP DATABASE nordabiz_test;
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres psql -c 'CREATE DATABASE nordabiz_test;'"
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz_test" < "$BACKUP_FILE"
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz_test -c 'SELECT COUNT(*) FROM companies;'"
|
||||
# Then drop test database: sudo -u postgres psql -c "DROP DATABASE nordabiz_test;"
|
||||
```
|
||||
- [ ] Backup copied to redundant location
|
||||
```bash
|
||||
@ -254,15 +254,15 @@ This checklist ensures safe, repeatable deployments to production with minimal r
|
||||
### Pre-SQL Execution
|
||||
- [ ] Maintenance mode enabled (optional but recommended)
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "
|
||||
ssh maciejpi@57.128.200.27 "
|
||||
# Temporarily disable non-critical endpoints
|
||||
# Or show 'maintenance' page
|
||||
"
|
||||
```
|
||||
- [ ] Current user count recorded
|
||||
```bash
|
||||
psql -h 10.22.68.249 -U nordabiz_app -d nordabiz -c "
|
||||
SELECT COUNT(DISTINCT session_key) FROM django_session WHERE expire_date > NOW();"
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz -c \"
|
||||
SELECT COUNT(DISTINCT session_key) FROM django_session WHERE expire_date > NOW();\""
|
||||
# Current active users: _______
|
||||
```
|
||||
|
||||
@ -271,7 +271,7 @@ This checklist ensures safe, repeatable deployments to production with minimal r
|
||||
**IMPORTANT:** Execute SQL scripts in this exact order within a transaction:
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 << 'DEPLOY_EOF'
|
||||
ssh maciejpi@57.128.200.27 << 'DEPLOY_EOF'
|
||||
|
||||
# Start deployment
|
||||
echo "=== DEPLOYMENT STARTED at $(date) ==="
|
||||
@ -279,14 +279,14 @@ BACKUP_FILE="$HOME/backup_pre_deployment_$(date +%Y%m%d_%H%M%S).sql"
|
||||
|
||||
# Step 1: Full backup BEFORE any changes
|
||||
echo "STEP 1: Creating backup..."
|
||||
sudo -u www-data pg_dump -U nordabiz_app -d nordabiz > "$BACKUP_FILE"
|
||||
sudo -u postgres pg_dump -d nordabiz > "$BACKUP_FILE"
|
||||
echo "✓ Backup: $BACKUP_FILE"
|
||||
|
||||
# Step 2: Begin transaction (all SQL changes in one transaction)
|
||||
echo "STEP 2: Executing SQL migrations..."
|
||||
|
||||
# Execute schema migrations (in order of dependency)
|
||||
sudo -u www-data psql -U nordabiz_app -d nordabiz << 'SQL'
|
||||
sudo -u postgres psql -d nordabiz << 'SQL'
|
||||
BEGIN;
|
||||
|
||||
-- 2.1 News tables migration (if not already applied)
|
||||
@ -314,7 +314,7 @@ echo "✓ SQL migrations completed"
|
||||
|
||||
# Step 3: Verify data integrity
|
||||
echo "STEP 3: Verifying data integrity..."
|
||||
sudo -u www-data psql -U nordabiz_app -d nordabiz << 'SQL'
|
||||
sudo -u postgres psql -d nordabiz << 'SQL'
|
||||
-- Check for orphaned foreign keys
|
||||
SELECT 'Checking foreign key integrity...' AS status;
|
||||
|
||||
@ -327,7 +327,7 @@ SQL
|
||||
|
||||
# Step 4: Update indexes and statistics
|
||||
echo "STEP 4: Optimizing database..."
|
||||
sudo -u www-data psql -U nordabiz_app -d nordabiz << 'SQL'
|
||||
sudo -u postgres psql -d nordabiz << 'SQL'
|
||||
-- Update statistics for query planner
|
||||
ANALYZE;
|
||||
|
||||
@ -341,14 +341,14 @@ echo "✓ Database optimized"
|
||||
echo "STEP 5: Deploying application..."
|
||||
cd /var/www/nordabiznes
|
||||
|
||||
# Pull latest code (if using git)
|
||||
sudo -u www-data git pull origin master
|
||||
# Deploy via rsync (no .git on OVH VPS)
|
||||
# Run from LOCAL machine: rsync -avz --exclude='.git' --exclude='.env' --exclude='__pycache__' ./ maciejpi@57.128.200.27:/var/www/nordabiznes/
|
||||
|
||||
# Update dependencies
|
||||
sudo -u www-data /var/www/nordabiznes/venv/bin/pip install -q -r requirements.txt
|
||||
sudo /var/www/nordabiznes/venv/bin/pip install -q -r requirements.txt
|
||||
|
||||
# Validate Python syntax
|
||||
sudo -u www-data /var/www/nordabiznes/venv/bin/python -m py_compile app.py
|
||||
/var/www/nordabiznes/venv/bin/python -m py_compile app.py
|
||||
|
||||
echo "✓ Application files updated"
|
||||
|
||||
@ -363,7 +363,7 @@ if sudo systemctl is-active --quiet nordabiznes; then
|
||||
else
|
||||
echo "✗ ERROR: Application failed to start"
|
||||
echo "ROLLING BACK DATABASE..."
|
||||
sudo -u www-data psql -U nordabiz_app -d nordabiz < "$BACKUP_FILE"
|
||||
sudo -u postgres psql -d nordabiz < "$BACKUP_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@ -401,15 +401,15 @@ If using separate SSH sessions, execute in this order:
|
||||
|
||||
```bash
|
||||
# Session 1: Create backup
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
BACKUP_FILE="$HOME/backup_pre_deployment_$(date +%Y%m%d_%H%M%S).sql"
|
||||
sudo -u www-data pg_dump -U nordabiz_app -d nordabiz > "$BACKUP_FILE"
|
||||
sudo -u postgres pg_dump -d nordabiz > "$BACKUP_FILE"
|
||||
echo "Backup saved to: $BACKUP_FILE"
|
||||
exit
|
||||
|
||||
# Session 2: Execute SQL
|
||||
ssh maciejpi@10.22.68.249
|
||||
sudo -u www-data psql -U nordabiz_app -d nordabiz << 'EOF'
|
||||
ssh maciejpi@57.128.200.27
|
||||
sudo -u postgres psql -d nordabiz << 'EOF'
|
||||
BEGIN;
|
||||
\i /var/www/nordabiznes/database/migrate_news_tables.sql
|
||||
-- ... additional SQL ...
|
||||
@ -417,8 +417,8 @@ COMMIT;
|
||||
EOF
|
||||
|
||||
# Session 3: Validate
|
||||
ssh maciejpi@10.22.68.249
|
||||
sudo -u www-data psql -U nordabiz_app -d nordabiz -c "SELECT COUNT(*) FROM company_news;"
|
||||
ssh maciejpi@57.128.200.27
|
||||
sudo -u postgres psql -d nordabiz -c "SELECT COUNT(*) FROM company_news;"
|
||||
exit
|
||||
```
|
||||
|
||||
@ -430,15 +430,15 @@ exit
|
||||
```
|
||||
- [ ] Database size within expected range
|
||||
```bash
|
||||
psql -h 10.22.68.249 -U nordabiz_app -d nordabiz -c "
|
||||
SELECT pg_size_pretty(pg_database_size('nordabiz'));"
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz -c \"
|
||||
SELECT pg_size_pretty(pg_database_size('nordabiz'));\""
|
||||
# Compare to pre-deployment size (should be similar ±10%)
|
||||
```
|
||||
- [ ] New tables/columns exist (if schema changes)
|
||||
```bash
|
||||
psql -h 10.22.68.249 -U nordabiz_app -d nordabiz -c "
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz -c \"
|
||||
SELECT * FROM information_schema.tables
|
||||
WHERE table_name IN ('company_news', 'user_notifications');"
|
||||
WHERE table_name IN ('company_news', 'user_notifications');\""
|
||||
```
|
||||
|
||||
---
|
||||
@ -446,20 +446,22 @@ exit
|
||||
## Phase 5: Application Deployment
|
||||
|
||||
### Code Deployment
|
||||
- [ ] Application code pulled from Git
|
||||
- [ ] Application code deployed via rsync (no .git on OVH VPS)
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && sudo -u www-data git pull origin master"
|
||||
# Run from LOCAL machine (project root):
|
||||
rsync -avz --exclude='.git' --exclude='.env' --exclude='__pycache__' --exclude='venv' \
|
||||
./ maciejpi@57.128.200.27:/var/www/nordabiznes/
|
||||
```
|
||||
- [ ] Python dependencies installed
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "
|
||||
sudo -u www-data /var/www/nordabiznes/venv/bin/pip install -q -r /var/www/nordabiznes/requirements.txt
|
||||
ssh maciejpi@57.128.200.27 "
|
||||
sudo /var/www/nordabiznes/venv/bin/pip install -q -r /var/www/nordabiznes/requirements.txt
|
||||
"
|
||||
```
|
||||
- [ ] Application syntax validated
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "
|
||||
sudo -u www-data /var/www/nordabiznes/venv/bin/python -m py_compile /var/www/nordabiznes/app.py
|
||||
ssh maciejpi@57.128.200.27 "
|
||||
/var/www/nordabiznes/venv/bin/python -m py_compile /var/www/nordabiznes/app.py
|
||||
echo $? # Should return 0 (success)
|
||||
"
|
||||
```
|
||||
@ -467,16 +469,16 @@ exit
|
||||
### Service Restart
|
||||
- [ ] Application service restarted
|
||||
```bash
|
||||
ssh maciejpi@10.22.69.249 "sudo systemctl restart nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl restart nordabiznes"
|
||||
```
|
||||
- [ ] Service started successfully
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "sudo systemctl is-active nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl is-active nordabiznes"
|
||||
# Expected: active
|
||||
```
|
||||
- [ ] Service status verified
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "sudo systemctl status nordabiznes --no-pager | head -10"
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl status nordabiznes --no-pager | head -10"
|
||||
```
|
||||
|
||||
### Initial Health Checks
|
||||
@ -491,7 +493,7 @@ exit
|
||||
```
|
||||
- [ ] No critical errors in logs
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "
|
||||
ssh maciejpi@57.128.200.27 "
|
||||
sudo tail -30 /var/log/nordabiznes/app.log | grep -i 'ERROR\|CRITICAL'
|
||||
"
|
||||
# Should be empty or only non-critical warnings
|
||||
@ -521,16 +523,16 @@ exit
|
||||
### Database Queries
|
||||
- [ ] New tables accessible
|
||||
```bash
|
||||
psql -h 10.22.68.249 -U nordabiz_app -d nordabiz -c "
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz -c '
|
||||
SELECT * FROM company_news LIMIT 1;
|
||||
SELECT * FROM user_notifications LIMIT 1;"
|
||||
SELECT * FROM user_notifications LIMIT 1;'"
|
||||
```
|
||||
- [ ] Search indexes working
|
||||
```bash
|
||||
psql -h 10.22.68.249 -U nordabiz_app -d nordabiz -c "
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz -c \"
|
||||
EXPLAIN ANALYZE
|
||||
SELECT * FROM companies
|
||||
WHERE name ILIKE '%pixlab%' LIMIT 10;"
|
||||
WHERE name ILIKE '%pixlab%' LIMIT 10;\""
|
||||
# Should show "Index Scan" (not "Seq Scan")
|
||||
```
|
||||
|
||||
@ -542,8 +544,8 @@ exit
|
||||
```
|
||||
- [ ] Database query response time acceptable
|
||||
```bash
|
||||
time psql -h 10.22.68.249 -U nordabiz_app -d nordabiz -c "
|
||||
SELECT * FROM companies WHERE category_id = 1 LIMIT 50;"
|
||||
ssh maciejpi@57.128.200.27 "time sudo -u postgres psql -d nordabiz -c '
|
||||
SELECT * FROM companies WHERE category_id = 1 LIMIT 50;'"
|
||||
# real time should be < 100ms
|
||||
```
|
||||
- [ ] API endpoints respond within SLA
|
||||
@ -578,22 +580,22 @@ exit
|
||||
### Post-Deployment Monitoring (2 hours)
|
||||
- [ ] Monitor application logs for errors
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "
|
||||
ssh maciejpi@57.128.200.27 "
|
||||
tail -f /var/log/nordabiznes/app.log
|
||||
"
|
||||
# Watch for ERROR, CRITICAL, EXCEPTION
|
||||
```
|
||||
- [ ] Monitor database load
|
||||
```bash
|
||||
psql -h 10.22.68.249 -U nordabiz_app -d nordabiz -c "
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz -c \"
|
||||
SELECT pid, usename, state, query
|
||||
FROM pg_stat_activity
|
||||
WHERE datname = 'nordabiz' AND state != 'idle';"
|
||||
WHERE datname = 'nordabiz' AND state != 'idle';\""
|
||||
# Should be minimal
|
||||
```
|
||||
- [ ] Monitor system resources
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "top -b -n 1 | head -15"
|
||||
ssh maciejpi@57.128.200.27 "top -b -n 1 | head -15"
|
||||
```
|
||||
|
||||
### 24-Hour Follow-up
|
||||
@ -635,13 +637,13 @@ echo ""
|
||||
|
||||
# Step 1: Stop application
|
||||
echo "STEP 1: Stopping application..."
|
||||
ssh maciejpi@10.22.68.249 "sudo systemctl stop nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl stop nordabiznes"
|
||||
sleep 3
|
||||
|
||||
# Step 2: Restore database
|
||||
echo "STEP 2: Restoring database from backup..."
|
||||
ssh maciejpi@10.22.68.249 "
|
||||
sudo -u www-data psql -U nordabiz_app -d nordabiz << 'SQL'
|
||||
ssh maciejpi@57.128.200.27 "
|
||||
sudo -u postgres psql -d nordabiz << 'SQL'
|
||||
-- Drop all changes
|
||||
DROP TABLE IF EXISTS company_news CASCADE;
|
||||
DROP TABLE IF EXISTS user_notifications CASCADE;
|
||||
@ -649,7 +651,7 @@ DROP TABLE IF EXISTS user_notifications CASCADE;
|
||||
SQL
|
||||
|
||||
# Restore from backup
|
||||
sudo -u www-data psql -U nordabiz_app -d nordabiz < $BACKUP_FILE
|
||||
sudo -u postgres psql -d nordabiz < $BACKUP_FILE
|
||||
"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
@ -662,11 +664,8 @@ echo "✓ Database restored"
|
||||
|
||||
# Step 3: Restart application (previous version)
|
||||
echo "STEP 3: Restarting application..."
|
||||
ssh maciejpi@10.22.68.249 "
|
||||
cd /var/www/nordabiznes
|
||||
sudo -u www-data git checkout HEAD~1 # Revert to previous commit
|
||||
sudo systemctl start nordabiznes
|
||||
"
|
||||
# Re-deploy previous version via rsync from local backup, then:
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl start nordabiznes"
|
||||
|
||||
sleep 3
|
||||
|
||||
@ -711,44 +710,44 @@ echo "4. Schedule post-mortem review"
|
||||
curl -s https://nordabiznes.pl/health | jq .
|
||||
|
||||
# Database connection
|
||||
psql -h 10.22.68.249 -U nordabiz_app -d nordabiz -c "SELECT 1;"
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz -c 'SELECT 1;'"
|
||||
|
||||
# Service status
|
||||
ssh maciejpi@10.22.68.249 "sudo systemctl status nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl status nordabiznes"
|
||||
|
||||
# Log tailing
|
||||
ssh maciejpi@10.22.68.249 "sudo tail -f /var/log/nordabiznes/app.log"
|
||||
ssh maciejpi@57.128.200.27 "sudo tail -f /var/log/nordabiznes/app.log"
|
||||
|
||||
# Database statistics
|
||||
psql -h 10.22.68.249 -U nordabiz_app -d nordabiz -c "
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz -c \"
|
||||
SELECT
|
||||
schemaname,
|
||||
tablename,
|
||||
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
|
||||
FROM pg_tables
|
||||
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC LIMIT 10;"
|
||||
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC LIMIT 10;\""
|
||||
```
|
||||
|
||||
### Monitoring Queries
|
||||
```bash
|
||||
# Active connections
|
||||
psql -h 10.22.68.249 -U nordabiz_app -d nordabiz -c "
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz -c '
|
||||
SELECT datname, usename, count(*)
|
||||
FROM pg_stat_activity
|
||||
GROUP BY datname, usename;"
|
||||
GROUP BY datname, usename;'"
|
||||
|
||||
# Long-running queries
|
||||
psql -h 10.22.68.249 -U nordabiz_app -d nordabiz -c "
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz -c \"
|
||||
SELECT pid, usename, query, query_start
|
||||
FROM pg_stat_activity
|
||||
WHERE query != 'autovacuum'
|
||||
AND query_start < NOW() - interval '5 minutes';"
|
||||
AND query_start < NOW() - interval '5 minutes';\""
|
||||
|
||||
# Index usage
|
||||
psql -h 10.22.68.249 -U nordabiz_app -d nordabiz -c "
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz -c '
|
||||
SELECT schemaname, tablename, indexname, idx_scan
|
||||
FROM pg_stat_user_indexes
|
||||
ORDER BY idx_scan DESC LIMIT 20;"
|
||||
ORDER BY idx_scan DESC LIMIT 20;'"
|
||||
```
|
||||
|
||||
---
|
||||
@ -761,7 +760,7 @@ psql -h 10.22.68.249 -U nordabiz_app -d nordabiz -c "
|
||||
**Solution:**
|
||||
1. Check logs: `sudo journalctl -xe -u nordabiznes | tail -50`
|
||||
2. Check syntax: `python -m py_compile /var/www/nordabiznes/app.py`
|
||||
3. Check database connection: `psql -h 10.22.68.249 -U nordabiz_app -d nordabiz -c "SELECT 1;"`
|
||||
3. Check database connection: `ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz -c 'SELECT 1;'"`
|
||||
4. If database is issue, execute rollback script
|
||||
5. If code is issue, revert Git commit and restart
|
||||
|
||||
@ -834,11 +833,12 @@ Approval:
|
||||
- **Migration Scripts:** `/var/www/nordabiznes/database/*.sql`
|
||||
- **Application Logs:** `/var/log/nordabiznes/app.log`
|
||||
- **PostgreSQL Logs:** `sudo journalctl -u postgresql --no-pager`
|
||||
- **Production Server:** `10.22.68.249` (NORDABIZ-01)
|
||||
- **VPN Required:** FortiGate SSL-VPN (85.237.177.83)
|
||||
- **Production Server:** `57.128.200.27` (OVH VPS inpi-vps-waw01)
|
||||
- **Deploy method:** rsync (no .git on VPS), NOT git pull
|
||||
- **DB access:** `sudo -u postgres psql -d nordabiz` (.env is root-owned)
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2026-01-02
|
||||
**Last Updated:** 2026-04-04
|
||||
**Maintained By:** Norda Biznes Development Team
|
||||
**Next Review:** 2026-04-02 (quarterly)
|
||||
**Next Review:** 2026-07-04 (quarterly)
|
||||
|
||||
@ -47,16 +47,15 @@ git branch -d auto-claude/<task-id> # Usuń branch
|
||||
| Hourly (lokalnie) | `/var/backups/nordabiz/hourly/` | 24h |
|
||||
| Daily (lokalnie) | `/var/backups/nordabiz/daily/` | 30 dni |
|
||||
| Offsite (PBS) | `10.22.68.127:/backup/nordabiz/` | 30 dni |
|
||||
| VM Snapshots | Proxmox | 3 snapshoty |
|
||||
|
||||
### Szybkie przywracanie
|
||||
|
||||
```bash
|
||||
# Lista dostępnych backupów
|
||||
ssh maciejpi@10.22.68.249 "ls -lt /var/backups/nordabiz/hourly/ | head -5"
|
||||
ssh maciejpi@57.128.200.27 "ls -lt /var/backups/nordabiz/hourly/ | head -5"
|
||||
|
||||
# Restore z backupu
|
||||
ssh maciejpi@10.22.68.249 "sudo /var/www/nordabiznes/scripts/dr-restore.sh /var/backups/nordabiz/hourly/nordabiz_YYYYMMDD_HH.dump"
|
||||
ssh maciejpi@57.128.200.27 "sudo /var/www/nordabiznes/scripts/dr-restore.sh /var/backups/nordabiz/hourly/nordabiz_YYYYMMDD_HH.dump"
|
||||
|
||||
# Weryfikacja
|
||||
curl -I https://nordabiznes.pl/health
|
||||
|
||||
@ -34,7 +34,7 @@ API_KEY = 'AIzaSyAbc123...'
|
||||
DATABASE_URL = os.getenv('DATABASE_URL', 'postgresql://user:CHANGE_ME@localhost/nordabiz')
|
||||
|
||||
# ❌ BŁĘDNIE - wartość produkcyjna jako fallback:
|
||||
DATABASE_URL = os.getenv('DATABASE_URL', 'postgresql://user:RealPassword@10.22.68.249/nordabiz')
|
||||
DATABASE_URL = os.getenv('DATABASE_URL', 'postgresql://user:RealPassword@57.128.200.27/nordabiz')
|
||||
```
|
||||
|
||||
### 3. Przechowuj credentials w plikach .env
|
||||
@ -137,9 +137,9 @@ grep -r "postgresql://.*:.*@" --include="*.py" . | grep -v "CHANGE_ME" | grep -v
|
||||
### Uprawnienia plików
|
||||
|
||||
```bash
|
||||
# Tylko właściciel może czytać plik .env
|
||||
chmod 600 /var/www/nordabiznes/.env
|
||||
chown www-data:www-data /var/www/nordabiznes/.env
|
||||
# Tylko root może czytać plik .env (OVH VPS — .env jest root-owned)
|
||||
sudo chmod 600 /var/www/nordabiznes/.env
|
||||
sudo chown root:root /var/www/nordabiznes/.env
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@ -216,7 +216,7 @@ Skrypty w `scripts/` muszą używać **localhost (127.0.0.1)** do połączenia z
|
||||
DATABASE_URL = 'postgresql://nordabiz_app:<PASSWORD_FROM_ENV>@127.0.0.1:5432/nordabiz'
|
||||
|
||||
# BŁĘDNIE (PostgreSQL nie akceptuje zewnętrznych połączeń):
|
||||
DATABASE_URL = 'postgresql://nordabiz_app:<PASSWORD>@10.22.68.249:5432/nordabiz'
|
||||
DATABASE_URL = 'postgresql://nordabiz_app:<PASSWORD>@57.128.200.27:5432/nordabiz'
|
||||
```
|
||||
|
||||
**Pliki z konfiguracją bazy:**
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
# NordaBiz Disaster Recovery Playbook
|
||||
|
||||
**Wersja:** 1.0
|
||||
**Wersja:** 1.1
|
||||
**Data utworzenia:** 2026-02-02
|
||||
**Ostatnia aktualizacja:** 2026-02-02
|
||||
**Ostatnia aktualizacja:** 2026-04-04
|
||||
|
||||
> **UWAGA (2026-04-04):** Produkcja przeniesiona z OVH VPS inpi-vps-waw01 (VM 249, 57.128.200.27) na OVH VPS (57.128.200.27, hostname inpi-vps-waw01). Deploy via rsync (brak .git na serwerze). Migracje: `sudo -u postgres psql nordabiz`. .env jest root-owned, skrypty wymagają `sudo`.
|
||||
|
||||
---
|
||||
|
||||
@ -43,7 +45,7 @@
|
||||
|
||||
## Lokalizacje backupów
|
||||
|
||||
### Backup lokalny (NORDABIZ-01)
|
||||
### Backup lokalny (OVH VPS — inpi-vps-waw01)
|
||||
|
||||
```
|
||||
/var/backups/nordabiz/
|
||||
@ -68,12 +70,13 @@
|
||||
└── config/ # Mirror konfiguracji
|
||||
```
|
||||
|
||||
### VM Snapshots (Proxmox)
|
||||
### VM Snapshots (Proxmox — TYLKO STAGING)
|
||||
|
||||
- **Lokalizacja:** Proxmox VE (hypervisor)
|
||||
- **VM ID:** 249 (NORDABIZ-01), 119 (R11-REVPROXY-01)
|
||||
- **VM ID:** 248 (NORDABIZ-STAGING-01), 119 (R11-REVPROXY-01)
|
||||
- **Storage:** local-lvm lub PBS
|
||||
- **Dostęp:** Proxmox Web UI (https://10.22.68.10:8006)
|
||||
- **UWAGA:** Produkcja nie jest na Proxmox — snapshoty dotyczą tylko staging i reverse proxy
|
||||
|
||||
---
|
||||
|
||||
@ -96,34 +99,27 @@
|
||||
|
||||
---
|
||||
|
||||
### Scenariusz 2: Awaria VM (NORDABIZ-01)
|
||||
### Scenariusz 2: Awaria serwera produkcyjnego (OVH VPS)
|
||||
|
||||
**Objawy:**
|
||||
- Brak odpowiedzi SSH
|
||||
- Brak odpowiedzi SSH na 57.128.200.27
|
||||
- HTTP 502 na https://nordabiznes.pl
|
||||
- VM nie odpowiada w Proxmox
|
||||
|
||||
**Procedura:**
|
||||
|
||||
#### Opcja A: Restart VM
|
||||
1. Zaloguj się do Proxmox: https://10.22.68.10:8006
|
||||
2. VM 249 → Stop → Start
|
||||
3. Poczekaj 2-3 minuty
|
||||
4. Zweryfikuj: `curl -I https://nordabiznes.pl/health`
|
||||
#### Opcja A: Restart VPS via OVH Panel
|
||||
1. Zaloguj się do OVH Manager: https://www.ovh.com/manager/
|
||||
2. Znajdź VPS inpi-vps-waw01
|
||||
3. Wykonaj reboot
|
||||
4. Poczekaj 2-3 minuty
|
||||
5. Zweryfikuj: `curl -I https://nordabiznes.pl/health`
|
||||
|
||||
#### Opcja B: Rollback do snapshotu
|
||||
1. Proxmox → VM 249 → Snapshots
|
||||
2. Wybierz ostatni działający snapshot
|
||||
3. Kliknij "Rollback"
|
||||
4. Start VM
|
||||
5. Zweryfikuj
|
||||
|
||||
#### Opcja C: Pełne odtworzenie (nowa VM)
|
||||
1. Utwórz nową VM w Proxmox (4 vCPU, 8GB RAM, 100GB SSD)
|
||||
#### Opcja B: Pełne odtworzenie (nowy VPS lub VM)
|
||||
1. Utwórz nowy VPS w OVH lub VM w Proxmox
|
||||
2. Zainstaluj Ubuntu 22.04 LTS
|
||||
3. Postępuj zgodnie z sekcją [Pełne odtworzenie systemu](#pełne-odtworzenie-systemu)
|
||||
|
||||
**RTO:** 5 min (opcja A), 10 min (opcja B), 60 min (opcja C)
|
||||
**RTO:** 5 min (opcja A), 60 min (opcja B)
|
||||
|
||||
---
|
||||
|
||||
@ -214,18 +210,15 @@ sudo /var/www/nordabiznes/scripts/dr-restore.sh /tmp/nordabiz_20260201.dump
|
||||
|
||||
Wykonaj gdy VM jest całkowicie niedostępna lub skompromitowana.
|
||||
|
||||
#### Krok 1: Przygotowanie nowej VM
|
||||
#### Krok 1: Przygotowanie nowego serwera
|
||||
|
||||
```bash
|
||||
# Na Proxmox - utwórz VM:
|
||||
# - ID: 249 (lub nowy)
|
||||
# Na OVH Manager - utwórz VPS lub na Proxmox - utwórz VM:
|
||||
# - CPU: 4 vCPU
|
||||
# - RAM: 8 GB
|
||||
# - Disk: 100 GB SSD
|
||||
# - Network: vmbr0
|
||||
# - IP: 10.22.68.249/24
|
||||
# - Gateway: 10.22.68.1
|
||||
# - OS: Ubuntu 22.04 LTS
|
||||
# - Publiczny IP (jeśli OVH VPS) lub IP wewnętrzny (jeśli Proxmox)
|
||||
```
|
||||
|
||||
#### Krok 2: Instalacja pakietów
|
||||
@ -261,15 +254,16 @@ sudo -u postgres psql -d nordabiz -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN S
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/www/nordabiznes
|
||||
sudo chown www-data:www-data /var/www/nordabiznes
|
||||
sudo chown maciejpi:maciejpi /var/www/nordabiznes
|
||||
|
||||
# Clone z Gitea
|
||||
sudo -u www-data git clone https://10.22.68.180:3000/maciejpi/nordabiz.git /var/www/nordabiznes
|
||||
# Sync z lokalnej maszyny deweloperskiej (brak .git na serwerze!)
|
||||
rsync -avz --exclude='.git' --exclude='venv' --exclude='.env' \
|
||||
/path/to/nordabiz/ maciejpi@57.128.200.27:/var/www/nordabiznes/
|
||||
|
||||
# Virtualenv
|
||||
cd /var/www/nordabiznes
|
||||
sudo -u www-data python3 -m venv venv
|
||||
sudo -u www-data venv/bin/pip install -r requirements.txt
|
||||
python3 -m venv venv
|
||||
venv/bin/pip install -r requirements.txt
|
||||
```
|
||||
|
||||
#### Krok 6: Przywrócenie konfiguracji
|
||||
@ -277,7 +271,7 @@ sudo -u www-data venv/bin/pip install -r requirements.txt
|
||||
```bash
|
||||
# Skopiuj .env z backup
|
||||
scp maciejpi@10.22.68.127:/backup/nordabiz/config/latest/.env /var/www/nordabiznes/.env
|
||||
sudo chown www-data:www-data /var/www/nordabiznes/.env
|
||||
sudo chown root:root /var/www/nordabiznes/.env
|
||||
sudo chmod 600 /var/www/nordabiznes/.env
|
||||
|
||||
# WAŻNE: Zaktualizuj DATABASE_URL w .env jeśli zmieniłeś hasło!
|
||||
@ -305,14 +299,14 @@ curl -I http://localhost:5000/health
|
||||
sudo journalctl -u nordabiznes -f
|
||||
```
|
||||
|
||||
#### Krok 9: Aktualizacja NPM (jeśli zmieniono IP)
|
||||
#### Krok 9: Aktualizacja DNS (jeśli zmieniono IP)
|
||||
|
||||
Jeśli nowa VM ma inny IP, zaktualizuj NPM:
|
||||
Jeśli nowy serwer ma inny publiczny IP, zaktualizuj DNS w OVH:
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.250
|
||||
# NPM Admin: http://10.22.68.250:81
|
||||
# Proxy Host 27 → Forward Host: NOWE_IP, Port: 5000
|
||||
# OVH Manager → nordabiznes.pl → DNS Zone → rekord A → NOWE_IP
|
||||
# Produkcja na OVH VPS ma publiczny IP — ruch nie przechodzi przez NPM (10.22.68.250)
|
||||
# NPM obsługuje tylko staging (staging.nordabiznes.pl)
|
||||
```
|
||||
|
||||
---
|
||||
@ -366,16 +360,13 @@ Wykonuj co kwartał:
|
||||
curl -I https://nordabiznes.pl/health
|
||||
|
||||
# Sprawdź status usługi
|
||||
ssh maciejpi@10.22.68.249 "sudo systemctl status nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl status nordabiznes"
|
||||
|
||||
# Sprawdź logi
|
||||
ssh maciejpi@10.22.68.249 "sudo journalctl -u nordabiznes -n 50"
|
||||
ssh maciejpi@57.128.200.27 "sudo journalctl -u nordabiznes -n 50"
|
||||
|
||||
# Sprawdź NPM
|
||||
# Sprawdź NPM (tylko staging)
|
||||
ssh maciejpi@10.22.68.250 "docker ps | grep nginx-proxy-manager"
|
||||
|
||||
# Sprawdź port forwarding
|
||||
ssh maciejpi@10.22.68.250 "docker exec nginx-proxy-manager_app_1 sqlite3 /data/database.sqlite \"SELECT forward_port FROM proxy_host WHERE id = 27;\""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@ -174,7 +174,7 @@ git merge feature/phase2-auth-public
|
||||
git push origin master && git push inpi master
|
||||
|
||||
# 3. Deploy
|
||||
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && sudo systemctl restart nordabiznes"
|
||||
|
||||
# 4. Weryfikacja (natychmiast!)
|
||||
curl -sI https://nordabiznes.pl/ | head -3
|
||||
@ -188,7 +188,7 @@ curl -sI https://nordabiznes.pl/dashboard | head -3
|
||||
# Natychmiastowy rollback
|
||||
git revert HEAD
|
||||
git push origin master && git push inpi master
|
||||
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && sudo systemctl restart nordabiznes"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@ -510,7 +510,7 @@ git add . && git commit -m "refactor(phase-X): ..."
|
||||
git push origin master && git push inpi master
|
||||
|
||||
# 4. Deploy
|
||||
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes"
|
||||
|
||||
# 5. Weryfikacja produkcji
|
||||
curl https://nordabiznes.pl/health
|
||||
@ -521,7 +521,7 @@ curl https://nordabiznes.pl/health
|
||||
```bash
|
||||
git revert HEAD
|
||||
git push origin master && git push inpi master
|
||||
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@ -5,10 +5,10 @@
|
||||
|
||||
## Klucz SSH do dodania na PBS
|
||||
|
||||
Klucz publiczny z NORDABIZ-01 (10.22.68.249):
|
||||
Klucz publiczny z OVH VPS (57.128.200.27, inpi-vps-waw01):
|
||||
|
||||
```
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHHpPjhwjOUBTmo0MFus4QsgAlI5JxbPNlhW0aPV7vIg maciejpi@NORDABIZ-01
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHHpPjhwjOUBTmo0MFus4QsgAlI5JxbPNlhW0aPV7vIg maciejpi@inpi-vps-waw01
|
||||
```
|
||||
|
||||
## Instrukcja konfiguracji
|
||||
@ -20,7 +20,7 @@ Zaloguj się na PBS przez konsolę Proxmox lub inną metodę i wykonaj:
|
||||
```bash
|
||||
# Na PBS (10.22.68.127)
|
||||
mkdir -p /home/maciejpi/.ssh
|
||||
echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHHpPjhwjOUBTmo0MFus4QsgAlI5JxbPNlhW0aPV7vIg maciejpi@NORDABIZ-01" >> /home/maciejpi/.ssh/authorized_keys
|
||||
echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHHpPjhwjOUBTmo0MFus4QsgAlI5JxbPNlhW0aPV7vIg maciejpi@inpi-vps-waw01" >> /home/maciejpi/.ssh/authorized_keys
|
||||
chmod 700 /home/maciejpi/.ssh
|
||||
chmod 600 /home/maciejpi/.ssh/authorized_keys
|
||||
chown -R maciejpi:maciejpi /home/maciejpi/.ssh
|
||||
@ -34,10 +34,10 @@ sudo mkdir -p /backup/nordabiz/{daily,config}
|
||||
sudo chown -R maciejpi:maciejpi /backup/nordabiz
|
||||
```
|
||||
|
||||
### Krok 3: Weryfikacja połączenia z NORDABIZ-01
|
||||
### Krok 3: Weryfikacja połączenia z OVH VPS (inpi-vps-waw01)
|
||||
|
||||
```bash
|
||||
# Na NORDABIZ-01 (10.22.68.249)
|
||||
# Na OVH VPS (57.128.200.27, inpi-vps-waw01)
|
||||
ssh maciejpi@10.22.68.127 "echo OK"
|
||||
```
|
||||
|
||||
@ -46,7 +46,7 @@ ssh maciejpi@10.22.68.127 "echo OK"
|
||||
Po weryfikacji połączenia, utwórz plik cron:
|
||||
|
||||
```bash
|
||||
# Na NORDABIZ-01
|
||||
# Na OVH VPS (inpi-vps-waw01)
|
||||
sudo tee /etc/cron.d/nordabiz-offsite << 'EOF'
|
||||
# NordaBiz Offsite Backup
|
||||
SHELL=/bin/bash
|
||||
@ -65,7 +65,7 @@ sudo chmod 644 /etc/cron.d/nordabiz-offsite
|
||||
### Krok 5: Test synchronizacji
|
||||
|
||||
```bash
|
||||
# Na NORDABIZ-01
|
||||
# Na OVH VPS (inpi-vps-waw01)
|
||||
rsync -avz --dry-run /var/backups/nordabiz/daily/ maciejpi@10.22.68.127:/backup/nordabiz/daily/
|
||||
```
|
||||
|
||||
@ -85,5 +85,5 @@ Jeśli PBS jest niedostępny, można użyć r11-git-inpi (10.22.68.180) jako alt
|
||||
ssh maciejpi@10.22.68.127 "ls -la /backup/nordabiz/daily/"
|
||||
|
||||
# Sprawdź logi
|
||||
ssh maciejpi@10.22.68.249 "tail -20 /var/log/nordabiznes/backup.log"
|
||||
ssh maciejpi@57.128.200.27 "tail -20 /var/log/nordabiznes/backup.log"
|
||||
```
|
||||
|
||||
@ -138,7 +138,7 @@ Environment variables:
|
||||
## 8) Deployment and Ops
|
||||
|
||||
Production:
|
||||
- App server: NORDABIZ-01 (10.22.68.249)
|
||||
- App server: OVH VPS inpi-vps-waw01 (57.128.200.27)
|
||||
- DB: PostgreSQL on same host (5432)
|
||||
- Reverse proxy: NPM on 10.22.68.250
|
||||
- Domain: nordabiznes.pl
|
||||
@ -155,7 +155,7 @@ CRITICAL CONFIG:
|
||||
Deployment workflow:
|
||||
- push to GitHub and Gitea
|
||||
- pull on staging, test
|
||||
- pull on prod, run migrations via scripts/run_migration.py
|
||||
- deploy to OVH VPS (57.128.200.27) via rsync, run migrations via sudo -u postgres psql
|
||||
- restart systemd service
|
||||
|
||||
---
|
||||
|
||||
@ -104,5 +104,5 @@ git commit -m "docs: Release notes vX.XX.0"
|
||||
|
||||
# 3. Push i deploy
|
||||
git push origin master && git push inpi master
|
||||
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes"
|
||||
```
|
||||
|
||||
@ -150,7 +150,7 @@ python3 app.py
|
||||
### Step 1: SSH to Production Server
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
```
|
||||
|
||||
**IMPORTANT:** Always SSH as `maciejpi`, NEVER as root!
|
||||
@ -168,7 +168,7 @@ sudo -u www-data nano .env
|
||||
### Step 3: Set Production Credentials
|
||||
|
||||
```bash
|
||||
# Production PostgreSQL (same server)
|
||||
# Production PostgreSQL (same server — OVH VPS 57.128.200.27)
|
||||
DATABASE_URL=postgresql://nordabiz_app:YOUR_PRODUCTION_PASSWORD@127.0.0.1:5432/nordabiz
|
||||
|
||||
# Flask Configuration
|
||||
@ -186,13 +186,13 @@ GOOGLE_PLACES_API_KEY=your_production_places_key
|
||||
### Step 4: Set File Permissions
|
||||
|
||||
```bash
|
||||
# Ensure .env is readable only by www-data
|
||||
sudo chown www-data:www-data /var/www/nordabiznes/.env
|
||||
# Ensure .env is readable only by root (OVH VPS — .env is root-owned)
|
||||
sudo chown root:root /var/www/nordabiznes/.env
|
||||
sudo chmod 600 /var/www/nordabiznes/.env
|
||||
|
||||
# Verify permissions
|
||||
ls -la /var/www/nordabiznes/.env
|
||||
# Expected: -rw------- 1 www-data www-data
|
||||
# Expected: -rw------- 1 root root
|
||||
```
|
||||
|
||||
### Step 5: Restart Application
|
||||
@ -245,7 +245,7 @@ nano ~/.pgpass
|
||||
|
||||
```
|
||||
# Format: hostname:port:database:username:password
|
||||
10.22.68.249:5432:nordabiz:nordabiz_app:your_production_password
|
||||
57.128.200.27:5432:nordabiz:nordabiz_app:your_production_password
|
||||
localhost:5433:nordabiz:nordabiz_user:nordabiz_password
|
||||
```
|
||||
|
||||
@ -564,10 +564,10 @@ sudo systemctl restart nordabiznes
|
||||
# Development
|
||||
nano .env # Update DATABASE_URL with new password
|
||||
|
||||
# Production
|
||||
ssh maciejpi@10.22.68.249
|
||||
# Production (OVH VPS)
|
||||
ssh maciejpi@57.128.200.27
|
||||
cd /var/www/nordabiznes
|
||||
sudo -u www-data nano .env # Update DATABASE_URL with new password
|
||||
sudo nano .env # Update DATABASE_URL with new password (.env is root-owned)
|
||||
sudo systemctl restart nordabiznes
|
||||
```
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# System Context Diagram (C4 Level 1)
|
||||
|
||||
**Document Version:** 1.0
|
||||
**Last Updated:** 2026-01-10
|
||||
**Last Updated:** 2026-04-04
|
||||
**Status:** Production LIVE
|
||||
**Diagram Type:** C4 Model - Level 1 (System Context)
|
||||
|
||||
@ -344,7 +344,7 @@ System Event → NordaBiz Hub → MS Graph API → Outlook
|
||||
### Data Protection
|
||||
- **Passwords:** Hashed with bcrypt
|
||||
- **Sessions:** Flask session cookies (encrypted)
|
||||
- **HTTPS:** Forced via NPM (SSL termination)
|
||||
- **HTTPS:** Forced via nginx on OVH VPS (SSL termination with Let's Encrypt)
|
||||
- **CSRF:** Flask-WTF protection enabled
|
||||
|
||||
---
|
||||
@ -353,9 +353,13 @@ System Event → NordaBiz Hub → MS Graph API → Outlook
|
||||
|
||||
**Production:**
|
||||
- **URL:** https://nordabiznes.pl
|
||||
- **Server:** NORDABIZ-01 (10.22.68.249)
|
||||
- **Proxy:** R11-REVPROXY-01 (10.22.68.250)
|
||||
- **Status:** LIVE since 2025-11-23
|
||||
- **Server:** OVH VPS (57.128.200.27, hostname: inpi-vps-waw01)
|
||||
- **SSL:** nginx with Let's Encrypt (on the same VPS)
|
||||
- **Status:** LIVE since 2025-11-23 (migrated to OVH VPS 2026-04)
|
||||
|
||||
**Staging:**
|
||||
- **URL:** https://staging.nordabiznes.pl
|
||||
- **Server:** NORDABIZ-STAGING-01 (VM 248, 10.22.68.248) -- on-prem via NPM + FortiGate
|
||||
|
||||
**Development:**
|
||||
- **Local:** localhost:5000 or 5001
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Container Diagram (C4 Level 2)
|
||||
|
||||
**Document Version:** 1.0
|
||||
**Last Updated:** 2026-01-10
|
||||
**Last Updated:** 2026-04-04
|
||||
**Status:** Production LIVE
|
||||
**Diagram Type:** C4 Model - Level 2 (Containers)
|
||||
|
||||
@ -32,11 +32,11 @@ graph TB
|
||||
|
||||
%% System boundary
|
||||
subgraph "Norda Biznes Partner System"
|
||||
subgraph "R11-REVPROXY-01 [VM 119 | 10.22.68.250]"
|
||||
NPM["🔒 Nginx Proxy Manager<br/>(Reverse Proxy)<br/><br/>Technology: Nginx + Docker<br/>Port: 443 (HTTPS)<br/><br/>Responsibilities:<br/>- SSL/TLS termination<br/>- Request routing<br/>- HTTP→HTTPS redirect<br/>- Let's Encrypt automation"]
|
||||
subgraph "OVH VPS [inpi-vps-waw01 | 57.128.200.27]"
|
||||
Nginx["🔒 Nginx<br/>(Reverse Proxy)<br/><br/>Technology: Nginx<br/>Port: 443 (HTTPS)<br/><br/>Responsibilities:<br/>- SSL/TLS termination<br/>- Request routing<br/>- HTTP→HTTPS redirect<br/>- Let's Encrypt (certbot)"]
|
||||
end
|
||||
|
||||
subgraph "NORDABIZ-01 [VM 249 | 10.22.68.249]"
|
||||
subgraph "OVH VPS [inpi-vps-waw01 | 57.128.200.27]"
|
||||
WebApp["🌐 Flask Web Application<br/>(Application Server)<br/><br/>Technology: Flask 3.0 + Gunicorn<br/>Language: Python 3.9+<br/>Port: 5000<br/><br/>Responsibilities:<br/>- HTTP request handling<br/>- Business logic<br/>- Template rendering<br/>- API endpoints<br/>- Authentication & authorization<br/>- Session management"]
|
||||
|
||||
Database["💾 PostgreSQL Database<br/>(Data Store)<br/><br/>Technology: PostgreSQL 14<br/>Port: 5432 (localhost only)<br/><br/>Responsibilities:<br/>- Persistent data storage<br/>- Full-text search (FTS)<br/>- Fuzzy matching (pg_trgm)<br/>- Data integrity & constraints<br/>- 36 tables (companies, users, etc.)"]
|
||||
@ -68,12 +68,12 @@ graph TB
|
||||
end
|
||||
|
||||
%% User interactions
|
||||
Users -->|"HTTPS<br/>Port 443"| NPM
|
||||
Admin -->|"HTTPS<br/>Port 443"| NPM
|
||||
Users -->|"HTTPS<br/>Port 443"| Nginx
|
||||
Admin -->|"HTTPS<br/>Port 443"| Nginx
|
||||
Admin -->|"SSH<br/>Port 22"| WebApp
|
||||
|
||||
%% NPM routing
|
||||
NPM -->|"HTTP<br/>Port 5000<br/>(CRITICAL!)"| WebApp
|
||||
%% Nginx routing
|
||||
Nginx -->|"HTTP<br/>127.0.0.1:5000"| WebApp
|
||||
|
||||
%% Web app to database
|
||||
WebApp -->|"SQL Queries<br/>SQLAlchemy ORM<br/>localhost:5432"| Database
|
||||
@ -113,7 +113,7 @@ graph TB
|
||||
class WebApp,Scripts containerStyle
|
||||
class Database databaseStyle
|
||||
class SearchSvc,ChatSvc,EmailSvc,GeminiSvc,KRSSvc,GBPSvc,ITSvc serviceStyle
|
||||
class NPM proxyStyle
|
||||
class Nginx proxyStyle
|
||||
class Gemini,BraveAPI,PageSpeed,Places,KRS,MSGraph,ALEO,Rejestr externalStyle
|
||||
class Users,Admin personStyle
|
||||
```
|
||||
@ -122,54 +122,45 @@ graph TB
|
||||
|
||||
## Container Descriptions
|
||||
|
||||
### 🔒 Nginx Proxy Manager (NPM)
|
||||
### 🔒 Nginx (Reverse Proxy)
|
||||
|
||||
**Location:** R11-REVPROXY-01 (VM 119, IP 10.22.68.250)
|
||||
**Technology:** Nginx + Docker
|
||||
**Location:** OVH VPS (57.128.200.27, hostname: inpi-vps-waw01)
|
||||
**Technology:** Nginx
|
||||
**Protocol:** HTTPS (Port 443)
|
||||
**Purpose:** SSL termination and reverse proxy
|
||||
|
||||
**Responsibilities:**
|
||||
- Terminate SSL/TLS connections (Let's Encrypt certificates)
|
||||
- Route incoming HTTPS requests to backend Flask app
|
||||
- Terminate SSL/TLS connections (Let's Encrypt certificates via certbot)
|
||||
- Route incoming HTTPS requests to backend Gunicorn on 127.0.0.1:5000
|
||||
- Automatically renew SSL certificates
|
||||
- Force HTTP→HTTPS redirects
|
||||
- Basic security headers (HSTS, CSP)
|
||||
- Security headers (HSTS, CSP)
|
||||
|
||||
**Critical Configuration:**
|
||||
```
|
||||
HTTPS :443 → HTTP 10.22.68.249:5000
|
||||
HTTPS :443 → HTTP 127.0.0.1:5000
|
||||
```
|
||||
|
||||
**⚠️ WARNING:** NPM **MUST** forward to port **5000**, NOT port 80!
|
||||
- Port 80 on NORDABIZ-01 runs a system nginx that redirects to HTTPS
|
||||
- Forwarding to port 80 causes infinite redirect loop (ERR_TOO_MANY_REDIRECTS)
|
||||
- See: `docs/INCIDENT_REPORT_20260102.md`
|
||||
|
||||
**Verification:**
|
||||
```bash
|
||||
curl -I https://nordabiznes.pl/health
|
||||
# Expected: HTTP 200 OK
|
||||
```
|
||||
|
||||
**NPM Access:**
|
||||
- **Web UI:** http://10.22.68.250:81
|
||||
- **Proxy Host ID:** 27
|
||||
- **Domain:** nordabiznes.pl
|
||||
- **SSL:** Let's Encrypt (auto-renewal)
|
||||
**SSL:** Let's Encrypt (certbot auto-renewal)
|
||||
|
||||
---
|
||||
|
||||
### 🌐 Flask Web Application
|
||||
|
||||
**Location:** NORDABIZ-01 (VM 249, IP 10.22.68.249)
|
||||
**Location:** OVH VPS (57.128.200.27, hostname: inpi-vps-waw01)
|
||||
**Technology:** Flask 3.0 + Gunicorn WSGI server
|
||||
**Language:** Python 3.9+
|
||||
**Protocol:** HTTP (Port 5000 - internal only)
|
||||
**Main File:** `/var/www/nordabiznes/app.py` (13,144 lines)
|
||||
**Protocol:** HTTP (Port 5000 - localhost only, via nginx proxy_pass)
|
||||
**Main File:** `/var/www/nordabiznes/app.py`
|
||||
|
||||
**Responsibilities:**
|
||||
- Handle HTTP requests from NPM
|
||||
- Handle HTTP requests from nginx
|
||||
- Render HTML templates (Jinja2)
|
||||
- Provide REST API endpoints (90+ routes)
|
||||
- User authentication (Flask-Login)
|
||||
@ -208,13 +199,13 @@ sudo systemctl restart nordabiznes
|
||||
sudo journalctl -u nordabiznes -f
|
||||
```
|
||||
|
||||
**Application User:** `www-data` (NOT root or maciejpi)
|
||||
**Application User:** `maciejpi`
|
||||
|
||||
---
|
||||
|
||||
### 💾 PostgreSQL Database
|
||||
|
||||
**Location:** NORDABIZ-01 (VM 249, IP 10.22.68.249)
|
||||
**Location:** OVH VPS (57.128.200.27, hostname: inpi-vps-waw01)
|
||||
**Technology:** PostgreSQL 14
|
||||
**Protocol:** PostgreSQL wire protocol (Port 5432)
|
||||
**Access:** **localhost ONLY** (127.0.0.1)
|
||||
@ -254,10 +245,10 @@ sudo journalctl -u nordabiznes -f
|
||||
|
||||
**Connection Strings:**
|
||||
```python
|
||||
# Flask App (from NORDABIZ-01)
|
||||
# Flask App (from OVH VPS)
|
||||
DATABASE_URL = 'postgresql://nordabiz_app:***@127.0.0.1:5432/nordabiz'
|
||||
|
||||
# Background Scripts (from NORDABIZ-01)
|
||||
# Background Scripts (from OVH VPS)
|
||||
DATABASE_URL = 'postgresql://nordabiz_app:***@127.0.0.1:5432/nordabiz'
|
||||
```
|
||||
|
||||
@ -274,7 +265,7 @@ Only connections from `localhost` (127.0.0.1) are allowed.
|
||||
|
||||
### ⚙️ Background Scripts
|
||||
|
||||
**Location:** NORDABIZ-01 (VM 249, IP 10.22.68.249)
|
||||
**Location:** OVH VPS (57.128.200.27, hostname: inpi-vps-waw01)
|
||||
**Directory:** `/var/www/nordabiznes/scripts/`
|
||||
**Technology:** Python 3.9+ (same virtualenv as Flask app)
|
||||
**Execution:** Manual via SSH or Cron jobs
|
||||
@ -300,7 +291,7 @@ Only connections from `localhost` (127.0.0.1) are allowed.
|
||||
**Execution Example:**
|
||||
```bash
|
||||
cd /var/www/nordabiznes
|
||||
sudo -u www-data /var/www/nordabiznes/venv/bin/python3 scripts/seo_audit.py --company-id 26
|
||||
/var/www/nordabiznes/venv/bin/python3 scripts/seo_audit.py --company-id 26
|
||||
```
|
||||
|
||||
**Cron Jobs (Planned):**
|
||||
@ -619,14 +610,14 @@ All external APIs are called via HTTPS with appropriate authentication.
|
||||
User Browser (HTTPS :443)
|
||||
│
|
||||
▼
|
||||
NPM @ 10.22.68.250:443 (SSL termination)
|
||||
Nginx @ 57.128.200.27:443 (SSL termination)
|
||||
│
|
||||
├─ Decrypt HTTPS
|
||||
├─ Verify SSL certificate
|
||||
└─ Forward as HTTP
|
||||
│
|
||||
▼
|
||||
Flask App @ 10.22.68.249:5000 (HTTP)
|
||||
Flask App @ 127.0.0.1:5000 (HTTP)
|
||||
│
|
||||
├─ Authenticate user (session cookie)
|
||||
├─ Authorize request (permissions)
|
||||
@ -644,7 +635,7 @@ PostgreSQL @ 127.0.0.1:5432 (local only)
|
||||
Flask App (render template / JSON)
|
||||
│
|
||||
▼
|
||||
NPM (encrypt response)
|
||||
Nginx (encrypt response)
|
||||
│
|
||||
▼
|
||||
User Browser (HTTPS response)
|
||||
@ -731,8 +722,8 @@ Admin Dashboard (display results)
|
||||
| Zone | Components | Access Level | Trust Level |
|
||||
|------|------------|--------------|-------------|
|
||||
| **Public Internet** | User browsers | Untrusted | Low |
|
||||
| **DMZ (Proxy)** | NPM (10.22.68.250) | Semi-trusted | Medium |
|
||||
| **App Zone** | Flask App (10.22.68.249:5000) | Trusted | High |
|
||||
| **Proxy (nginx)** | Nginx on OVH VPS (57.128.200.27:443) | Semi-trusted | Medium |
|
||||
| **App Zone** | Flask App (127.0.0.1:5000) | Trusted | High |
|
||||
| **Data Zone** | PostgreSQL (127.0.0.1:5432) | Highly trusted | Critical |
|
||||
| **External APIs** | Gemini, Brave, etc. | Untrusted | Low |
|
||||
|
||||
@ -753,7 +744,7 @@ Admin Dashboard (display results)
|
||||
|
||||
### HTTPS/TLS
|
||||
|
||||
- **Certificate:** Let's Encrypt (auto-renewal via NPM)
|
||||
- **Certificate:** Let's Encrypt (auto-renewal via certbot on OVH VPS)
|
||||
- **Protocols:** TLS 1.2, TLS 1.3
|
||||
- **Cipher Suites:** Modern (AES-GCM, ChaCha20-Poly1305)
|
||||
- **HSTS:** Enabled (max-age=31536000)
|
||||
@ -763,7 +754,7 @@ Admin Dashboard (display results)
|
||||
- **Access:** Localhost only (no external connections)
|
||||
- **Authentication:** Password-based (strong passwords)
|
||||
- **Encryption:** At-rest encryption (OS-level)
|
||||
- **Backups:** Encrypted snapshots (Proxmox Backup Server)
|
||||
- **Backups:** pg_dump + offsite copy
|
||||
|
||||
---
|
||||
|
||||
@ -771,25 +762,27 @@ Admin Dashboard (display results)
|
||||
|
||||
### Production Environment
|
||||
|
||||
**NPM Container (R11-REVPROXY-01):**
|
||||
```yaml
|
||||
Proxy Host Configuration:
|
||||
Domain: nordabiznes.pl
|
||||
Scheme: http
|
||||
Forward Hostname: 10.22.68.249
|
||||
Forward Port: 5000 # CRITICAL: Must be 5000, not 80!
|
||||
Cache Assets: Yes
|
||||
Block Common Exploits: Yes
|
||||
Websockets Support: No
|
||||
SSL:
|
||||
Force SSL: Yes
|
||||
HTTP/2 Support: Yes
|
||||
HSTS Enabled: Yes
|
||||
Certificate: Let's Encrypt
|
||||
Auto-renewal: Yes
|
||||
**Nginx (OVH VPS):**
|
||||
```nginx
|
||||
# /etc/nginx/sites-available/nordabiznes.pl
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name nordabiznes.pl www.nordabiznes.pl;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/nordabiznes.pl/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/nordabiznes.pl/privkey.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Flask/Gunicorn (NORDABIZ-01):**
|
||||
**Flask/Gunicorn (OVH VPS):**
|
||||
```ini
|
||||
# /etc/systemd/system/nordabiznes.service
|
||||
[Unit]
|
||||
@ -798,8 +791,8 @@ After=network.target postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
User=www-data
|
||||
Group=www-data
|
||||
User=maciejpi
|
||||
Group=maciejpi
|
||||
WorkingDirectory=/var/www/nordabiznes
|
||||
Environment="PATH=/var/www/nordabiznes/venv/bin"
|
||||
ExecStart=/var/www/nordabiznes/venv/bin/gunicorn \
|
||||
@ -814,7 +807,7 @@ ExecStart=/var/www/nordabiznes/venv/bin/gunicorn \
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
**PostgreSQL (NORDABIZ-01):**
|
||||
**PostgreSQL (OVH VPS):**
|
||||
```conf
|
||||
# /etc/postgresql/14/main/postgresql.conf
|
||||
listen_addresses = 'localhost' # ONLY localhost!
|
||||
@ -887,13 +880,13 @@ tail -f /var/log/nordabiznes/access.log
|
||||
tail -f /var/log/nordabiznes/error.log
|
||||
```
|
||||
|
||||
**NPM (Nginx Proxy Manager):**
|
||||
**Nginx (OVH VPS):**
|
||||
```bash
|
||||
# Docker logs
|
||||
docker logs -f <npm-container-id>
|
||||
# Nginx access logs
|
||||
tail -f /var/log/nginx/access.log
|
||||
|
||||
# NPM access logs
|
||||
docker exec <npm-container-id> tail -f /data/logs/proxy-host-27_access.log
|
||||
# Nginx error logs
|
||||
tail -f /var/log/nginx/error.log
|
||||
```
|
||||
|
||||
**PostgreSQL:**
|
||||
@ -920,33 +913,22 @@ FROM pg_tables WHERE schemaname = 'public' ORDER BY pg_total_relation_size(schem
|
||||
|
||||
## Critical Configuration Warnings
|
||||
|
||||
### ⚠️ NPM Proxy Port Configuration
|
||||
### ⚠️ Nginx Proxy Configuration
|
||||
|
||||
**CRITICAL:** NPM **MUST** forward to port **5000**, NOT 80!
|
||||
|
||||
**Why?**
|
||||
- Port 80 on NORDABIZ-01 runs system nginx
|
||||
- System nginx redirects HTTP → HTTPS
|
||||
- NPM → :80 → nginx → HTTPS → NPM (infinite loop!)
|
||||
- Results in: `ERR_TOO_MANY_REDIRECTS`
|
||||
**Production** uses nginx on OVH VPS (57.128.200.27) as reverse proxy to Gunicorn on 127.0.0.1:5000.
|
||||
|
||||
**Correct Configuration:**
|
||||
```
|
||||
NPM (10.22.68.250:443) → Flask (10.22.68.249:5000) ✓
|
||||
```
|
||||
|
||||
**Incorrect Configuration (causes loop):**
|
||||
```
|
||||
NPM (10.22.68.250:443) → Nginx (10.22.68.249:80) → NPM ✗
|
||||
Nginx (57.128.200.27:443) → Gunicorn (127.0.0.1:5000) ✓
|
||||
```
|
||||
|
||||
**Verification After Changes:**
|
||||
```bash
|
||||
curl -I https://nordabiznes.pl/health
|
||||
# Must return: HTTP 200 OK (not redirect loop)
|
||||
# Must return: HTTP 200 OK
|
||||
```
|
||||
|
||||
**Incident Report:** `docs/INCIDENT_REPORT_20260102.md`
|
||||
**Historical Note:** The old on-prem setup used NPM on 10.22.68.250 forwarding to 10.22.68.249:5000. See `docs/INCIDENT_REPORT_20260102.md` for historical port misconfiguration incident.
|
||||
|
||||
---
|
||||
|
||||
@ -962,11 +944,11 @@ listen_addresses = 'localhost' # NOT '*' or '0.0.0.0'!
|
||||
**Scripts MUST use localhost (127.0.0.1):**
|
||||
|
||||
```python
|
||||
# CORRECT (from NORDABIZ-01)
|
||||
# CORRECT (from OVH VPS)
|
||||
DATABASE_URL = 'postgresql://nordabiz_app:***@127.0.0.1:5432/nordabiz'
|
||||
|
||||
# INCORRECT (external connection attempt)
|
||||
DATABASE_URL = 'postgresql://nordabiz_app:***@10.22.68.249:5432/nordabiz'
|
||||
DATABASE_URL = 'postgresql://nordabiz_app:***@57.128.200.27:5432/nordabiz'
|
||||
```
|
||||
|
||||
---
|
||||
@ -980,7 +962,7 @@ DATABASE_URL = 'postgresql://nordabiz_app:***@10.22.68.249:5432/nordabiz'
|
||||
- OAuth client secrets
|
||||
|
||||
**Storage:**
|
||||
- Production: `.env` file on NORDABIZ-01
|
||||
- Production: `.env` file on OVH VPS (57.128.200.27)
|
||||
- Development: `.env` file on local machine
|
||||
- Backup: Secure password manager (not in git!)
|
||||
|
||||
@ -1058,7 +1040,7 @@ DATABASE_URL = 'postgresql://nordabiz_app:***@10.22.68.249:5432/nordabiz'
|
||||
---
|
||||
|
||||
**Document Status:** ✅ Complete
|
||||
**Diagram Validated:** 2026-01-10
|
||||
**Diagram Validated:** 2026-04-04
|
||||
**Mermaid Syntax:** v10.6+
|
||||
**Renders in:** GitHub, GitLab, VS Code (with Mermaid extension)
|
||||
**Production Verified:** 2026-01-10 (via health check)
|
||||
**Production Verified:** 2026-04-04 (OVH VPS migration)
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
# Deployment Architecture Diagram
|
||||
|
||||
**Document Version:** 1.0
|
||||
**Last Updated:** 2026-01-10
|
||||
**Last Updated:** 2026-04-04
|
||||
**Status:** Production LIVE
|
||||
**Diagram Type:** Infrastructure / Deployment View
|
||||
|
||||
---
|
||||
|
||||
> **UWAGA (2026-04-04):** Produkcja przeniesiona z OVH VPS inpi-vps-waw01 (VM 249, 57.128.200.27) na **OVH VPS (57.128.200.27, hostname inpi-vps-waw01)**. Diagramy Mermaid poniżej odzwierciedlają starą architekturę — faktyczny stan to: DNS nordabiznes.pl -> 57.128.200.27 (bezpośrednio, bez NPM). NPM (10.22.68.250) obsługuje tylko staging.
|
||||
|
||||
## Overview
|
||||
|
||||
This diagram shows the **physical deployment architecture** of the Norda Biznes Partner system. It illustrates:
|
||||
@ -30,37 +32,22 @@ graph TB
|
||||
%% External actors and entry points
|
||||
Internet["🌐 INTERNET<br/>External Users"]
|
||||
|
||||
%% Public gateway
|
||||
Fortigate["🛡️ FORTIGATE FIREWALL<br/>Public IP: 85.237.177.83<br/><br/>NAT Rules:<br/>• :443 → 10.22.68.250:443<br/>• :80 → 10.22.68.250:80"]
|
||||
|
||||
%% Internal network boundary
|
||||
subgraph "INPI Internal Network (10.22.68.0/24)"
|
||||
|
||||
%% Reverse proxy server
|
||||
subgraph "R11-REVPROXY-01<br/>VM 119 | 10.22.68.250"
|
||||
NPM["🔒 NGINX PROXY MANAGER<br/>(Docker Container)<br/><br/>Ports:<br/>• :443 - HTTPS (SSL termination)<br/>• :80 - HTTP redirect<br/>• :81 - Admin UI (internal)<br/><br/>SSL: Let's Encrypt<br/>Certificate ID: 27<br/>Domains: nordabiznes.pl"]
|
||||
%% Production server (OVH VPS)
|
||||
subgraph "OVH VPS [inpi-vps-waw01 | 57.128.200.27]"
|
||||
subgraph "Reverse Proxy"
|
||||
Nginx["🔒 NGINX<br/>Ports: 443 (HTTPS), 80 (redirect)<br/><br/>SSL: Let's Encrypt (certbot)<br/>Domains: nordabiznes.pl"]
|
||||
end
|
||||
|
||||
%% Backend application server
|
||||
subgraph "NORDABIZ-01<br/>VM 249 | 10.22.68.249"
|
||||
subgraph "Application Layer"
|
||||
Gunicorn["🌐 GUNICORN + FLASK<br/>Port: 5000<br/>Workers: 4<br/>User: www-data<br/><br/>App: /var/www/nordabiznes<br/>Service: nordabiznes.service<br/>Timeout: 120s"]
|
||||
|
||||
NginxSys["⚠️ NGINX (System)<br/>Ports: 80, 443<br/><br/>Purpose: HTTP→HTTPS redirect<br/>Status: UNUSED in production<br/><br/>⚠️ NPM should NEVER use this!"]
|
||||
end
|
||||
|
||||
subgraph "Data Layer"
|
||||
PostgreSQL["💾 POSTGRESQL 14<br/>Port: 5432<br/>Listen: 127.0.0.1 ONLY<br/><br/>Database: nordabiz<br/>User: nordabiz_app<br/>Tables: 36<br/><br/>⚠️ No external connections!"]
|
||||
end
|
||||
|
||||
subgraph "Background Jobs"
|
||||
Scripts["⚙️ PYTHON SCRIPTS<br/>Execution: Cron / Manual<br/><br/>• seo_audit.py<br/>• social_media_audit.py<br/>• gbp_audit.py<br/>• fetch_company_news.py"]
|
||||
end
|
||||
subgraph "Application Layer"
|
||||
Gunicorn["🌐 GUNICORN + FLASK<br/>Port: 127.0.0.1:5000<br/>Workers: 4<br/>User: maciejpi<br/><br/>App: /var/www/nordabiznes<br/>Service: nordabiznes.service<br/>Timeout: 120s"]
|
||||
end
|
||||
|
||||
%% Git server
|
||||
subgraph "r11-git-inpi<br/>Server | 10.22.68.180"
|
||||
Gitea["📚 GITEA<br/>Port: 3000 (HTTPS)<br/><br/>Repository: maciejpi/nordabiz<br/>SSL: Self-signed<br/>Users: maciejpi, gitadmin"]
|
||||
subgraph "Data Layer"
|
||||
PostgreSQL["💾 POSTGRESQL 14<br/>Port: 5432<br/>Listen: 127.0.0.1 ONLY<br/><br/>Database: nordabiz<br/>User: nordabiz_app<br/>Tables: 36<br/><br/>No external connections!"]
|
||||
end
|
||||
|
||||
subgraph "Background Jobs"
|
||||
Scripts["⚙️ PYTHON SCRIPTS<br/>Execution: Cron / Manual<br/><br/>• seo_audit.py<br/>• social_media_audit.py<br/>• gbp_audit.py<br/>• fetch_company_news.py"]
|
||||
end
|
||||
end
|
||||
|
||||
@ -75,12 +62,10 @@ graph TB
|
||||
end
|
||||
|
||||
%% User traffic flow
|
||||
Internet -->|"HTTPS :443<br/>HTTP :80"| Fortigate
|
||||
Fortigate -->|"NAT<br/>443→443<br/>80→80"| NPM
|
||||
Internet -->|"HTTPS :443<br/>HTTP :80"| Nginx
|
||||
|
||||
%% NPM to backend (CRITICAL PATH)
|
||||
NPM ==>|"⚠️ CRITICAL!<br/>HTTP :5000<br/>(NOT port 80!)"| Gunicorn
|
||||
NPM -.->|"❌ WRONG!<br/>Creates redirect loop"| NginxSys
|
||||
%% Nginx to backend
|
||||
Nginx -->|"HTTP<br/>127.0.0.1:5000"| Gunicorn
|
||||
|
||||
%% Application to database (localhost only)
|
||||
Gunicorn <-->|"SQL<br/>localhost:5432<br/>SQLAlchemy ORM"| PostgreSQL
|
||||
@ -95,24 +80,15 @@ graph TB
|
||||
Scripts -->|"HTTPS<br/>Audit requests"| PageSpeed
|
||||
Scripts -->|"HTTPS<br/>News search"| BraveAPI
|
||||
|
||||
%% Git operations
|
||||
Gunicorn -.->|"git pull<br/>(deployment)"| Gitea
|
||||
|
||||
%% Styling
|
||||
classDef serverStyle fill:#2c3e50,stroke:#34495e,color:#ecf0f1,stroke-width:3px
|
||||
classDef appStyle fill:#1168bd,stroke:#0b4884,color:#ffffff,stroke-width:3px
|
||||
classDef dbStyle fill:#438dd5,stroke:#2e6295,color:#ffffff,stroke-width:3px
|
||||
classDef proxyStyle fill:#e74c3c,stroke:#c0392b,color:#ffffff,stroke-width:4px
|
||||
classDef firewallStyle fill:#f39c12,stroke:#d68910,color:#ffffff,stroke-width:4px
|
||||
classDef externalStyle fill:#95a5a6,stroke:#7f8c8d,color:#ffffff,stroke-width:2px
|
||||
classDef warningStyle fill:#e67e22,stroke:#d35400,color:#ffffff,stroke-width:3px
|
||||
|
||||
class NPM proxyStyle
|
||||
class Fortigate firewallStyle
|
||||
class Nginx proxyStyle
|
||||
class Gunicorn,Scripts appStyle
|
||||
class PostgreSQL dbStyle
|
||||
class NginxSys warningStyle
|
||||
class Gitea serverStyle
|
||||
class Gemini,BraveAPI,PageSpeed,Places,KRS,MSGraph externalStyle
|
||||
```
|
||||
|
||||
@ -120,35 +96,42 @@ graph TB
|
||||
|
||||
## Infrastructure Inventory
|
||||
|
||||
### Production Servers
|
||||
### Production Server
|
||||
|
||||
| Server | VM ID | IP Address | Hostname | OS | vCPU | RAM | Disk | Hypervisor |
|
||||
|--------|-------|------------|----------|-----|------|-----|------|------------|
|
||||
| **NORDABIZ-01** | 249 | 10.22.68.249 | nordabiz-01 | Ubuntu 22.04 | 4 | 8 GB | 100 GB SSD | Proxmox VE |
|
||||
| **R11-REVPROXY-01** | 119 | 10.22.68.250 | r11-revproxy-01 | Ubuntu 22.04 | 2 | 4 GB | 50 GB SSD | Proxmox VE |
|
||||
| **r11-git-inpi** | - | 10.22.68.180 | r11-git-inpi | Ubuntu 22.04 | 2 | 4 GB | 100 GB SSD | Proxmox VE |
|
||||
| Server | IP Address | Hostname | OS | vCPU | RAM | Disk | Provider |
|
||||
|--------|------------|----------|-----|------|-----|------|----------|
|
||||
| **OVH VPS** | 57.128.200.27 | inpi-vps-waw01 | Ubuntu 22.04 | 4 | 8 GB | 80 GB SSD | OVH Cloud (Warsaw) |
|
||||
|
||||
### Staging (on-prem, unchanged)
|
||||
|
||||
| Server | VM ID | IP Address | Hostname | OS | Hypervisor |
|
||||
|--------|-------|------------|----------|-----|------------|
|
||||
| **NORDABIZ-STAGING-01** | 248 | 10.22.68.248 | nordabiz-staging-01 | Ubuntu 22.04 | Proxmox VE |
|
||||
|
||||
**Note:** The old on-prem production VM 249 (57.128.200.27) and NPM reverse proxy (10.22.68.250) are no longer used for production. Staging still uses NPM + FortiGate path.
|
||||
|
||||
---
|
||||
|
||||
## Server Details
|
||||
|
||||
### NORDABIZ-01 (Backend Application Server)
|
||||
### OVH VPS (Production Server)
|
||||
|
||||
**Infrastructure:**
|
||||
- **VM ID:** 249
|
||||
- **IP Address:** 10.22.68.249
|
||||
- **Internal DNS:** nordabiznes.inpi.local
|
||||
- **IP Address:** 57.128.200.27
|
||||
- **Hostname:** inpi-vps-waw01
|
||||
- **OS:** Ubuntu 22.04 LTS
|
||||
- **Resources:** 4 vCPU, 8 GB RAM, 100 GB SSD
|
||||
- **Hypervisor:** Proxmox VE
|
||||
- **Resources:** 4 vCPU, 8 GB RAM, 80 GB SSD
|
||||
- **Provider:** OVH Cloud (Warsaw datacenter)
|
||||
- **DNS:** nordabiznes.pl -> 57.128.200.27 (A record in OVH DNS)
|
||||
|
||||
**Services Running:**
|
||||
|
||||
| Service | Port | Binding | User | Status | Purpose |
|
||||
|---------|------|---------|------|--------|---------|
|
||||
| **Gunicorn/Flask** | **5000** | **0.0.0.0** | **www-data** | **Active** | **Main Application** ✓ |
|
||||
| **Nginx** | **443** | **0.0.0.0** | **root** | **Active** | **SSL termination + reverse proxy** ✓ |
|
||||
| **Nginx** | **80** | **0.0.0.0** | **root** | **Active** | **HTTP→HTTPS redirect** |
|
||||
| **Gunicorn/Flask** | **5000** | **127.0.0.1** | **maciejpi** | **Active** | **Main Application** ✓ |
|
||||
| PostgreSQL 14 | 5432 | 127.0.0.1 | postgres | Active | Database |
|
||||
| Nginx (system) | 80, 443 | 0.0.0.0 | root | Active | HTTP→HTTPS redirect ⚠️ |
|
||||
| SSH | 22 | 0.0.0.0 | - | Active | Remote administration |
|
||||
|
||||
**Application Paths:**
|
||||
@ -187,7 +170,7 @@ tail -f /var/log/nordabiznes/error.log # Error log
|
||||
|
||||
**SSH Access:**
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
# CRITICAL: Always use 'maciejpi' user, NEVER 'root'!
|
||||
```
|
||||
|
||||
@ -195,8 +178,10 @@ ssh maciejpi@10.22.68.249
|
||||
```ini
|
||||
# /etc/systemd/system/nordabiznes.service
|
||||
[Service]
|
||||
User=maciejpi
|
||||
Group=maciejpi
|
||||
ExecStart=/var/www/nordabiznes/venv/bin/gunicorn \
|
||||
--bind 0.0.0.0:5000 \
|
||||
--bind 127.0.0.1:5000 \
|
||||
--workers 4 \
|
||||
--timeout 120 \
|
||||
--access-logfile /var/log/nordabiznes/access.log \
|
||||
@ -204,140 +189,37 @@ ExecStart=/var/www/nordabiznes/venv/bin/gunicorn \
|
||||
app:app
|
||||
```
|
||||
|
||||
**⚠️ WARNING - System Nginx:**
|
||||
- System nginx on ports 80/443 is for HTTP→HTTPS redirects ONLY
|
||||
- **NEVER** configure NPM to use port 80 or 443 on this server
|
||||
- Doing so causes infinite redirect loop (ERR_TOO_MANY_REDIRECTS)
|
||||
- See: `docs/INCIDENT_REPORT_20260102.md`
|
||||
**Nginx Configuration:**
|
||||
```nginx
|
||||
# /etc/nginx/sites-available/nordabiznes.pl
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name nordabiznes.pl www.nordabiznes.pl;
|
||||
ssl_certificate /etc/letsencrypt/live/nordabiznes.pl/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/nordabiznes.pl/privkey.pem;
|
||||
|
||||
---
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
### R11-REVPROXY-01 (Reverse Proxy Server)
|
||||
|
||||
**Infrastructure:**
|
||||
- **VM ID:** 119
|
||||
- **IP Address:** 10.22.68.250
|
||||
- **OS:** Ubuntu 22.04 LTS
|
||||
- **Resources:** 2 vCPU, 4 GB RAM, 50 GB SSD
|
||||
- **Hypervisor:** Proxmox VE
|
||||
|
||||
**Services Running:**
|
||||
|
||||
| Service | Port | Binding | Type | Purpose |
|
||||
|---------|------|---------|------|---------|
|
||||
| NPM (HTTPS) | 443 | 0.0.0.0 | Docker | Public HTTPS traffic (SSL termination) |
|
||||
| NPM (HTTP) | 80 | 0.0.0.0 | Docker | HTTP→HTTPS redirect |
|
||||
| NPM Admin UI | 81 | 0.0.0.0 | Docker | NPM management interface (internal only) |
|
||||
| SSH | 22 | 0.0.0.0 | System | Remote administration |
|
||||
|
||||
**Docker Setup:**
|
||||
```bash
|
||||
# NPM container details
|
||||
Container Name: nginx-proxy-manager_app_1
|
||||
Image: jc21/nginx-proxy-manager:latest
|
||||
Volumes:
|
||||
- /docker/npm/data:/data
|
||||
- /docker/npm/letsencrypt:/etc/letsencrypt
|
||||
|
||||
# Container management
|
||||
docker ps | grep nginx-proxy-manager # Check status
|
||||
docker logs nginx-proxy-manager_app_1 --tail 50 # View logs
|
||||
docker exec -it nginx-proxy-manager_app_1 /bin/sh # Shell access
|
||||
docker restart nginx-proxy-manager_app_1 # Restart
|
||||
server {
|
||||
listen 80;
|
||||
server_name nordabiznes.pl www.nordabiznes.pl;
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
```
|
||||
|
||||
**NPM Configuration Database:**
|
||||
- **Location:** `/data/database.sqlite` (inside container)
|
||||
- **Access:** SQLite database
|
||||
- **Backup:** Manual via docker cp
|
||||
|
||||
**NPM Web UI:**
|
||||
- **URL:** http://10.22.68.250:81
|
||||
- **Access:** Internal network only
|
||||
- **Authentication:** Admin credentials required
|
||||
|
||||
**SSH Access:**
|
||||
**SSL Certificate Management:**
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.250
|
||||
```
|
||||
|
||||
**Critical Proxy Host Configuration (ID: 27):**
|
||||
```sql
|
||||
-- Query NPM database
|
||||
docker exec nginx-proxy-manager_app_1 \
|
||||
sqlite3 /data/database.sqlite \
|
||||
"SELECT id, domain_names, forward_host, forward_port FROM proxy_host WHERE id = 27;"
|
||||
|
||||
-- Expected output:
|
||||
-- 27|["nordabiznes.pl","www.nordabiznes.pl"]|10.22.68.249|5000
|
||||
```
|
||||
|
||||
**⚠️ CRITICAL NPM CONFIGURATION:**
|
||||
|
||||
| Parameter | Value | Critical Notes |
|
||||
|-----------|-------|----------------|
|
||||
| Proxy Host ID | 27 | Fixed identifier |
|
||||
| Domain Names | nordabiznes.pl, www.nordabiznes.pl | Both variants |
|
||||
| Forward Scheme | http | SSL terminated at NPM |
|
||||
| Forward Host | 10.22.68.249 | NORDABIZ-01 IP |
|
||||
| **Forward Port** | **5000** | **MUST BE 5000, NOT 80!** ⚠️ |
|
||||
| SSL Certificate | Let's Encrypt (ID 27) | Auto-renewal enabled |
|
||||
| Force SSL | Yes | HTTP→HTTPS redirect |
|
||||
| HTTP/2 | Yes | Performance optimization |
|
||||
| HSTS | Yes | max-age=31536000 |
|
||||
| Block Exploits | Yes | Security hardening |
|
||||
|
||||
**Verification After NPM Changes:**
|
||||
```bash
|
||||
curl -I https://nordabiznes.pl/health
|
||||
# Expected: HTTP/2 200 OK
|
||||
# If redirect loop: Check forward_port is 5000!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### r11-git-inpi (Git Repository Server)
|
||||
|
||||
**Infrastructure:**
|
||||
- **IP Address:** 10.22.68.180
|
||||
- **Hostname:** r11-git-inpi
|
||||
- **OS:** Ubuntu 22.04 LTS
|
||||
- **Resources:** 2 vCPU, 4 GB RAM, 100 GB SSD
|
||||
|
||||
**Services Running:**
|
||||
|
||||
| Service | Port | Protocol | Access | Purpose |
|
||||
|---------|------|----------|--------|---------|
|
||||
| Gitea | 3000 | HTTPS | Internal | Git repository hosting |
|
||||
| SSH | 22 | SSH | Internal | Server administration |
|
||||
|
||||
**Gitea Configuration:**
|
||||
- **Web URL:** https://10.22.68.180:3000/
|
||||
- **Repository:** maciejpi/nordabiz
|
||||
- **Clone URL:** https://10.22.68.180:3000/maciejpi/nordabiz.git
|
||||
- **SSL Certificate:** Self-signed (SSL verification disabled)
|
||||
|
||||
**User Accounts:**
|
||||
- `maciejpi` - Personal account (repository owner)
|
||||
- `gitadmin` - Gitea administrator
|
||||
|
||||
**Production Deployment via Git:**
|
||||
```bash
|
||||
# On NORDABIZ-01
|
||||
cd /var/www/nordabiznes
|
||||
sudo -u www-data git -c http.sslVerify=false pull origin master
|
||||
sudo systemctl restart nordabiznes
|
||||
|
||||
# Verify deployment
|
||||
curl -I http://localhost:5000/health
|
||||
```
|
||||
|
||||
**Git Remote Configuration:**
|
||||
```bash
|
||||
# Production git config (on NORDABIZ-01)
|
||||
git remote -v
|
||||
# inpi https://10.22.68.180:3000/maciejpi/nordabiz.git (fetch)
|
||||
# inpi https://10.22.68.180:3000/maciejpi/nordabiz.git (push)
|
||||
# Let's Encrypt via certbot
|
||||
sudo certbot certificates # Check certificate status
|
||||
sudo certbot renew # Renew certificates
|
||||
sudo certbot renew --dry-run # Test renewal
|
||||
```
|
||||
|
||||
---
|
||||
@ -349,8 +231,8 @@ git remote -v
|
||||
| Segment | CIDR/Address | Purpose | Security Level |
|
||||
|---------|--------------|---------|----------------|
|
||||
| Public Internet | 0.0.0.0/0 | External user access | Untrusted |
|
||||
| WAN (Fortigate) | 85.237.177.83/32 | Public gateway | Perimeter |
|
||||
| INPI LAN | 10.22.68.0/24 | Internal services network | Trusted |
|
||||
| OVH VPS | 57.128.200.27/32 | Production server (direct) | Production |
|
||||
| INPI LAN | 10.22.68.0/24 | Staging + internal services | Trusted |
|
||||
| Localhost | 127.0.0.1/8 | Server-local services | Isolated |
|
||||
|
||||
### DNS Configuration
|
||||
@ -359,42 +241,29 @@ git remote -v
|
||||
|
||||
| Type | Name | Value | TTL | Purpose |
|
||||
|------|------|-------|-----|---------|
|
||||
| A | nordabiznes.pl | 85.237.177.83 | 3600 | Main domain |
|
||||
| A | www.nordabiznes.pl | 85.237.177.83 | 3600 | WWW subdomain |
|
||||
|
||||
**Internal DNS (INPI):**
|
||||
|
||||
| Type | Name | Value | Purpose |
|
||||
|------|------|-------|---------|
|
||||
| A | nordabiznes.inpi.local | 10.22.68.249 | Internal access |
|
||||
| A | nordabiz-01.inpi.local | 10.22.68.249 | Server hostname |
|
||||
| A | revproxy-01.inpi.local | 10.22.68.250 | Reverse proxy |
|
||||
| A | git.inpi.local | 10.22.68.180 | Git server |
|
||||
| A | nordabiznes.pl | 57.128.200.27 | 3600 | Main domain (OVH VPS) |
|
||||
| A | www.nordabiznes.pl | 57.128.200.27 | 3600 | WWW subdomain |
|
||||
| A | staging.nordabiznes.pl | 85.237.177.83 | 3600 | Staging (on-prem via FortiGate) |
|
||||
|
||||
---
|
||||
|
||||
## Port Mappings
|
||||
|
||||
### NORDABIZ-01 (10.22.68.249) Port Matrix
|
||||
### OVH VPS (57.128.200.27) Port Matrix
|
||||
|
||||
| Port | Protocol | Service | Binding | Access | Purpose | Security Notes |
|
||||
|------|----------|---------|---------|--------|---------|----------------|
|
||||
| 22 | TCP | SSH | 0.0.0.0 | Internal only | Server administration | Key-based auth |
|
||||
| 80 | TCP | Nginx (system) | 0.0.0.0 | Internal only | HTTP→HTTPS redirect | ⚠️ Not for NPM! |
|
||||
| 443 | TCP | Nginx (system) | 0.0.0.0 | Internal only | HTTPS redirect | ⚠️ Not for NPM! |
|
||||
| **5000** | **TCP** | **Gunicorn/Flask** | **0.0.0.0** | **Internal only** | **Main Application** | **✓ NPM uses this** |
|
||||
| 22 | TCP | SSH | 0.0.0.0 | Public (key-only) | Server administration | Key-based auth |
|
||||
| 80 | TCP | Nginx | 0.0.0.0 | Public | HTTP→HTTPS redirect | Auto-redirect |
|
||||
| 443 | TCP | Nginx | 0.0.0.0 | Public | HTTPS (SSL termination) | Let's Encrypt |
|
||||
| **5000** | **TCP** | **Gunicorn/Flask** | **127.0.0.1** | **Localhost only** | **Main Application** | **Nginx proxy_pass** |
|
||||
| 5432 | TCP | PostgreSQL | 127.0.0.1 | Localhost only | Database | No external access |
|
||||
| 5433 | TCP | - | - | Unused | Reserved for dev Docker | - |
|
||||
|
||||
**⚠️ PORT 5000 - CRITICAL NOTES:**
|
||||
- This is the **ONLY** correct port for NPM to connect to
|
||||
- Ports 80/443 are for nginx system service (redirects only)
|
||||
- Using port 80 in NPM causes infinite redirect loop
|
||||
- Always verify after NPM configuration changes
|
||||
**Traffic flow:** Internet -> nginx (:443) -> proxy_pass -> Gunicorn (127.0.0.1:5000)
|
||||
|
||||
---
|
||||
|
||||
### R11-REVPROXY-01 (10.22.68.250) Port Matrix
|
||||
### Port Matrix (historical on-prem, now staging only)
|
||||
|
||||
| Port | Protocol | Service | Binding | Access | Purpose | Security Notes |
|
||||
|------|----------|---------|---------|--------|---------|----------------|
|
||||
@ -403,9 +272,11 @@ git remote -v
|
||||
| 81 | TCP | NPM Admin UI | 0.0.0.0 | Internal only | NPM management | Auth required |
|
||||
| 443 | TCP | NPM | 0.0.0.0 | Public (via NAT) | HTTPS traffic | SSL termination |
|
||||
|
||||
**Note:** R11-REVPROXY-01 (10.22.68.250) with NPM is now used for staging only (staging.nordabiznes.pl).
|
||||
|
||||
---
|
||||
|
||||
### r11-git-inpi (10.22.68.180) Port Matrix
|
||||
### r11-git-inpi (10.22.68.180) Port Matrix (internal, for staging)
|
||||
|
||||
| Port | Protocol | Service | Binding | Access | Purpose | Security Notes |
|
||||
|------|----------|---------|---------|--------|---------|----------------|
|
||||
@ -414,18 +285,14 @@ git remote -v
|
||||
|
||||
---
|
||||
|
||||
### Fortigate Firewall NAT Rules
|
||||
### Fortigate Firewall NAT Rules (staging only)
|
||||
|
||||
| External Port | Protocol | Internal IP | Internal Port | Purpose | Traffic |
|
||||
|---------------|----------|-------------|---------------|---------|---------|
|
||||
| 443 | TCP | 10.22.68.250 | 443 | HTTPS public access | Incoming |
|
||||
| 443 | TCP | 10.22.68.250 | 443 | HTTPS staging access | Incoming |
|
||||
| 80 | TCP | 10.22.68.250 | 80 | HTTP redirect | Incoming |
|
||||
|
||||
**Firewall Rules:**
|
||||
- Allow: Public → 10.22.68.250:443 (HTTPS)
|
||||
- Allow: Public → 10.22.68.250:80 (HTTP, redirects to HTTPS)
|
||||
- Deny: All other inbound traffic
|
||||
- Allow: Internal → External (outbound API calls)
|
||||
**Note:** FortiGate NAT rules are now only used for staging (staging.nordabiznes.pl). Production traffic goes directly to OVH VPS (57.128.200.27) without FortiGate.
|
||||
|
||||
---
|
||||
|
||||
@ -438,30 +305,21 @@ git remote -v
|
||||
│ 1. USER BROWSER │
|
||||
│ https://nordabiznes.pl │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│ DNS: nordabiznes.pl → 85.237.177.83
|
||||
│ DNS: nordabiznes.pl → 57.128.200.27
|
||||
│ HTTPS :443
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 2. FORTIGATE FIREWALL (85.237.177.83) │
|
||||
│ NAT: 443 → 10.22.68.250:443 │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│ Forward to proxy
|
||||
│ HTTPS :443
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 3. NPM @ R11-REVPROXY-01 (10.22.68.250:443) │
|
||||
│ 2. NGINX @ OVH VPS (57.128.200.27:443) │
|
||||
│ • Accept HTTPS connection │
|
||||
│ • TLS handshake (Let's Encrypt certificate) │
|
||||
│ • TLS handshake (Let's Encrypt certificate via certbot) │
|
||||
│ • Terminate SSL/TLS │
|
||||
│ • Add security headers (HSTS, etc.) │
|
||||
│ • Proxy pass to backend │
|
||||
│ • Proxy pass to Gunicorn │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│ ⚠️ CRITICAL PATH
|
||||
│ http://10.22.68.249:5000
|
||||
│ (NOT port 80!)
|
||||
│ http://127.0.0.1:5000
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 4. GUNICORN @ NORDABIZ-01 (10.22.68.249:5000) │
|
||||
│ 3. GUNICORN @ OVH VPS (127.0.0.1:5000) │
|
||||
│ • Receive HTTP request (decrypted) │
|
||||
│ • Load balance across 4 workers │
|
||||
│ • Pass to Flask application │
|
||||
@ -469,7 +327,7 @@ git remote -v
|
||||
│ Process request
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 5. FLASK APP (app.py) │
|
||||
│ 4. FLASK APP (app.py) │
|
||||
│ • Rate limiting check (Flask-Limiter) │
|
||||
│ • Session validation (Flask-Login) │
|
||||
│ • CSRF protection (Flask-WTF) │
|
||||
@ -480,7 +338,7 @@ git remote -v
|
||||
│ localhost:5432
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 6. POSTGRESQL @ localhost:5432 │
|
||||
│ 5. POSTGRESQL @ localhost:5432 │
|
||||
│ • Execute SQL query (SQLAlchemy ORM) │
|
||||
│ • Apply constraints and indexes │
|
||||
│ • Return result set │
|
||||
@ -488,62 +346,32 @@ git remote -v
|
||||
│ Results
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 7. FLASK APP (render response) │
|
||||
│ 6. FLASK APP (render response) │
|
||||
│ • Jinja2 template rendering │
|
||||
│ • JSON serialization (API routes) │
|
||||
│ • Apply response headers │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│ HTTP response
|
||||
▼
|
||||
GUNICORN → NPM (encrypt with TLS) → FORTIGATE → USER BROWSER
|
||||
GUNICORN → NGINX (encrypt with TLS) → USER BROWSER
|
||||
```
|
||||
|
||||
**Timing Breakdown:**
|
||||
- DNS resolution: ~50ms
|
||||
- SSL handshake: ~100ms
|
||||
- NPM proxy: ~10ms
|
||||
- Nginx proxy: ~5ms
|
||||
- Flask processing: ~50-500ms (depends on query complexity)
|
||||
- Database query: ~10-100ms
|
||||
- Template rendering: ~20-50ms
|
||||
- **Total:** ~240-810ms (typical range)
|
||||
- **Total:** ~235-805ms (typical range)
|
||||
|
||||
---
|
||||
|
||||
### Failed Request Flow (Port 80 Misconfiguration)
|
||||
### Historical: Failed Request Flow (Port 80 Misconfiguration)
|
||||
|
||||
**⚠️ This caused the 2026-01-02 production incident**
|
||||
**This applied to the old on-prem setup (pre-OVH migration). See `docs/INCIDENT_REPORT_20260102.md` for details.**
|
||||
|
||||
```
|
||||
USER BROWSER
|
||||
│
|
||||
│ https://nordabiznes.pl
|
||||
▼
|
||||
FORTIGATE (NAT)
|
||||
│
|
||||
▼
|
||||
NPM @ 10.22.68.250:443
|
||||
│ SSL termination
|
||||
│ ❌ WRONG: Proxy to http://10.22.68.249:80
|
||||
▼
|
||||
NGINX (System) @ 10.22.68.249:80
|
||||
│
|
||||
│ ❌ Return: 301 → https://nordabiznes.pl
|
||||
▼
|
||||
NPM (receives redirect)
|
||||
│ SSL termination again
|
||||
│ ❌ Proxy to http://10.22.68.249:80 (LOOP!)
|
||||
▼
|
||||
... Infinite redirect loop ...
|
||||
│
|
||||
▼
|
||||
BROWSER ERROR: ERR_TOO_MANY_REDIRECTS
|
||||
```
|
||||
|
||||
**Root Cause:** NPM forwarding to port 80 instead of port 5000
|
||||
|
||||
**Fix:** Change NPM `forward_port` from 80 to 5000
|
||||
|
||||
**Prevention:** Always verify `forward_port = 5000` after NPM changes
|
||||
The old setup used NPM (10.22.68.250) forwarding to on-prem VM (57.128.200.27). Misconfiguring the forward port to 80 instead of 5000 caused an infinite redirect loop. This is no longer applicable to the current OVH VPS production setup.
|
||||
|
||||
---
|
||||
|
||||
@ -553,14 +381,13 @@ BROWSER ERROR: ERR_TOO_MANY_REDIRECTS
|
||||
|
||||
**Certificate Details:**
|
||||
- **Provider:** Let's Encrypt
|
||||
- **Managed By:** NPM (Nginx Proxy Manager)
|
||||
- **Certificate ID:** 27
|
||||
- **Managed By:** certbot on OVH VPS
|
||||
- **Domains:**
|
||||
- nordabiznes.pl
|
||||
- www.nordabiznes.pl
|
||||
- **Key Type:** RSA 2048-bit
|
||||
- **Validity:** 90 days (auto-renewed)
|
||||
- **Renewal:** Automatic (30 days before expiry)
|
||||
- **Renewal:** Automatic via certbot cron/timer
|
||||
|
||||
**TLS Configuration:**
|
||||
- **Protocols:** TLS 1.2, TLS 1.3 (TLS 1.0/1.1 disabled)
|
||||
@ -568,7 +395,7 @@ BROWSER ERROR: ERR_TOO_MANY_REDIRECTS
|
||||
- **HTTP/2:** Enabled
|
||||
- **HSTS:** Enabled (max-age=31536000, includeSubDomains)
|
||||
|
||||
**Security Headers (Added by NPM):**
|
||||
**Security Headers (Added by nginx):**
|
||||
```http
|
||||
Strict-Transport-Security: max-age=31536000; includeSubDomains
|
||||
X-Frame-Options: SAMEORIGIN
|
||||
@ -582,8 +409,8 @@ X-XSS-Protection: 1; mode=block
|
||||
openssl s_client -connect nordabiznes.pl:443 -servername nordabiznes.pl < /dev/null 2>/dev/null | \
|
||||
openssl x509 -noout -dates -subject -issuer
|
||||
|
||||
# Check expiry date
|
||||
curl -vI https://nordabiznes.pl 2>&1 | grep -E "(expire|issuer)"
|
||||
# Check certificate status on server
|
||||
ssh maciejpi@57.128.200.27 "sudo certbot certificates"
|
||||
|
||||
# Test SSL configuration
|
||||
curl -I https://nordabiznes.pl/health
|
||||
@ -620,7 +447,9 @@ curl -I https://nordabiznes.pl/health
|
||||
|
||||
## Deployment Workflow
|
||||
|
||||
### Git-Based Deployment
|
||||
### Rsync-Based Deployment (OVH VPS)
|
||||
|
||||
Production deployment uses rsync (no git on OVH VPS).
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
@ -638,35 +467,32 @@ curl -I https://nordabiznes.pl/health
|
||||
│ GIT REPOSITORIES │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ GITEA @ r11-git-inpi (10.22.68.180:3000) │ │
|
||||
│ │ Repository: maciejpi/nordabiz │ │
|
||||
│ │ Purpose: Production deployment source │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ GITHUB @ github.com │ │
|
||||
│ │ Repository: pienczyn/nordabiz │ │
|
||||
│ │ Purpose: Cloud backup, collaboration │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ GITEA @ r11-git-inpi (10.22.68.180:3000) │ │
|
||||
│ │ Repository: maciejpi/nordabiz │ │
|
||||
│ │ Purpose: Internal backup, staging deploy │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
└───────────────────────────┬────────────────────────────────────┘
|
||||
│
|
||||
│ ssh + git pull
|
||||
│ rsync (from dev Mac)
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ PRODUCTION (NORDABIZ-01) │
|
||||
│ PRODUCTION (OVH VPS 57.128.200.27) │
|
||||
│ │
|
||||
│ 1. SSH to server: │
|
||||
│ ssh maciejpi@10.22.68.249 │
|
||||
│ 1. Deploy via rsync: │
|
||||
│ rsync -avz --exclude='.env' --exclude='venv/' \ │
|
||||
│ ./ maciejpi@57.128.200.27:/var/www/nordabiznes/ │
|
||||
│ │
|
||||
│ 2. Pull latest code: │
|
||||
│ cd /var/www/nordabiznes │
|
||||
│ sudo -u www-data git -c http.sslVerify=false pull │
|
||||
│ 2. Restart service: │
|
||||
│ ssh maciejpi@57.128.200.27 \ │
|
||||
│ "sudo systemctl reload nordabiznes" │
|
||||
│ │
|
||||
│ 3. Restart service: │
|
||||
│ sudo systemctl restart nordabiznes │
|
||||
│ │
|
||||
│ 4. Verify deployment: │
|
||||
│ curl -I http://localhost:5000/health │
|
||||
│ 3. Verify deployment: │
|
||||
│ curl -I https://nordabiznes.pl/health │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
@ -674,14 +500,12 @@ curl -I https://nordabiznes.pl/health
|
||||
**Deployment Checklist:**
|
||||
1. ✅ Code tested locally
|
||||
2. ✅ Git commit with descriptive message
|
||||
3. ✅ Push to both remotes (Gitea + GitHub)
|
||||
4. ✅ SSH to production server as `maciejpi`
|
||||
5. ✅ Pull latest code as `www-data` user
|
||||
6. ✅ Restart `nordabiznes` service
|
||||
7. ✅ Verify health endpoint (localhost:5000)
|
||||
8. ✅ Verify public endpoint (nordabiznes.pl)
|
||||
9. ✅ Check logs for errors (`journalctl -u nordabiznes`)
|
||||
10. ✅ Update release notes in `app.py`
|
||||
3. ✅ Push to both remotes (GitHub + Gitea)
|
||||
4. ✅ Rsync to OVH VPS
|
||||
5. ✅ Reload `nordabiznes` service
|
||||
6. ✅ Verify health endpoint (nordabiznes.pl)
|
||||
7. ✅ Check logs for errors (`journalctl -u nordabiznes`)
|
||||
8. ✅ Update release notes in `app.py`
|
||||
|
||||
---
|
||||
|
||||
@ -708,16 +532,12 @@ Content-Type: application/json
|
||||
|
||||
**Health Check Commands:**
|
||||
```bash
|
||||
# External check (via NPM)
|
||||
# External check (via nginx)
|
||||
curl -I https://nordabiznes.pl/health
|
||||
# Expected: HTTP/2 200 OK
|
||||
|
||||
# Internal check (direct to Flask)
|
||||
curl -I http://10.22.68.249:5000/health
|
||||
# Expected: HTTP/1.1 200 OK
|
||||
|
||||
# Localhost check (from NORDABIZ-01)
|
||||
curl -I http://localhost:5000/health
|
||||
# Localhost check (from OVH VPS)
|
||||
ssh maciejpi@57.128.200.27 "curl -I http://localhost:5000/health"
|
||||
# Expected: HTTP/1.1 200 OK
|
||||
```
|
||||
|
||||
@ -751,20 +571,16 @@ sudo -u postgres psql -c "SELECT version();"
|
||||
sudo -u postgres psql -c "SELECT count(*) FROM pg_stat_activity WHERE datname='nordabiz';"
|
||||
```
|
||||
|
||||
**NPM Service:**
|
||||
**Nginx Service (OVH VPS):**
|
||||
```bash
|
||||
# Check Docker container
|
||||
docker ps | grep nginx-proxy-manager
|
||||
# Should show container running
|
||||
# Check nginx status
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl status nginx"
|
||||
|
||||
# Check NPM logs
|
||||
docker logs nginx-proxy-manager_app_1 --tail 50
|
||||
# Check nginx logs
|
||||
ssh maciejpi@57.128.200.27 "sudo tail -20 /var/log/nginx/error.log"
|
||||
|
||||
# Verify proxy configuration
|
||||
docker exec nginx-proxy-manager_app_1 \
|
||||
sqlite3 /data/database.sqlite \
|
||||
"SELECT forward_port FROM proxy_host WHERE id = 27;"
|
||||
# Expected: 5000
|
||||
# Test nginx config
|
||||
ssh maciejpi@57.128.200.27 "sudo nginx -t"
|
||||
```
|
||||
|
||||
### Planned Monitoring (Not Yet Implemented)
|
||||
@ -784,7 +600,7 @@ docker exec nginx-proxy-manager_app_1 \
|
||||
### PostgreSQL Database Backups
|
||||
|
||||
**Current Status:** Manual backups only
|
||||
**Backup Location:** `/backup/nordabiz/` (on NORDABIZ-01)
|
||||
**Backup Location:** `/backup/nordabiz/` (on OVH VPS inpi-vps-waw01)
|
||||
|
||||
**Manual Backup Procedure:**
|
||||
```bash
|
||||
@ -824,83 +640,27 @@ curl -I http://localhost:5000/health
|
||||
|
||||
---
|
||||
|
||||
### VM Snapshots (Proxmox)
|
||||
### OVH VPS Backups
|
||||
|
||||
**Snapshot Schedule:**
|
||||
- **Daily:** 7-day retention
|
||||
- **Weekly:** 4-week retention
|
||||
- **Monthly:** 6-month retention
|
||||
|
||||
**Snapshot Storage:** Proxmox Backup Server
|
||||
|
||||
**Restore Procedure:**
|
||||
1. Access Proxmox VE web interface
|
||||
2. Navigate to VM (249 or 119)
|
||||
3. Select "Snapshots" tab
|
||||
4. Choose restore point
|
||||
5. Confirm snapshot restoration
|
||||
6. Start VM after restore
|
||||
7. Verify services are running
|
||||
|
||||
**Recovery Time Objective (RTO):** ~15 minutes
|
||||
**Recovery Point Objective (RPO):** ~24 hours (daily snapshots)
|
||||
|
||||
---
|
||||
|
||||
### NPM Configuration Backup
|
||||
|
||||
**Database Location:** `/data/database.sqlite` (inside NPM container)
|
||||
|
||||
**Manual Backup:**
|
||||
```bash
|
||||
# Backup NPM database
|
||||
docker exec nginx-proxy-manager_app_1 cat /data/database.sqlite > \
|
||||
/backup/npm/npm_$(date +%Y%m%d).sqlite
|
||||
|
||||
# Backup SSL certificates
|
||||
docker exec nginx-proxy-manager_app_1 tar czf - /etc/letsencrypt > \
|
||||
/backup/npm/letsencrypt_$(date +%Y%m%d).tar.gz
|
||||
```
|
||||
|
||||
**Restore Procedure:**
|
||||
```bash
|
||||
# Stop NPM container
|
||||
docker stop nginx-proxy-manager_app_1
|
||||
|
||||
# Restore database
|
||||
cat /backup/npm/npm_20260110.sqlite | \
|
||||
docker exec -i nginx-proxy-manager_app_1 sh -c 'cat > /data/database.sqlite'
|
||||
|
||||
# Restore SSL certificates
|
||||
cat /backup/npm/letsencrypt_20260110.tar.gz | \
|
||||
docker exec -i nginx-proxy-manager_app_1 sh -c 'tar xzf - -C /'
|
||||
|
||||
# Start NPM container
|
||||
docker start nginx-proxy-manager_app_1
|
||||
|
||||
# Verify
|
||||
curl -I https://nordabiznes.pl/health
|
||||
```
|
||||
**OVH Automated Backups:**
|
||||
- OVH VPS snapshot backups (configured via OVH panel)
|
||||
- Application code backed up via git (GitHub + Gitea)
|
||||
- Database backed up via pg_dump + offsite copy
|
||||
|
||||
---
|
||||
|
||||
## Security Configuration
|
||||
|
||||
### Firewall Rules (Fortigate)
|
||||
### OVH VPS Firewall
|
||||
|
||||
**The OVH VPS uses ufw (Uncomplicated Firewall) and/or OVH firewall:**
|
||||
|
||||
**Inbound Rules:**
|
||||
```
|
||||
Priority 1: ALLOW Public (0.0.0.0/0) → 10.22.68.250:443 (HTTPS)
|
||||
Priority 2: ALLOW Public (0.0.0.0/0) → 10.22.68.250:80 (HTTP redirect)
|
||||
Priority 100: DENY All other inbound traffic
|
||||
```
|
||||
|
||||
**Outbound Rules:**
|
||||
```
|
||||
Priority 1: ALLOW 10.22.68.0/24 → Internet (HTTPS :443)
|
||||
Priority 2: ALLOW 10.22.68.0/24 → Internet (DNS :53)
|
||||
Priority 3: ALLOW 10.22.68.0/24 → Internet (NTP :123)
|
||||
Priority 100: DENY All other outbound traffic
|
||||
ALLOW 22/tcp (SSH - key-based auth only)
|
||||
ALLOW 80/tcp (HTTP - redirects to HTTPS)
|
||||
ALLOW 443/tcp (HTTPS - production traffic)
|
||||
DENY all other inbound
|
||||
```
|
||||
|
||||
### SSH Access Control
|
||||
@ -912,10 +672,6 @@ Priority 100: DENY All other outbound traffic
|
||||
- SSH key-based authentication (required)
|
||||
- Password authentication disabled
|
||||
|
||||
**Firewall:**
|
||||
- SSH accessible from internal network only (10.22.68.0/24)
|
||||
- No public SSH access
|
||||
|
||||
**SSH Configuration:**
|
||||
```bash
|
||||
# /etc/ssh/sshd_config
|
||||
@ -925,6 +681,11 @@ PubkeyAuthentication yes
|
||||
AllowUsers maciejpi
|
||||
```
|
||||
|
||||
**SSH Access:**
|
||||
```bash
|
||||
ssh maciejpi@57.128.200.27
|
||||
```
|
||||
|
||||
### Database Security
|
||||
|
||||
**PostgreSQL Access Control:**
|
||||
@ -1008,7 +769,7 @@ curl -I https://nordabiznes.pl/health
|
||||
**Diagnosis:**
|
||||
```bash
|
||||
# 1. Check Gunicorn service
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
sudo systemctl status nordabiznes
|
||||
# If not running: sudo systemctl start nordabiznes
|
||||
|
||||
@ -1022,7 +783,7 @@ curl -I http://localhost:5000/health
|
||||
|
||||
# 4. Test from NPM server
|
||||
ssh maciejpi@10.22.68.250
|
||||
curl -I http://10.22.68.249:5000/health
|
||||
curl -I http://57.128.200.27:5000/health
|
||||
# Expected: HTTP/1.1 200 OK
|
||||
```
|
||||
|
||||
@ -1161,7 +922,7 @@ docker restart nginx-proxy-manager_app_1
|
||||
|
||||
- [ ] All IP addresses are current
|
||||
- [ ] All port numbers are correct
|
||||
- [ ] NPM proxy configuration verified (port 5000!)
|
||||
- [ ] Nginx proxy_pass configuration verified (127.0.0.1:5000)
|
||||
- [ ] DNS records match actual configuration
|
||||
- [ ] SSL certificate status checked
|
||||
- [ ] Firewall rules documented accurately
|
||||
@ -1192,23 +953,20 @@ docker restart nginx-proxy-manager_app_1
|
||||
---
|
||||
|
||||
**Document Status:** ✅ Complete
|
||||
**Diagram Validated:** 2026-01-10
|
||||
**Production Verified:** 2026-01-10
|
||||
**Diagram Validated:** 2026-04-04
|
||||
**Production Verified:** 2026-04-04 (OVH VPS migration)
|
||||
**Mermaid Syntax:** v10.6+
|
||||
**Renders in:** GitHub, GitLab, VS Code (with Mermaid extension)
|
||||
|
||||
---
|
||||
|
||||
**⚠️ CRITICAL CONFIGURATION REMINDER:**
|
||||
**Production traffic flow:**
|
||||
```
|
||||
Internet → Nginx (57.128.200.27:443) → Gunicorn (127.0.0.1:5000)
|
||||
```
|
||||
|
||||
**NPM Proxy Host 27 MUST use:**
|
||||
- **Forward Host:** 10.22.68.249
|
||||
- **Forward Port:** 5000 (NOT 80!)
|
||||
|
||||
**Always verify after changes:**
|
||||
**Verify production:**
|
||||
```bash
|
||||
curl -I https://nordabiznes.pl/health
|
||||
# Expected: HTTP/2 200 OK
|
||||
```
|
||||
|
||||
**See:** `docs/INCIDENT_REPORT_20260102.md` for details on port 80 incident
|
||||
|
||||
@ -1058,15 +1058,15 @@ alembic upgrade head
|
||||
|
||||
**Production:**
|
||||
```bash
|
||||
# SSH to NORDABIZ-01
|
||||
ssh maciejpi@10.22.68.249
|
||||
# SSH to OVH VPS
|
||||
ssh maciejpi@57.128.200.27
|
||||
|
||||
# Backup database
|
||||
pg_dump nordabiz > backup_$(date +%Y%m%d).sql
|
||||
|
||||
# Apply migration
|
||||
cd /var/www/nordabiznes
|
||||
sudo -u www-data alembic upgrade head
|
||||
/var/www/nordabiznes/venv/bin/python3 scripts/run_migration.py database/migrations/XXX_nazwa.sql
|
||||
|
||||
# Verify
|
||||
psql -U nordabiz_app -d nordabiz -c "\dt"
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
# Network Topology Diagram
|
||||
|
||||
**Document Version:** 1.0
|
||||
**Last Updated:** 2026-01-10
|
||||
**Last Updated:** 2026-04-04
|
||||
**Status:** Production LIVE
|
||||
**Diagram Type:** Network Topology / Infrastructure Network
|
||||
|
||||
---
|
||||
|
||||
> **UWAGA (2026-04-04):** Produkcja przeniesiona z OVH VPS inpi-vps-waw01 (VM 249, 57.128.200.27) na **OVH VPS (57.128.200.27, hostname inpi-vps-waw01)**. Diagramy Mermaid poniżej odzwierciedlają starą architekturę — ruch produkcyjny nie przechodzi już przez Fortigate/NPM. Staging (10.22.68.248) i NPM (10.22.68.250) bez zmian.
|
||||
|
||||
## Overview
|
||||
|
||||
This document provides a **network-centric view** of the Norda Biznes Partner infrastructure. It focuses on:
|
||||
@ -37,90 +39,58 @@ graph TB
|
||||
%% External network
|
||||
subgraph "Public Internet (Untrusted)"
|
||||
Users["👥 External Users<br/>Website Visitors"]
|
||||
DNS_OVH["🌐 OVH DNS<br/>nordabiznes.pl<br/>→ 85.237.177.83"]
|
||||
DNS_OVH["🌐 OVH DNS<br/>nordabiznes.pl<br/>→ 57.128.200.27"]
|
||||
end
|
||||
|
||||
%% Perimeter security
|
||||
subgraph "Network Perimeter"
|
||||
Fortigate["🛡️ FORTIGATE FIREWALL<br/><br/>WAN: 85.237.177.83<br/>LAN: 10.22.68.1<br/><br/>NAT Rules:<br/>• 85.237.177.83:443 → 10.22.68.250:443<br/>• 85.237.177.83:80 → 10.22.68.250:80<br/><br/>Firewall Policy:<br/>• Allow HTTPS (443) from ANY<br/>• Allow HTTP (80) from ANY<br/>• Allow SSH (22) from ADMIN_NET<br/>• Default: DENY ALL"]
|
||||
%% Production (OVH VPS - direct internet access)
|
||||
subgraph "OVH Cloud (Warsaw) — PRODUCTION"
|
||||
VPS["🖥️ OVH VPS inpi-vps-waw01<br/><br/>IP: 57.128.200.27<br/><br/>Services:<br/>• Nginx :443 (SSL termination)<br/>• Nginx :80 (redirect)<br/>• Gunicorn :5000 (localhost)<br/>• PostgreSQL :5432 (localhost)<br/>• SSH :22"]
|
||||
end
|
||||
|
||||
%% Internal network zones
|
||||
subgraph "INPI Internal Network (10.22.68.0/24)"
|
||||
%% Staging (on-prem via FortiGate + NPM)
|
||||
subgraph "INPI Internal Network (10.22.68.0/24) — STAGING"
|
||||
Fortigate["🛡️ FORTIGATE<br/>WAN: 85.237.177.83<br/>LAN: 10.22.68.1<br/><br/>NAT for staging:<br/>• :443 → 10.22.68.250:443"]
|
||||
|
||||
%% DMZ Zone
|
||||
subgraph "DMZ Zone (Semi-Trusted)"
|
||||
NPM_Server["🖥️ R11-REVPROXY-01<br/><br/>IP: 10.22.68.250<br/>VM ID: 119<br/>Hostname: r11-revproxy-01<br/><br/>Services:<br/>• NPM (Docker) :443, :80, :81<br/>• SSH :22<br/><br/>Gateway: 10.22.68.1<br/>DNS: 10.22.68.1"]
|
||||
end
|
||||
NPM_Server["🖥️ R11-REVPROXY-01<br/>10.22.68.250<br/>NPM (staging proxy)"]
|
||||
|
||||
%% Application Zone
|
||||
subgraph "Application Zone (Trusted)"
|
||||
App_Server["🖥️ NORDABIZ-01<br/><br/>IP: 10.22.68.249<br/>VM ID: 249<br/>Hostname: nordabiz-01<br/>DNS: nordabiznes.inpi.local<br/><br/>Services:<br/>• Flask/Gunicorn :5000<br/>• PostgreSQL :5432 (localhost)<br/>• Nginx :80, :443 (system)<br/>• SSH :22<br/><br/>Gateway: 10.22.68.1<br/>DNS: 10.22.68.1"]
|
||||
end
|
||||
Staging_Server["🖥️ NORDABIZ-STAGING-01<br/>10.22.68.248<br/>Staging app + DB"]
|
||||
|
||||
%% Internal Services Zone
|
||||
subgraph "Internal Services Zone (Trusted)"
|
||||
Git_Server["🖥️ r11-git-inpi<br/><br/>IP: 10.22.68.180<br/>Hostname: r11-git-inpi<br/><br/>Services:<br/>• Gitea :3000 (HTTPS)<br/>• SSH :22<br/><br/>Gateway: 10.22.68.1"]
|
||||
end
|
||||
|
||||
%% Internal DNS
|
||||
Internal_DNS["🔍 Internal DNS<br/>Zone: inpi.local<br/><br/>Records:<br/>• nordabiznes.inpi.local → 10.22.68.249<br/>• git.inpi.local → 10.22.68.180"]
|
||||
Git_Server["🖥️ r11-git-inpi<br/>10.22.68.180<br/>Gitea :3000"]
|
||||
end
|
||||
|
||||
%% External services
|
||||
subgraph "External APIs (Internet)"
|
||||
API_Google["☁️ Google Cloud APIs<br/><br/>• Gemini AI API<br/>• PageSpeed Insights API<br/>• Places API<br/><br/>Auth: API Keys<br/>HTTPS only"]
|
||||
|
||||
API_MS["☁️ Microsoft Graph API<br/><br/>• Email sending<br/><br/>Auth: OAuth 2.0<br/>HTTPS only"]
|
||||
|
||||
API_Brave["☁️ Brave Search API<br/><br/>• News search<br/><br/>Auth: API Key<br/>HTTPS only"]
|
||||
|
||||
API_KRS["🏛️ KRS Open API<br/><br/>• Company registry<br/><br/>Auth: Public<br/>HTTPS only"]
|
||||
|
||||
Web_Scrapers["🌐 Web Scraping<br/><br/>• ALEO.com (NIP)<br/>• rejestr.io (Connections)<br/><br/>HTTPS only"]
|
||||
API_Google["☁️ Google Cloud APIs<br/>Gemini AI, PageSpeed, Places"]
|
||||
API_MS["☁️ Microsoft Graph API<br/>Email sending"]
|
||||
API_Brave["☁️ Brave Search API"]
|
||||
API_KRS["🏛️ KRS Open API"]
|
||||
end
|
||||
|
||||
%% Network flows - User traffic
|
||||
%% Production traffic (direct to OVH VPS)
|
||||
Users -->|"DNS Query<br/>nordabiznes.pl"| DNS_OVH
|
||||
DNS_OVH -->|"DNS Response<br/>85.237.177.83"| Users
|
||||
Users -->|"HTTPS :443<br/>HTTP :80"| Fortigate
|
||||
DNS_OVH -->|"DNS Response<br/>57.128.200.27"| Users
|
||||
Users -->|"HTTPS :443"| VPS
|
||||
|
||||
%% Firewall to DMZ
|
||||
Fortigate -->|"NAT + Route<br/>:443 → 10.22.68.250:443<br/>:80 → 10.22.68.250:80"| NPM_Server
|
||||
%% Staging traffic (via FortiGate)
|
||||
Fortigate -.->|"staging NAT<br/>→ NPM"| NPM_Server
|
||||
NPM_Server -.->|"HTTP :5000"| Staging_Server
|
||||
|
||||
%% DMZ to Application Zone
|
||||
NPM_Server ==>|"⚠️ CRITICAL<br/>HTTP :5000<br/>(NOT port 80!)"| App_Server
|
||||
NPM_Server -.->|"Internal DNS Query<br/>nordabiznes.inpi.local"| Internal_DNS
|
||||
|
||||
%% Application to External APIs
|
||||
App_Server -->|"HTTPS<br/>API Requests"| API_Google
|
||||
App_Server -->|"HTTPS<br/>OAuth 2.0"| API_MS
|
||||
App_Server -->|"HTTPS<br/>API Requests"| API_Brave
|
||||
App_Server -->|"HTTPS<br/>API Requests"| API_KRS
|
||||
App_Server -->|"HTTPS<br/>Web Scraping"| Web_Scrapers
|
||||
|
||||
%% Git deployment
|
||||
App_Server -.->|"git pull<br/>HTTPS :3000<br/>Deployment only"| Git_Server
|
||||
|
||||
%% Admin SSH access
|
||||
Fortigate -.->|"SSH :22<br/>Admin only"| NPM_Server
|
||||
Fortigate -.->|"SSH :22<br/>Admin only"| App_Server
|
||||
Fortigate -.->|"SSH :22<br/>Admin only"| Git_Server
|
||||
%% API calls from production
|
||||
VPS -->|"HTTPS"| API_Google
|
||||
VPS -->|"HTTPS OAuth 2.0"| API_MS
|
||||
VPS -->|"HTTPS"| API_Brave
|
||||
VPS -->|"HTTPS"| API_KRS
|
||||
|
||||
%% Styling
|
||||
classDef public fill:#f9f,stroke:#333,stroke-width:2px
|
||||
classDef perimeter fill:#f96,stroke:#333,stroke-width:3px
|
||||
classDef dmz fill:#ff9,stroke:#333,stroke-width:2px
|
||||
classDef app fill:#9f9,stroke:#333,stroke-width:2px
|
||||
classDef internal fill:#99f,stroke:#333,stroke-width:2px
|
||||
classDef production fill:#9f9,stroke:#333,stroke-width:3px
|
||||
classDef staging fill:#ff9,stroke:#333,stroke-width:2px
|
||||
classDef external fill:#ccc,stroke:#333,stroke-width:1px
|
||||
|
||||
class Users,DNS_OVH public
|
||||
class Fortigate perimeter
|
||||
class NPM_Server dmz
|
||||
class App_Server app
|
||||
class Git_Server,Internal_DNS internal
|
||||
class API_Google,API_MS,API_Brave,API_KRS,Web_Scrapers external
|
||||
class VPS production
|
||||
class Fortigate,NPM_Server,Staging_Server,Git_Server staging
|
||||
class API_Google,API_MS,API_Brave,API_KRS external
|
||||
```
|
||||
|
||||
---
|
||||
@ -148,46 +118,31 @@ graph TB
|
||||
|
||||
---
|
||||
|
||||
### Zone 2: Network Perimeter (Firewall)
|
||||
### Zone 2: Network Perimeter (FortiGate) — Staging Only
|
||||
|
||||
**Purpose:** Network security boundary, NAT, and traffic filtering
|
||||
**Purpose:** Network security boundary for staging environment. Production traffic no longer goes through FortiGate.
|
||||
|
||||
**Components:**
|
||||
- **Fortigate Firewall**
|
||||
- Model: (Infrastructure-specific)
|
||||
- WAN IP: 85.237.177.83
|
||||
- LAN IP: 10.22.68.1 (gateway for internal network)
|
||||
|
||||
**Security Level:** **Perimeter** - First line of defense
|
||||
**Security Level:** **Perimeter** - First line of defense for staging
|
||||
|
||||
**NAT Configuration:**
|
||||
**NAT Configuration (staging only):**
|
||||
|
||||
| Public Address | Public Port | Internal Address | Internal Port | Protocol | Purpose |
|
||||
|----------------|-------------|------------------|---------------|----------|---------|
|
||||
| 85.237.177.83 | 443 | 10.22.68.250 | 443 | TCP | HTTPS to NPM |
|
||||
| 85.237.177.83 | 443 | 10.22.68.250 | 443 | TCP | HTTPS to NPM (staging) |
|
||||
| 85.237.177.83 | 80 | 10.22.68.250 | 80 | TCP | HTTP to NPM (redirect) |
|
||||
|
||||
**Firewall Rules:**
|
||||
|
||||
| Priority | Source | Destination | Port | Action | Purpose |
|
||||
|----------|--------|-------------|------|--------|---------|
|
||||
| 1 | ANY | 85.237.177.83 | 443 | ALLOW | Public HTTPS access |
|
||||
| 2 | ANY | 85.237.177.83 | 80 | ALLOW | HTTP (redirect to HTTPS) |
|
||||
| 3 | ADMIN_NET | 10.22.68.0/24 | 22 | ALLOW | SSH administration |
|
||||
| 4 | 10.22.68.0/24 | ANY | 443 | ALLOW | Outbound HTTPS (APIs) |
|
||||
| 5 | 10.22.68.0/24 | ANY | 80 | ALLOW | Outbound HTTP (fallback) |
|
||||
| 99 | ANY | ANY | ANY | DENY | Default deny all |
|
||||
|
||||
**Notes:**
|
||||
- Stateful firewall maintains connection state
|
||||
- Return traffic automatically allowed for established connections
|
||||
- No inbound SSH from public internet (admin access via internal network only)
|
||||
**Note:** Production (nordabiznes.pl) DNS now points directly to OVH VPS (57.128.200.27), bypassing FortiGate entirely.
|
||||
|
||||
---
|
||||
|
||||
### Zone 3: DMZ Zone (Semi-Trusted)
|
||||
### Zone 3: DMZ Zone (Semi-Trusted) — Staging Only
|
||||
|
||||
**Purpose:** SSL termination, reverse proxy, and public-facing services
|
||||
**Purpose:** SSL termination and reverse proxy for staging environment only.
|
||||
|
||||
**Network:** 10.22.68.250/32 (single host)
|
||||
|
||||
@ -195,15 +150,16 @@ graph TB
|
||||
- **R11-REVPROXY-01** (VM 119)
|
||||
- IP: 10.22.68.250
|
||||
- Services: Nginx Proxy Manager (Docker), SSH
|
||||
- Handles: staging.nordabiznes.pl
|
||||
|
||||
**Security Level:** **Semi-Trusted** - Exposed to internet traffic, hardened configuration
|
||||
**Security Level:** **Semi-Trusted** - Exposed to staging traffic
|
||||
|
||||
**Inbound Traffic:**
|
||||
- From Internet (via Fortigate NAT): HTTPS :443, HTTP :80
|
||||
- From ADMIN_NET: SSH :22
|
||||
|
||||
**Outbound Traffic:**
|
||||
- To Application Zone (10.22.68.249): HTTP :5000 (Flask/Gunicorn)
|
||||
- To Application Zone (57.128.200.27): HTTP :5000 (Flask/Gunicorn)
|
||||
- To Internet: HTTPS (for Let's Encrypt ACME challenge, Docker image updates)
|
||||
|
||||
**Security Controls:**
|
||||
@ -218,7 +174,7 @@ graph TB
|
||||
# NPM Proxy Host Configuration (Host ID: 27)
|
||||
domain_names:
|
||||
- nordabiznes.pl
|
||||
forward_host: 10.22.68.249
|
||||
forward_host: 57.128.200.27
|
||||
forward_port: 5000 # ⚠️ MUST be 5000, NOT 80!
|
||||
ssl:
|
||||
certificate_id: 27
|
||||
@ -230,7 +186,7 @@ ssl:
|
||||
**Common Misconfiguration:**
|
||||
```yaml
|
||||
# ❌ WRONG - Causes infinite redirect loop
|
||||
forward_port: 80 # This forwards to nginx on NORDABIZ-01, which redirects to HTTPS
|
||||
forward_port: 80 # This forwards to nginx on OVH VPS inpi-vps-waw01, which redirects to HTTPS
|
||||
```
|
||||
|
||||
**Correct Configuration:**
|
||||
@ -246,7 +202,7 @@ curl -I https://nordabiznes.pl/health
|
||||
# Expected: HTTP/2 200 (success)
|
||||
|
||||
# Test internal routing (from NPM server)
|
||||
curl -I http://10.22.68.249:5000/health
|
||||
curl -I http://57.128.200.27:5000/health
|
||||
# Expected: HTTP/1.1 200 (success)
|
||||
```
|
||||
|
||||
@ -254,27 +210,26 @@ curl -I http://10.22.68.249:5000/health
|
||||
|
||||
---
|
||||
|
||||
### Zone 4: Application Zone (Trusted)
|
||||
### Zone 4: Application Zone — Production (OVH VPS)
|
||||
|
||||
**Purpose:** Application hosting, business logic processing
|
||||
|
||||
**Network:** 10.22.68.249/32 (single host)
|
||||
**Network:** 57.128.200.27 (OVH VPS, public IP)
|
||||
|
||||
**Components:**
|
||||
- **NORDABIZ-01** (VM 249)
|
||||
- IP: 10.22.68.249
|
||||
- Internal DNS: nordabiznes.inpi.local
|
||||
- Services: Flask/Gunicorn :5000, PostgreSQL :5432 (localhost), Nginx :80/443 (unused), SSH :22
|
||||
- **OVH VPS** (inpi-vps-waw01)
|
||||
- IP: 57.128.200.27
|
||||
- Services: Nginx :443/:80, Gunicorn :5000 (localhost), PostgreSQL :5432 (localhost), SSH :22
|
||||
|
||||
**Security Level:** **Trusted** - Internal zone, application processing
|
||||
**Security Level:** **Production** - Public-facing, secured by nginx + ufw
|
||||
|
||||
**Inbound Traffic:**
|
||||
- From DMZ (10.22.68.250): HTTP :5000 (NPM → Gunicorn)
|
||||
- From ADMIN_NET: SSH :22
|
||||
- From Internet: HTTPS :443 (nginx SSL termination)
|
||||
- From Internet: HTTP :80 (redirects to HTTPS)
|
||||
- SSH :22 (key-based auth only)
|
||||
|
||||
**Outbound Traffic:**
|
||||
- To External APIs: HTTPS :443 (Google, Microsoft, Brave, KRS)
|
||||
- To Git Server (10.22.68.180): HTTPS :3000 (deployment)
|
||||
- To Internet: HTTP/HTTPS (web scraping ALEO.com, rejestr.io)
|
||||
|
||||
**Localhost Services (127.0.0.1):**
|
||||
@ -293,7 +248,7 @@ curl -I http://10.22.68.249:5000/health
|
||||
|
||||
**Network Configuration:**
|
||||
```
|
||||
IP Address: 10.22.68.249/24
|
||||
IP Address: 57.128.200.27/24
|
||||
Gateway: 10.22.68.1
|
||||
DNS: 10.22.68.1
|
||||
```
|
||||
@ -324,7 +279,7 @@ DNS: 10.22.68.1
|
||||
**Security Level:** **Trusted** - Internal services, no public exposure
|
||||
|
||||
**Inbound Traffic:**
|
||||
- From Application Zone (10.22.68.249): HTTPS :3000 (git pull for deployment)
|
||||
- From Application Zone (57.128.200.27): HTTPS :3000 (git pull for deployment)
|
||||
- From ADMIN_NET: SSH :22, HTTPS :3000 (git operations)
|
||||
|
||||
**Outbound Traffic:**
|
||||
@ -365,7 +320,7 @@ DNS: 10.22.68.1
|
||||
|
||||
**Network Flow:**
|
||||
```
|
||||
App Server (10.22.68.249)
|
||||
App Server (57.128.200.27)
|
||||
→ Fortigate (10.22.68.1)
|
||||
→ Internet Gateway
|
||||
→ External API (HTTPS :443)
|
||||
@ -394,7 +349,7 @@ App Server (10.22.68.249)
|
||||
|------------|----------|-------|---------|------|
|
||||
| 10.22.68.1 | fortigate-lan | N/A | Default gateway | Perimeter |
|
||||
| 10.22.68.180 | r11-git-inpi | N/A | Gitea server | Internal Services |
|
||||
| 10.22.68.249 | nordabiz-01 | 249 | Application + DB server | Application |
|
||||
| 57.128.200.27 | nordabiz-01 | 249 | Application + DB server | Application |
|
||||
| 10.22.68.250 | r11-revproxy-01 | 119 | NPM reverse proxy | DMZ |
|
||||
|
||||
### Reserved Ranges
|
||||
@ -417,15 +372,15 @@ App Server (10.22.68.249)
|
||||
| HTTPS | NPM | 10.22.68.250 | 443 | TCP | Public (via NAT) | SSL termination |
|
||||
| HTTP | NPM | 10.22.68.250 | 80 | TCP | Public (via NAT) | Redirect to HTTPS |
|
||||
| **Application Services** |
|
||||
| Flask/Gunicorn | NORDABIZ-01 | 10.22.68.249 | 5000 | TCP | Internal only | Web application |
|
||||
| PostgreSQL | NORDABIZ-01 | 127.0.0.1 | 5432 | TCP | Localhost only | Database |
|
||||
| Nginx (unused) | NORDABIZ-01 | 10.22.68.249 | 80 | TCP | Internal only | HTTP redirect (not used) |
|
||||
| Nginx (unused) | NORDABIZ-01 | 10.22.68.249 | 443 | TCP | Internal only | SSL (not used) |
|
||||
| Flask/Gunicorn | OVH VPS inpi-vps-waw01 | 57.128.200.27 | 5000 | TCP | Internal only | Web application |
|
||||
| PostgreSQL | OVH VPS inpi-vps-waw01 | 127.0.0.1 | 5432 | TCP | Localhost only | Database |
|
||||
| Nginx (unused) | OVH VPS inpi-vps-waw01 | 57.128.200.27 | 80 | TCP | Internal only | HTTP redirect (not used) |
|
||||
| Nginx (unused) | OVH VPS inpi-vps-waw01 | 57.128.200.27 | 443 | TCP | Internal only | SSL (not used) |
|
||||
| **Internal Services** |
|
||||
| Gitea | r11-git-inpi | 10.22.68.180 | 3000 | TCP | Internal only | Git repository |
|
||||
| NPM Admin | NPM | 10.22.68.250 | 81 | TCP | Internal only | NPM web UI |
|
||||
| **Administration** |
|
||||
| SSH | NORDABIZ-01 | 10.22.68.249 | 22 | TCP | Admin network | Remote admin |
|
||||
| SSH | OVH VPS inpi-vps-waw01 | 57.128.200.27 | 22 | TCP | Admin network | Remote admin |
|
||||
| SSH | NPM | 10.22.68.250 | 22 | TCP | Admin network | Remote admin |
|
||||
| SSH | r11-git-inpi | 10.22.68.180 | 22 | TCP | Admin network | Remote admin |
|
||||
|
||||
@ -435,9 +390,9 @@ App Server (10.22.68.249)
|
||||
|
||||
| Source | Destination | Protocol | Purpose | Notes |
|
||||
|--------|-------------|----------|---------|-------|
|
||||
| NPM :443 | 10.22.68.249:5000 | HTTP | HTTPS requests → Flask | ✅ CORRECT |
|
||||
| NPM :80 | 10.22.68.249:5000 | HTTP | HTTP requests → Flask | ✅ CORRECT |
|
||||
| NPM :443 | 10.22.68.249:80 | HTTP | HTTPS requests → Nginx | ❌ WRONG - Redirect loop! |
|
||||
| NPM :443 | 57.128.200.27:5000 | HTTP | HTTPS requests → Flask | ✅ CORRECT |
|
||||
| NPM :80 | 57.128.200.27:5000 | HTTP | HTTP requests → Flask | ✅ CORRECT |
|
||||
| NPM :443 | 57.128.200.27:80 | HTTP | HTTPS requests → Nginx | ❌ WRONG - Redirect loop! |
|
||||
|
||||
**Why Port 5000 is Critical:**
|
||||
1. NPM terminates SSL at port 443
|
||||
@ -496,7 +451,7 @@ whois nordabiznes.pl
|
||||
|
||||
| Record Type | Name | Value | Purpose |
|
||||
|-------------|------|-------|---------|
|
||||
| A | nordabiznes.inpi.local | 10.22.68.249 | Application server |
|
||||
| A | nordabiznes.inpi.local | 57.128.200.27 | Application server |
|
||||
| A | git.inpi.local | 10.22.68.180 | Gitea server |
|
||||
| A | npm.inpi.local | 10.22.68.250 | NPM server |
|
||||
|
||||
@ -515,7 +470,7 @@ whois nordabiznes.pl
|
||||
|
||||
**Configuration:**
|
||||
```bash
|
||||
# /etc/resolv.conf on NORDABIZ-01
|
||||
# /etc/resolv.conf on OVH VPS inpi-vps-waw01
|
||||
nameserver 10.22.68.1
|
||||
search inpi.local
|
||||
```
|
||||
@ -528,12 +483,12 @@ search inpi.local
|
||||
|
||||
All internal servers use **10.22.68.1** (Fortigate LAN interface) as default gateway.
|
||||
|
||||
### Routing Table (NORDABIZ-01 Example)
|
||||
### Routing Table (OVH VPS inpi-vps-waw01 Example)
|
||||
|
||||
```bash
|
||||
# ip route show
|
||||
default via 10.22.68.1 dev ens18 # All non-local traffic → Fortigate
|
||||
10.22.68.0/24 dev ens18 proto kernel scope link src 10.22.68.249 # Local subnet
|
||||
10.22.68.0/24 dev ens18 proto kernel scope link src 57.128.200.27 # Local subnet
|
||||
127.0.0.0/8 dev lo # Localhost
|
||||
```
|
||||
|
||||
@ -550,7 +505,7 @@ Fortigate WAN (85.237.177.83:443)
|
||||
↓ NAT translation
|
||||
Fortigate LAN → NPM (10.22.68.250:443)
|
||||
↓ SSL termination, proxy
|
||||
NPM → Flask/Gunicorn (10.22.68.249:5000)
|
||||
NPM → Flask/Gunicorn (57.128.200.27:5000)
|
||||
↓ HTTP request processing
|
||||
Flask → PostgreSQL (127.0.0.1:5432)
|
||||
↓ SQL query
|
||||
@ -569,7 +524,7 @@ Fortigate WAN → User Browser (85.237.177.83:443)
|
||||
#### Example 2: Application Calls External API (Gemini AI)
|
||||
|
||||
```
|
||||
Flask Application (10.22.68.249)
|
||||
Flask Application (57.128.200.27)
|
||||
↓ HTTPS :443
|
||||
Default Gateway (10.22.68.1)
|
||||
↓ NAT + Firewall
|
||||
@ -579,7 +534,7 @@ Google Gemini API (generativelanguage.googleapis.com)
|
||||
↓ API response
|
||||
Internet → Fortigate WAN
|
||||
↓ NAT reverse
|
||||
Fortigate LAN → Flask Application (10.22.68.249)
|
||||
Fortigate LAN → Flask Application (57.128.200.27)
|
||||
```
|
||||
|
||||
**Total Hops:** 3 (internal) + variable (internet) + 2 (return) ≈ 15-25 hops
|
||||
@ -588,7 +543,7 @@ Fortigate LAN → Flask Application (10.22.68.249)
|
||||
#### Example 3: Git Pull for Deployment
|
||||
|
||||
```
|
||||
Flask Application (10.22.68.249)
|
||||
Flask Application (57.128.200.27)
|
||||
↓ git pull over HTTPS :3000
|
||||
Local routing (same subnet)
|
||||
↓ Direct connection
|
||||
@ -712,7 +667,7 @@ openssl s_client -connect nordabiznes.pl:443 -servername nordabiznes.pl
|
||||
|
||||
**Internal Health Checks:**
|
||||
```bash
|
||||
# From NORDABIZ-01, check Gunicorn
|
||||
# From OVH VPS inpi-vps-waw01, check Gunicorn
|
||||
curl -I http://127.0.0.1:5000/health
|
||||
# Expected: HTTP/1.1 200 OK
|
||||
|
||||
@ -789,13 +744,13 @@ curl http://10.22.68.250:443
|
||||
# If fails: NPM service down
|
||||
|
||||
# Check if Gunicorn is responding
|
||||
curl http://10.22.68.249:5000/health
|
||||
curl http://57.128.200.27:5000/health
|
||||
# If fails: Gunicorn service down
|
||||
```
|
||||
|
||||
**Resolution:**
|
||||
1. Restart NPM: `docker restart nginx-proxy-manager` (on R11-REVPROXY-01)
|
||||
2. Restart Gunicorn: `sudo systemctl restart nordabiznes` (on NORDABIZ-01)
|
||||
2. Restart Gunicorn: `sudo systemctl restart nordabiznes` (on OVH VPS inpi-vps-waw01)
|
||||
3. Check Fortigate firewall rules (requires admin access)
|
||||
|
||||
---
|
||||
@ -834,7 +789,7 @@ openssl s_client -connect nordabiznes.pl:443 -servername nordabiznes.pl | openss
|
||||
**Diagnosis:**
|
||||
```bash
|
||||
# Check network latency
|
||||
ping -c 10 10.22.68.249
|
||||
ping -c 10 57.128.200.27
|
||||
# Expected: <2ms average
|
||||
|
||||
# Check database performance
|
||||
@ -895,7 +850,7 @@ NPM forwarding to port 80 instead of port 5000
|
||||
```bash
|
||||
# Check NPM configuration (from R11-REVPROXY-01)
|
||||
docker exec -it nginx-proxy-manager cat /data/nginx/proxy_host/27.conf
|
||||
# Look for: proxy_pass http://10.22.68.249:XXXX
|
||||
# Look for: proxy_pass http://57.128.200.27:XXXX
|
||||
# XXXX should be 5000, NOT 80
|
||||
```
|
||||
|
||||
@ -932,7 +887,7 @@ graph LR
|
||||
|
||||
NPM_NIC["NPM NIC<br/>10.22.68.250<br/>MAC: xx:xx:xx:xx:xx:01"]
|
||||
|
||||
APP_NIC["NORDABIZ-01 NIC<br/>10.22.68.249<br/>MAC: xx:xx:xx:xx:xx:02"]
|
||||
APP_NIC["OVH VPS inpi-vps-waw01 NIC<br/>57.128.200.27<br/>MAC: xx:xx:xx:xx:xx:02"]
|
||||
|
||||
GIT_NIC["Git Server NIC<br/>10.22.68.180<br/>MAC: xx:xx:xx:xx:xx:03"]
|
||||
end
|
||||
@ -953,7 +908,7 @@ sequenceDiagram
|
||||
participant DNS as OVH DNS
|
||||
participant FW as Fortigate
|
||||
participant NPM as NPM (10.22.68.250)
|
||||
participant App as Flask (10.22.68.249)
|
||||
participant App as Flask (57.128.200.27)
|
||||
participant DB as PostgreSQL (127.0.0.1)
|
||||
|
||||
User->>DNS: DNS query: nordabiznes.pl
|
||||
@ -964,7 +919,7 @@ sequenceDiagram
|
||||
FW->>NPM: HTTPS :443 (10.22.68.250)
|
||||
|
||||
Note over NPM: SSL termination<br/>Decrypt HTTPS → HTTP
|
||||
NPM->>App: HTTP :5000 (10.22.68.249)
|
||||
NPM->>App: HTTP :5000 (57.128.200.27)
|
||||
Note over App: Flask processes request
|
||||
|
||||
App->>DB: SQL query (localhost:5432)
|
||||
@ -984,7 +939,7 @@ sequenceDiagram
|
||||
participant User as User Browser
|
||||
participant FW as Fortigate
|
||||
participant NPM as NPM (10.22.68.250)
|
||||
participant Nginx as Nginx (10.22.68.249:80)
|
||||
participant Nginx as Nginx (57.128.200.27:80)
|
||||
|
||||
User->>FW: HTTPS :443
|
||||
FW->>NPM: HTTPS :443
|
||||
@ -1039,13 +994,13 @@ docker cp nginx-proxy-manager:/tmp/npm-backup.tar.gz ./npm-backup-$(date +%Y%m%d
|
||||
|
||||
**PostgreSQL Configuration:**
|
||||
```bash
|
||||
# Backup PostgreSQL config (from NORDABIZ-01)
|
||||
# Backup PostgreSQL config (from OVH VPS inpi-vps-waw01)
|
||||
sudo tar czf postgresql-config-backup-$(date +%Y%m%d).tar.gz /etc/postgresql/14/main/
|
||||
```
|
||||
|
||||
**Network Configuration:**
|
||||
```bash
|
||||
# Backup network config (from NORDABIZ-01)
|
||||
# Backup network config (from OVH VPS inpi-vps-waw01)
|
||||
sudo tar czf network-config-backup-$(date +%Y%m%d).tar.gz /etc/netplan/ /etc/systemd/network/
|
||||
```
|
||||
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
# Critical Configurations Reference
|
||||
|
||||
**Document Version:** 1.0
|
||||
**Last Updated:** 2026-01-10
|
||||
**Status:** Production LIVE
|
||||
**Last Updated:** 2026-04-04
|
||||
**Status:** Production LIVE (OVH VPS)
|
||||
**Diagram Type:** Configuration Reference / Operations Guide
|
||||
|
||||
---
|
||||
|
||||
> **NOTE (2026-04-04):** Production migrated from on-prem VM 249 (10.22.68.249) to OVH VPS (57.128.200.27, hostname inpi-vps-waw01). NPM proxy host 27 configuration now applies to staging only. Production uses nginx on the VPS directly. Deploy via rsync (no git on OVH VPS).
|
||||
|
||||
## Overview
|
||||
|
||||
This document provides a **comprehensive reference** of all critical configurations for the Norda Biznes Partner infrastructure. It serves as the **single source of truth** for:
|
||||
@ -54,19 +56,32 @@ This document provides a **comprehensive reference** of all critical configurati
|
||||
|
||||
---
|
||||
|
||||
## NPM Reverse Proxy Configuration
|
||||
## Production Reverse Proxy Configuration (OVH VPS)
|
||||
|
||||
### ⚠️ CRITICAL: Port 5000 Configuration
|
||||
Production uses nginx on OVH VPS (57.128.200.27) as a reverse proxy to Gunicorn on 127.0.0.1:5000.
|
||||
|
||||
**Proxy Host ID:** 27
|
||||
**Domains:** nordabiznes.pl, www.nordabiznes.pl
|
||||
**Traffic flow:** Internet -> nginx (57.128.200.27:443) -> Gunicorn (127.0.0.1:5000)
|
||||
|
||||
> **CRITICAL WARNING:** NPM must forward to **port 5000**, NOT port 80!
|
||||
>
|
||||
> Forwarding to port 80 causes an infinite redirect loop (ERR_TOO_MANY_REDIRECTS).
|
||||
> This was the root cause of the 2026-01-02 production incident.
|
||||
>
|
||||
> See: [INCIDENT_REPORT_20260102.md](../INCIDENT_REPORT_20260102.md)
|
||||
**SSL:** Let's Encrypt via certbot (auto-renewal)
|
||||
|
||||
**Verification:**
|
||||
```bash
|
||||
curl -I https://nordabiznes.pl/health
|
||||
# Expected: HTTP/2 200 OK
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## NPM Reverse Proxy Configuration (STAGING ONLY)
|
||||
|
||||
> **NOTE:** This section now applies to **staging** (staging.nordabiznes.pl) only.
|
||||
> Production no longer uses NPM.
|
||||
|
||||
### Port 5000 Configuration (Staging)
|
||||
|
||||
**Proxy Host ID:** 44 (staging)
|
||||
|
||||
> **HISTORICAL WARNING:** The 2026-01-02 production incident was caused by NPM forwarding to port 80 instead of 5000. See: [INCIDENT_REPORT_20260102.md](../INCIDENT_REPORT_20260102.md)
|
||||
|
||||
### Complete NPM Configuration
|
||||
|
||||
@ -78,7 +93,7 @@ This document provides a **comprehensive reference** of all critical configurati
|
||||
"www.nordabiznes.pl"
|
||||
],
|
||||
"forward_scheme": "http",
|
||||
"forward_host": "10.22.68.249",
|
||||
"forward_host": "57.128.200.27",
|
||||
"forward_port": 5000, // ⚠️ CRITICAL: Must be 5000, NOT 80!
|
||||
"certificate_id": 27,
|
||||
"ssl_forced": true,
|
||||
@ -115,7 +130,7 @@ This document provides a **comprehensive reference** of all critical configurati
|
||||
ssh maciejpi@10.22.68.250 "docker exec nginx-proxy-manager_app_1 \
|
||||
sqlite3 /data/database.sqlite \
|
||||
\"SELECT id, domain_names, forward_host, forward_port FROM proxy_host WHERE id = 27;\""
|
||||
# Expected output: 27|["nordabiznes.pl","www.nordabiznes.pl"]|10.22.68.249|5000
|
||||
# Expected output: 27|["nordabiznes.pl","www.nordabiznes.pl"]|57.128.200.27|5000
|
||||
|
||||
# 2. Verify website is accessible
|
||||
curl -I https://nordabiznes.pl/health
|
||||
@ -182,21 +197,22 @@ else:
|
||||
|
||||
## Port Mappings Reference
|
||||
|
||||
### Complete Port Matrix
|
||||
### Production Port Matrix (OVH VPS 57.128.200.27)
|
||||
|
||||
| Server | Service | Port | Protocol | Access | Purpose |
|
||||
|--------|---------|------|----------|--------|---------|
|
||||
| **Fortigate (WAN)** | Public Gateway | 443 | HTTPS | Public | SSL entry point |
|
||||
| Fortigate (WAN) | HTTP Redirect | 80 | HTTP | Public | Redirect to HTTPS |
|
||||
| **R11-REVPROXY-01** | NPM Proxy | 443 | HTTPS | LAN | SSL termination |
|
||||
| R11-REVPROXY-01 | NPM Proxy | 80 | HTTP | LAN | HTTP → HTTPS redirect |
|
||||
| R11-REVPROXY-01 | NPM Admin | 81 | HTTP | LAN | Admin panel |
|
||||
| R11-REVPROXY-01 | SSH | 22 | SSH | Admin | Remote management |
|
||||
| **NORDABIZ-01** | **Flask/Gunicorn** | **5000** | **HTTP** | **LAN** | **Application (CRITICAL!)** |
|
||||
| NORDABIZ-01 | PostgreSQL | 5432 | TCP | Localhost | Database |
|
||||
| NORDABIZ-01 | Nginx (System) | 80 | HTTP | LAN | ⚠️ DO NOT USE (causes redirect loop) |
|
||||
| NORDABIZ-01 | Nginx (System) | 443 | HTTPS | LAN | ⚠️ DO NOT USE |
|
||||
| NORDABIZ-01 | SSH | 22 | SSH | Admin | Remote management |
|
||||
| **OVH VPS** | Nginx (HTTPS) | 443 | HTTPS | Public | SSL termination + proxy |
|
||||
| OVH VPS | Nginx (HTTP) | 80 | HTTP | Public | Redirect to HTTPS |
|
||||
| OVH VPS | **Gunicorn/Flask** | **5000** | HTTP | **Localhost** | **Application** |
|
||||
| OVH VPS | PostgreSQL | 5432 | TCP | Localhost | Database |
|
||||
| OVH VPS | SSH | 22 | SSH | Public (key-only) | Remote management |
|
||||
|
||||
### Staging Port Matrix (on-prem, via FortiGate + NPM)
|
||||
| **OVH VPS inpi-vps-waw01** | **Flask/Gunicorn** | **5000** | **HTTP** | **LAN** | **Application (CRITICAL!)** |
|
||||
| OVH VPS inpi-vps-waw01 | PostgreSQL | 5432 | TCP | Localhost | Database |
|
||||
| OVH VPS inpi-vps-waw01 | Nginx (System) | 80 | HTTP | LAN | ⚠️ DO NOT USE (causes redirect loop) |
|
||||
| OVH VPS inpi-vps-waw01 | Nginx (System) | 443 | HTTPS | LAN | ⚠️ DO NOT USE |
|
||||
| OVH VPS inpi-vps-waw01 | SSH | 22 | SSH | Admin | Remote management |
|
||||
| **r11-git-inpi** | Gitea HTTPS | 3000 | HTTPS | LAN | Git repository |
|
||||
| r11-git-inpi | SSH | 22 | SSH | Admin | Remote management |
|
||||
|
||||
@ -210,9 +226,9 @@ Public → Internal NAT Mappings:
|
||||
|
||||
Internal → Internal Routing:
|
||||
|
||||
10.22.68.250:* → 10.22.68.249:5000 (NPM → Flask) ⚠️ CRITICAL PORT
|
||||
10.22.68.249:* → 127.0.0.1:5432 (Flask → PostgreSQL)
|
||||
10.22.68.249:* → 10.22.68.180:3000 (Flask → Gitea)
|
||||
10.22.68.250:* → 57.128.200.27:5000 (NPM → Flask) ⚠️ CRITICAL PORT
|
||||
57.128.200.27:* → 127.0.0.1:5432 (Flask → PostgreSQL)
|
||||
57.128.200.27:* → 10.22.68.180:3000 (Flask → Gitea)
|
||||
```
|
||||
|
||||
### Port Usage by Zone
|
||||
@ -226,7 +242,7 @@ Internal → Internal Routing:
|
||||
- Port 80 (HTTP) - NPM HTTP redirect
|
||||
- Port 81 (HTTP) - NPM admin panel (internal only)
|
||||
|
||||
**Application Zone (NORDABIZ-01):**
|
||||
**Application Zone (OVH VPS inpi-vps-waw01):**
|
||||
- **Port 5000 (HTTP) - Flask/Gunicorn application** ⚠️ CRITICAL
|
||||
- Port 5432 (TCP) - PostgreSQL (localhost only)
|
||||
- Port 80/443 (HTTP/HTTPS) - System nginx (DO NOT USE for app)
|
||||
@ -426,7 +442,7 @@ FLASK_ENV=development
|
||||
|
||||
### Production Database
|
||||
|
||||
**Server:** NORDABIZ-01 (10.22.68.249)
|
||||
**Server:** OVH VPS (57.128.200.27, hostname: inpi-vps-waw01)
|
||||
**DBMS:** PostgreSQL 14
|
||||
**Database Name:** `nordabiz`
|
||||
**Port:** 5432 (localhost only)
|
||||
@ -545,7 +561,7 @@ sudo -u postgres psql -U nordabiz_app nordabiz < /tmp/nordabiz_backup.sql
|
||||
|
||||
```bash
|
||||
gunicorn \
|
||||
--bind 0.0.0.0:5000 \
|
||||
--bind 127.0.0.1:5000 \
|
||||
--workers 4 \
|
||||
--timeout 120 \
|
||||
--max-requests 1000 \
|
||||
@ -561,7 +577,7 @@ gunicorn \
|
||||
# Recommended workers formula
|
||||
workers = (2 * num_cpu_cores) + 1
|
||||
|
||||
# For NORDABIZ-01 (4 vCPUs)
|
||||
# For OVH VPS inpi-vps-waw01 (4 vCPUs)
|
||||
workers = (2 * 4) + 1 = 9 # Maximum
|
||||
workers = 4 # Current (conservative)
|
||||
```
|
||||
@ -596,13 +612,13 @@ Requires=postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=www-data
|
||||
Group=www-data
|
||||
User=maciejpi
|
||||
Group=maciejpi
|
||||
WorkingDirectory=/var/www/nordabiznes
|
||||
Environment="PATH=/var/www/nordabiznes/venv/bin"
|
||||
EnvironmentFile=/var/www/nordabiznes/.env
|
||||
ExecStart=/var/www/nordabiznes/venv/bin/gunicorn \
|
||||
--bind 0.0.0.0:5000 \
|
||||
--bind 127.0.0.1:5000 \
|
||||
--workers 4 \
|
||||
--timeout 120 \
|
||||
--max-requests 1000 \
|
||||
@ -691,37 +707,32 @@ sudo systemctl restart nordabiznes
|
||||
| **inpi** (primary) | `https://10.22.68.180:3000/maciejpi/nordabiz.git` | Internal Gitea (deployment source) |
|
||||
| **origin** | `git@github.com:pienczyn/nordabiz.git` | GitHub (cloud backup) |
|
||||
|
||||
### Production Git Configuration
|
||||
### Production Deployment (OVH VPS — no git)
|
||||
|
||||
**Repository Location:** `/var/www/nordabiznes/`
|
||||
**User:** `www-data`
|
||||
**Branch:** `master`
|
||||
**Deployment method:** rsync from development machine (no git on OVH VPS)
|
||||
|
||||
**Git Configuration (`/var/www/nordabiznes/.git/config`):**
|
||||
```ini
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = false
|
||||
logallrefupdates = true
|
||||
**Application Location:** `/var/www/nordabiznes/`
|
||||
**User:** `maciejpi`
|
||||
|
||||
[remote "inpi"]
|
||||
url = https://10.22.68.180:3000/maciejpi/nordabiz.git
|
||||
fetch = +refs/heads/*:refs/remotes/inpi/*
|
||||
### Deployment Workflow
|
||||
|
||||
[remote "origin"]
|
||||
url = git@github.com:pienczyn/nordabiz.git
|
||||
fetch = +refs/heads/*:refs/remotes/origin/*
|
||||
```bash
|
||||
# On development machine (Mac)
|
||||
git push origin master # Push to GitHub
|
||||
git push inpi master # Push to internal Gitea
|
||||
|
||||
[branch "master"]
|
||||
remote = inpi
|
||||
merge = refs/heads/master
|
||||
# Deploy to production via rsync
|
||||
rsync -avz --exclude='.env' --exclude='venv/' --exclude='.git/' \
|
||||
./ maciejpi@57.128.200.27:/var/www/nordabiznes/
|
||||
|
||||
[http]
|
||||
sslVerify = false # Required for self-signed Gitea cert
|
||||
# Restart service
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl reload nordabiznes"
|
||||
|
||||
# Verify
|
||||
curl -I https://nordabiznes.pl/health
|
||||
```
|
||||
|
||||
### Gitea Server Configuration
|
||||
### Gitea Server Configuration (staging deploy source)
|
||||
|
||||
**Server:** r11-git-inpi
|
||||
**IP:** 10.22.68.180
|
||||
@ -730,42 +741,6 @@ sudo systemctl restart nordabiznes
|
||||
**User:** `maciejpi`
|
||||
**Repository:** `maciejpi/nordabiz`
|
||||
|
||||
### Deployment Workflow
|
||||
|
||||
```bash
|
||||
# On development machine (Mac)
|
||||
git push inpi master # Push to internal Gitea
|
||||
git push origin master # Backup to GitHub (optional)
|
||||
|
||||
# On production server (NORDABIZ-01)
|
||||
ssh maciejpi@10.22.68.249
|
||||
cd /var/www/nordabiznes
|
||||
sudo -u www-data git pull # Pull from Gitea
|
||||
sudo systemctl restart nordabiznes # Restart application
|
||||
curl -I https://nordabiznes.pl/health # Verify deployment
|
||||
```
|
||||
|
||||
### Git Commands for Production
|
||||
|
||||
```bash
|
||||
# Check current branch and status
|
||||
sudo -u www-data git branch
|
||||
sudo -u www-data git status
|
||||
|
||||
# Pull latest changes
|
||||
sudo -u www-data git pull
|
||||
|
||||
# View commit history
|
||||
sudo -u www-data git log --oneline -10
|
||||
|
||||
# Rollback to previous commit (emergency)
|
||||
sudo -u www-data git reset --hard HEAD~1
|
||||
|
||||
# Force pull (discard local changes)
|
||||
sudo -u www-data git fetch --all
|
||||
sudo -u www-data git reset --hard inpi/master
|
||||
```
|
||||
|
||||
### SSH Keys for Git Access
|
||||
|
||||
**Production Server:**
|
||||
@ -881,16 +856,16 @@ Policy 5: Default Deny
|
||||
Action: DENY
|
||||
```
|
||||
|
||||
### Linux iptables (NORDABIZ-01)
|
||||
### OVH VPS Firewall (ufw)
|
||||
|
||||
**Status:** Not actively used (relies on Fortigate firewall)
|
||||
**Status:** Active on OVH VPS (production does not use FortiGate)
|
||||
|
||||
**Default Policy:**
|
||||
```bash
|
||||
# Check iptables status
|
||||
sudo iptables -L -n -v
|
||||
# Check ufw status
|
||||
sudo ufw status verbose
|
||||
|
||||
# Expected: Mostly ACCEPT policies (Fortigate handles filtering)
|
||||
# Expected rules: ALLOW 22/tcp, 80/tcp, 443/tcp
|
||||
```
|
||||
|
||||
### PostgreSQL Access Control
|
||||
@ -939,7 +914,7 @@ sudo -u postgres psql -U nordabiz_app nordabiz < \
|
||||
- **Location:** Proxmox Backup Server
|
||||
- **Schedule:** Weekly
|
||||
- **Retention:** 4 weeks
|
||||
- **VM ID:** 249 (NORDABIZ-01)
|
||||
- **VM ID:** 249 (OVH VPS inpi-vps-waw01)
|
||||
|
||||
### Configuration Backups
|
||||
|
||||
@ -1043,7 +1018,7 @@ sudo chmod 600 /home/maciejpi/backups/.env.backup
|
||||
4. **Apply change to production**
|
||||
```bash
|
||||
# SSH to production
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
|
||||
# Pull changes
|
||||
cd /var/www/nordabiznes
|
||||
@ -1138,7 +1113,7 @@ ssh maciejpi@10.22.68.250 "docker exec nginx-proxy-manager_app_1 \
|
||||
\"SELECT id, domain_names, forward_host, forward_port FROM proxy_host WHERE id = 27;\""
|
||||
|
||||
# Expected output:
|
||||
# 27|["nordabiznes.pl","www.nordabiznes.pl"]|10.22.68.249|5000
|
||||
# 27|["nordabiznes.pl","www.nordabiznes.pl"]|57.128.200.27|5000
|
||||
# ^^^^
|
||||
# MUST BE 5000!
|
||||
|
||||
@ -1193,7 +1168,7 @@ sudo journalctl -u nordabiznes -p err -n 20
|
||||
|
||||
| Server | IP | SSH User | Purpose |
|
||||
|--------|-----|----------|---------|
|
||||
| NORDABIZ-01 | 10.22.68.249 | `maciejpi` | Application server |
|
||||
| OVH VPS inpi-vps-waw01 | 57.128.200.27 | `maciejpi` | Application server |
|
||||
| R11-REVPROXY-01 | 10.22.68.250 | `maciejpi` | NPM proxy |
|
||||
| r11-git-inpi | 10.22.68.180 | `maciejpi` | Gitea repository |
|
||||
|
||||
@ -1206,7 +1181,7 @@ sudo journalctl -u nordabiznes -p err -n 20
|
||||
curl -I https://nordabiznes.pl/health
|
||||
|
||||
# 2. Check service status
|
||||
ssh maciejpi@10.22.68.249 "sudo systemctl status nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl status nordabiznes"
|
||||
|
||||
# 3. Check NPM proxy configuration
|
||||
ssh maciejpi@10.22.68.250 "docker exec nginx-proxy-manager_app_1 \
|
||||
@ -1214,10 +1189,10 @@ ssh maciejpi@10.22.68.250 "docker exec nginx-proxy-manager_app_1 \
|
||||
\"SELECT forward_port FROM proxy_host WHERE id = 27;\""
|
||||
|
||||
# 4. View recent errors
|
||||
ssh maciejpi@10.22.68.249 "sudo journalctl -u nordabiznes -p err -n 20"
|
||||
ssh maciejpi@57.128.200.27 "sudo journalctl -u nordabiznes -p err -n 20"
|
||||
|
||||
# 5. Restart application
|
||||
ssh maciejpi@10.22.68.249 "sudo systemctl restart nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl restart nordabiznes"
|
||||
```
|
||||
|
||||
### Documentation Resources
|
||||
@ -1238,7 +1213,7 @@ ssh maciejpi@10.22.68.249 "sudo systemctl restart nordabiznes"
|
||||
```
|
||||
Proxy Host ID: 27
|
||||
Domains: nordabiznes.pl, www.nordabiznes.pl
|
||||
Backend: 10.22.68.249:5000 ⚠️ PORT 5000 (NOT 80!)
|
||||
Backend: 57.128.200.27:5000 ⚠️ PORT 5000 (NOT 80!)
|
||||
SSL: Let's Encrypt (auto-renew)
|
||||
```
|
||||
|
||||
@ -1271,7 +1246,7 @@ curl -I https://nordabiznes.pl/health # Test health
|
||||
### Quick Reference: Emergency Rollback
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
cd /var/www/nordabiznes
|
||||
sudo systemctl stop nordabiznes
|
||||
sudo -u www-data git reset --hard HEAD~1
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
# Security Architecture
|
||||
|
||||
**Document Version:** 1.0
|
||||
**Last Updated:** 2026-01-10
|
||||
**Status:** Production LIVE
|
||||
**Last Updated:** 2026-04-04
|
||||
**Status:** Production LIVE (OVH VPS)
|
||||
**Diagram Type:** Security Architecture / Threat Model
|
||||
|
||||
---
|
||||
@ -60,35 +60,29 @@ graph TB
|
||||
Internet["🌐 Public Internet<br/><br/>Trust Level: NONE<br/>Access: Anonymous users<br/>Threat Level: HIGH<br/><br/>Threats:<br/>• DDoS attacks<br/>• SQL injection<br/>• XSS attacks<br/>• CSRF attacks<br/>• Brute force<br/>• Bot traffic"]
|
||||
end
|
||||
|
||||
subgraph "Zone 1: Network Perimeter (SECURITY BOUNDARY)"
|
||||
Fortigate["🛡️ FORTIGATE FIREWALL<br/><br/>Trust Level: BOUNDARY<br/>Controls:<br/>• NAT (85.237.177.83 → 10.22.68.250)<br/>• Port filtering (443, 80, 22)<br/>• Stateful inspection<br/>• DDoS protection<br/>• Intrusion prevention<br/><br/>Default Policy: DENY ALL"]
|
||||
subgraph "Zone 1: Nginx Reverse Proxy (SECURITY BOUNDARY)"
|
||||
Nginx["🔒 NGINX REVERSE PROXY<br/>IP: 57.128.200.27<br/><br/>Trust Level: BOUNDARY<br/>Controls:<br/>• SSL/TLS termination (Let's Encrypt)<br/>• HTTP → HTTPS redirect<br/>• Request filtering<br/>• HSTS enforcement<br/>• Security headers<br/><br/>Exposed Ports: 443, 80<br/>Proxy to: 127.0.0.1:5000"]
|
||||
end
|
||||
|
||||
subgraph "Zone 2: DMZ - Reverse Proxy (SEMI-TRUSTED)"
|
||||
DMZ["🖥️ NPM REVERSE PROXY<br/>IP: 10.22.68.250<br/><br/>Trust Level: LOW<br/>Controls:<br/>• SSL/TLS termination<br/>• HTTP → HTTPS redirect<br/>• Let's Encrypt certificates<br/>• Request filtering (block exploits)<br/>• WebSocket upgrade control<br/>• HSTS enforcement<br/><br/>Exposed Ports: 443, 80, 81 (admin)<br/>Allowed Outbound: App Zone only"]
|
||||
end
|
||||
|
||||
subgraph "Zone 3: Application Zone (TRUSTED)"
|
||||
AppZone["🖥️ APPLICATION SERVER<br/>IP: 10.22.68.249<br/><br/>Trust Level: MEDIUM<br/>Controls:<br/>• Flask-Login authentication<br/>• CSRF protection (Flask-WTF)<br/>• Rate limiting (Flask-Limiter)<br/>• Input sanitization<br/>• XSS prevention<br/>• SQL injection prevention (SQLAlchemy ORM)<br/>• Session security (secure cookies)<br/><br/>Exposed Ports: 5000 (internal), 22 (SSH)<br/>Allowed Outbound: Internet (APIs), Data Zone"]
|
||||
subgraph "Zone 2: Application Zone (TRUSTED)"
|
||||
AppZone["🖥️ APPLICATION SERVER<br/>OVH VPS (57.128.200.27)<br/><br/>Trust Level: MEDIUM<br/>Controls:<br/>• Flask-Login authentication<br/>• CSRF protection (Flask-WTF)<br/>• Rate limiting (Flask-Limiter)<br/>• Input sanitization<br/>• XSS prevention<br/>• SQL injection prevention (SQLAlchemy ORM)<br/>• Session security (secure cookies)<br/><br/>Gunicorn: 127.0.0.1:5000 (localhost only)<br/>SSH: 22 (key-based auth)"]
|
||||
end
|
||||
|
||||
subgraph "Zone 4: Data Zone (HIGHLY TRUSTED)"
|
||||
DataZone["🗄️ DATABASE SERVER<br/>IP: 10.22.68.249:5432<br/><br/>Trust Level: HIGH<br/>Controls:<br/>• PostgreSQL authentication<br/>• Localhost-only binding (127.0.0.1)<br/>• Role-based access control<br/>• Connection encryption (SSL/TLS)<br/>• pg_hba.conf restrictions<br/>• Database user separation<br/><br/>Exposed Ports: 5432 (localhost only)<br/>Allowed Connections: Application Zone only"]
|
||||
DataZone["🗄️ DATABASE SERVER<br/>IP: 57.128.200.27:5432<br/><br/>Trust Level: HIGH<br/>Controls:<br/>• PostgreSQL authentication<br/>• Localhost-only binding (127.0.0.1)<br/>• Role-based access control<br/>• Connection encryption (SSL/TLS)<br/>• pg_hba.conf restrictions<br/>• Database user separation<br/><br/>Exposed Ports: 5432 (localhost only)<br/>Allowed Connections: Application Zone only"]
|
||||
end
|
||||
|
||||
subgraph "Zone 5: External APIs (THIRD-PARTY)"
|
||||
APIs["☁️ EXTERNAL APIs<br/><br/>Trust Level: THIRD-PARTY<br/>Services:<br/>• Google Gemini AI<br/>• Google PageSpeed Insights<br/>• Google Places API<br/>• Microsoft Graph API<br/>• Brave Search API<br/>• KRS Open API<br/><br/>Controls:<br/>• API key authentication<br/>• OAuth 2.0 (MS Graph)<br/>• HTTPS/TLS 1.2+ only<br/>• Rate limiting (client-side)<br/>• API key rotation<br/>• Cost tracking"]
|
||||
end
|
||||
|
||||
Internet -->|"HTTPS :443<br/>HTTP :80"| Fortigate
|
||||
Fortigate -->|"NAT + Filter<br/>Allow: 443, 80"| DMZ
|
||||
DMZ -->|"HTTP :5000<br/>(internal network)"| AppZone
|
||||
Internet -->|"HTTPS :443<br/>HTTP :80"| Nginx
|
||||
Nginx -->|"HTTP :5000<br/>(localhost)"| AppZone
|
||||
AppZone -->|"PostgreSQL :5432<br/>(localhost)"| DataZone
|
||||
AppZone -->|"HTTPS<br/>(API requests)"| APIs
|
||||
|
||||
style Internet fill:#ff6b6b,color:#fff
|
||||
style Fortigate fill:#f59e0b,color:#fff
|
||||
style DMZ fill:#fbbf24,color:#000
|
||||
style Nginx fill:#f59e0b,color:#fff
|
||||
style AppZone fill:#10b981,color:#fff
|
||||
style DataZone fill:#3b82f6,color:#fff
|
||||
style APIs fill:#8b5cf6,color:#fff
|
||||
@ -98,35 +92,31 @@ graph TB
|
||||
|
||||
| Boundary | Between Zones | Security Controls | Threat Mitigation |
|
||||
|----------|---------------|-------------------|-------------------|
|
||||
| **External → Perimeter** | Internet → Fortigate | NAT, port filtering, stateful firewall | DDoS, port scanning, unauthorized access |
|
||||
| **Perimeter → DMZ** | Fortigate → NPM | Port restrictions (443, 80), SSL enforcement | Man-in-the-middle, protocol attacks |
|
||||
| **DMZ → Application** | NPM → Flask | Internal network isolation, port 5000 only | Lateral movement, privilege escalation |
|
||||
| **External → Proxy** | Internet → Nginx | SSL termination, request filtering, HSTS | DDoS, port scanning, unauthorized access |
|
||||
| **Proxy → Application** | Nginx → Gunicorn | Localhost-only binding (127.0.0.1:5000) | Lateral movement, privilege escalation |
|
||||
| **Application → Data** | Flask → PostgreSQL | Localhost-only binding, role-based access | SQL injection, unauthorized data access |
|
||||
| **Application → Internet** | Flask → External APIs | HTTPS/TLS, API key authentication, rate limiting | API key theft, cost overrun, data leakage |
|
||||
|
||||
### 1.3 Network Segmentation
|
||||
|
||||
**Physical Segmentation:**
|
||||
- **10.22.68.0/24** - Internal INPI network (RFC 1918 private addressing)
|
||||
- **85.237.177.83** - Public IP (NAT at Fortigate)
|
||||
**Production (OVH VPS):**
|
||||
- **57.128.200.27** - Public IP (OVH VPS, direct internet access)
|
||||
- **Nginx:** Port 443/80 (public, SSL termination)
|
||||
- **Gunicorn:** 127.0.0.1:5000 (localhost only, via nginx proxy_pass)
|
||||
- **PostgreSQL:** 127.0.0.1:5432 (localhost only)
|
||||
|
||||
**Logical Segmentation:**
|
||||
- **DMZ:** 10.22.68.250 (reverse proxy only)
|
||||
- **Application:** 10.22.68.249 (Flask app, no direct Internet access for incoming)
|
||||
- **Data:** 10.22.68.249:5432 (localhost binding, no network exposure)
|
||||
**Staging (on-prem):**
|
||||
- **10.22.68.0/24** - Internal INPI network
|
||||
- **85.237.177.83** - Public IP (NAT at FortiGate for staging)
|
||||
- FortiGate + NPM (10.22.68.250) for staging.nordabiznes.pl
|
||||
|
||||
**Firewall Rules (Fortigate):**
|
||||
**OVH VPS Firewall (ufw):**
|
||||
```
|
||||
# Inbound (WAN → LAN)
|
||||
allow tcp/443 from ANY to 10.22.68.250 # HTTPS to NPM
|
||||
allow tcp/80 from ANY to 10.22.68.250 # HTTP to NPM (redirects to HTTPS)
|
||||
allow tcp/22 from ADMIN_NET to 10.22.68.249 # SSH (admin only)
|
||||
deny all from ANY to ANY # Default deny
|
||||
|
||||
# Outbound (LAN → WAN)
|
||||
allow tcp/443 from 10.22.68.249 to ANY # API calls (HTTPS)
|
||||
allow tcp/80 from 10.22.68.249 to ANY # HTTP (rare, redirects)
|
||||
deny all from 10.22.68.250 to ANY # NPM cannot initiate outbound (except Let's Encrypt)
|
||||
# Production firewall rules
|
||||
allow tcp/443 from ANY # HTTPS
|
||||
allow tcp/80 from ANY # HTTP (redirect to HTTPS)
|
||||
allow tcp/22 from ANY # SSH (key-based auth only)
|
||||
deny all other inbound
|
||||
```
|
||||
|
||||
### 1.4 Attack Surface
|
||||
@ -146,8 +136,8 @@ deny all from 10.22.68.250 to ANY # NPM cannot initiate outbound (except L
|
||||
|
||||
**Reduced Attack Surface:**
|
||||
- PostgreSQL: Localhost-only binding (no network exposure)
|
||||
- SSH: Restricted to admin network (firewall rule)
|
||||
- NPM Admin UI: Port 81 (internal network only)
|
||||
- Gunicorn: Localhost-only binding (127.0.0.1:5000, not exposed to internet)
|
||||
- SSH: Key-based authentication only (password auth disabled)
|
||||
|
||||
---
|
||||
|
||||
@ -670,7 +660,7 @@ def ratelimit_handler(e):
|
||||
|
||||
#### 4.4.1 Security Headers
|
||||
|
||||
**HTTP Security Headers (Configured in NPM):**
|
||||
**HTTP Security Headers (Configured in nginx):**
|
||||
|
||||
```
|
||||
# HSTS (HTTP Strict Transport Security)
|
||||
@ -693,7 +683,7 @@ Referrer-Policy: strict-origin-when-cross-origin
|
||||
```
|
||||
|
||||
**Current Status:**
|
||||
- ✅ HSTS enabled (NPM configuration)
|
||||
- ✅ HSTS enabled (nginx configuration)
|
||||
- ✅ X-Frame-Options: SAMEORIGIN
|
||||
- ✅ X-Content-Type-Options: nosniff
|
||||
- ❌ Content-Security-Policy (not yet implemented - requires frontend refactoring)
|
||||
@ -704,7 +694,7 @@ Referrer-Policy: strict-origin-when-cross-origin
|
||||
- **Certificate Provider:** Let's Encrypt (free, auto-renewal)
|
||||
- **Certificate Type:** RSA 2048-bit
|
||||
- **TLS Protocols:** TLS 1.2, TLS 1.3 only
|
||||
- **HTTP → HTTPS Redirect:** Enforced at NPM
|
||||
- **HTTP → HTTPS Redirect:** Enforced at nginx
|
||||
- **HSTS:** Enabled (max-age=31536000)
|
||||
|
||||
**Cipher Suites (Modern):**
|
||||
@ -716,11 +706,11 @@ ECDHE-RSA-AES128-GCM-SHA256
|
||||
ECDHE-RSA-AES256-GCM-SHA384
|
||||
```
|
||||
|
||||
**SSL Termination:** At NPM reverse proxy (10.22.68.250)
|
||||
**Internal Communication:** HTTP (10.22.68.250 → 10.22.68.249:5000)
|
||||
**SSL Termination:** At nginx on OVH VPS (57.128.200.27)
|
||||
**Internal Communication:** HTTP (nginx → 127.0.0.1:5000)
|
||||
|
||||
**Certificate Renewal:**
|
||||
- Automatic via NPM + Let's Encrypt
|
||||
- Automatic via certbot + Let's Encrypt (on OVH VPS)
|
||||
- Renewal frequency: Every 90 days
|
||||
- Grace period: 30 days before expiry
|
||||
|
||||
@ -851,16 +841,17 @@ SMTP_PASSWORD=<password>
|
||||
|
||||
#### 5.2.1 External Attack Surface (Internet-facing)
|
||||
|
||||
**NPM Reverse Proxy (10.22.68.250:443, :80):**
|
||||
**Nginx Reverse Proxy (57.128.200.27:443, :80):**
|
||||
- **Exposed Services:** HTTPS (443), HTTP (80, redirects to HTTPS)
|
||||
- **Attack Vectors:**
|
||||
- DDoS attacks (mitigated by Fortigate)
|
||||
- DDoS attacks (mitigated by OVH DDoS protection)
|
||||
- SSL/TLS vulnerabilities (mitigated by modern cipher suites)
|
||||
- HTTP request smuggling (mitigated by NPM validation)
|
||||
- HTTP request smuggling (mitigated by nginx validation)
|
||||
- **Mitigation:**
|
||||
- Fortigate stateful firewall + DDoS protection
|
||||
- OVH network-level DDoS protection
|
||||
- Let's Encrypt TLS 1.2/1.3 only
|
||||
- NPM request filtering ("block exploits" enabled)
|
||||
- ufw firewall on VPS
|
||||
- nginx request filtering
|
||||
|
||||
**Public Endpoints:**
|
||||
- `/` (Company directory)
|
||||
@ -909,7 +900,7 @@ SMTP_PASSWORD=<password>
|
||||
|
||||
#### 5.2.4 Database Attack Surface
|
||||
|
||||
**PostgreSQL (10.22.68.249:5432):**
|
||||
**PostgreSQL (57.128.200.27:5432):**
|
||||
- **Binding:** Localhost only (127.0.0.1)
|
||||
- **Authentication:** Password-based (pg_hba.conf)
|
||||
- **Encryption:** SSL/TLS for connections (planned)
|
||||
@ -1249,12 +1240,12 @@ def send_chat_message(id):
|
||||
# Inbound rules
|
||||
allow tcp/443 from ANY to 10.22.68.250 # HTTPS to NPM
|
||||
allow tcp/80 from ANY to 10.22.68.250 # HTTP to NPM
|
||||
allow tcp/22 from ADMIN_NET to 10.22.68.249 # SSH (admin only)
|
||||
allow tcp/22 from ADMIN_NET to 57.128.200.27 # SSH (admin only)
|
||||
deny all from ANY to ANY # Default deny
|
||||
|
||||
# Outbound rules
|
||||
allow tcp/443 from 10.22.68.249 to ANY # API calls (HTTPS)
|
||||
allow tcp/80 from 10.22.68.249 to ANY # HTTP (rare)
|
||||
allow tcp/443 from 57.128.200.27 to ANY # API calls (HTTPS)
|
||||
allow tcp/80 from 57.128.200.27 to ANY # HTTP (rare)
|
||||
deny all from 10.22.68.250 to ANY # NPM cannot initiate outbound
|
||||
```
|
||||
|
||||
@ -1267,7 +1258,7 @@ deny all from 10.22.68.250 to ANY # NPM cannot initiate outbound
|
||||
|
||||
**Network Segmentation:**
|
||||
- **DMZ Zone:** 10.22.68.250 (NPM only)
|
||||
- **Application Zone:** 10.22.68.249 (Flask + PostgreSQL)
|
||||
- **Application Zone:** 57.128.200.27 (Flask + PostgreSQL)
|
||||
- **Internal Services:** 10.22.68.180 (Git server)
|
||||
|
||||
**Network Security Best Practices:**
|
||||
@ -1558,8 +1549,8 @@ ssh maciejpi@10.22.68.250
|
||||
docker ps | grep npm
|
||||
|
||||
# 4. Check network connectivity
|
||||
ping 10.22.68.249
|
||||
curl http://10.22.68.249:5000/health
|
||||
ping 57.128.200.27
|
||||
curl http://57.128.200.27:5000/health
|
||||
```
|
||||
|
||||
**Recovery:**
|
||||
@ -1590,7 +1581,7 @@ docker restart <npm-container-id>
|
||||
|
||||
**Lessons Learned:**
|
||||
- Document critical configurations (port mappings) in architecture docs
|
||||
- Add verification steps after NPM configuration changes
|
||||
- Add verification steps after nginx configuration changes
|
||||
- Implement monitoring to detect redirect loops
|
||||
|
||||
---
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# 11. Troubleshooting Guide
|
||||
|
||||
**Document Type:** Operations Guide
|
||||
**Last Updated:** 2026-01-10
|
||||
**Last Updated:** 2026-04-04
|
||||
**Maintainer:** DevOps Team
|
||||
|
||||
---
|
||||
@ -80,7 +80,7 @@ graph TD
|
||||
|
||||
- Browser error: `ERR_TOO_MANY_REDIRECTS`
|
||||
- Portal completely inaccessible via https://nordabiznes.pl
|
||||
- Internal access works fine (http://10.22.68.249:5000)
|
||||
- Internal access works fine (http://57.128.200.27:5000)
|
||||
- Affects 100% of external users
|
||||
|
||||
#### Root Cause
|
||||
@ -103,15 +103,15 @@ docker exec nginx-proxy-manager_app_1 \
|
||||
"SELECT id, domain_names, forward_host, forward_port FROM proxy_host WHERE id = 27;"
|
||||
|
||||
# Expected output:
|
||||
# 27|["nordabiznes.pl","www.nordabiznes.pl"]|10.22.68.249|5000
|
||||
# 27|["nordabiznes.pl","www.nordabiznes.pl"]|57.128.200.27|5000
|
||||
|
||||
# If forward_port shows 80 → PROBLEM FOUND!
|
||||
|
||||
# 2. Test backend directly
|
||||
curl -I http://10.22.68.249:80/
|
||||
curl -I http://57.128.200.27:80/
|
||||
# If this returns 301 redirect → confirms issue
|
||||
|
||||
curl -I http://10.22.68.249:5000/health
|
||||
curl -I http://57.128.200.27:5000/health
|
||||
# Should return 200 OK if Flask is running
|
||||
```
|
||||
|
||||
@ -125,7 +125,7 @@ open http://10.22.68.250:81
|
||||
|
||||
# 2. Navigate to: Proxy Hosts → nordabiznes.pl (ID 27)
|
||||
# 3. Edit configuration:
|
||||
# - Forward Hostname/IP: 10.22.68.249
|
||||
# - Forward Hostname/IP: 57.128.200.27
|
||||
# - Forward Port: 5000 (CRITICAL!)
|
||||
# - Scheme: http
|
||||
# 4. Save and test
|
||||
@ -142,7 +142,7 @@ NPM_URL = "http://10.22.68.250:81/api"
|
||||
data = {
|
||||
"domain_names": ["nordabiznes.pl", "www.nordabiznes.pl"],
|
||||
"forward_scheme": "http",
|
||||
"forward_host": "10.22.68.249",
|
||||
"forward_host": "57.128.200.27",
|
||||
"forward_port": 5000, # CRITICAL: Must be 5000!
|
||||
"certificate_id": 27,
|
||||
"ssl_forced": True,
|
||||
@ -191,14 +191,14 @@ docker logs nginx-proxy-manager_app_1 --tail 20
|
||||
#### Root Causes
|
||||
|
||||
1. Flask/Gunicorn service stopped
|
||||
2. Backend server (10.22.68.249) unreachable
|
||||
2. Backend server (57.128.200.27) unreachable
|
||||
3. Firewall blocking port 5000
|
||||
|
||||
#### Diagnosis
|
||||
|
||||
```bash
|
||||
# 1. Check Flask service status
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
sudo systemctl status nordabiznes
|
||||
|
||||
# 2. Check if port 5000 is listening
|
||||
@ -250,10 +250,10 @@ sudo -u www-data /var/www/nordabiznes/venv/bin/python3 app.py
|
||||
```bash
|
||||
# Test connectivity from NPM to backend
|
||||
ssh maciejpi@10.22.68.250
|
||||
curl -I http://10.22.68.249:5000/health
|
||||
curl -I http://57.128.200.27:5000/health
|
||||
|
||||
# Check firewall rules
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
sudo iptables -L -n | grep 5000
|
||||
```
|
||||
|
||||
@ -287,7 +287,7 @@ curl -I https://nordabiznes.pl/health
|
||||
|
||||
```bash
|
||||
# 1. Check Gunicorn worker status
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
ps aux | grep gunicorn
|
||||
# Look for zombie workers or high CPU usage
|
||||
|
||||
@ -440,7 +440,7 @@ nslookup nordabiznes.pl 8.8.8.8
|
||||
|
||||
# 2. Check internal DNS (inpi.local)
|
||||
nslookup nordabiznes.inpi.local 10.22.68.1
|
||||
# Should return: 10.22.68.249
|
||||
# Should return: 57.128.200.27
|
||||
|
||||
# 3. Test from different locations
|
||||
curl -I -H "Host: nordabiznes.pl" http://85.237.177.83/health
|
||||
@ -486,7 +486,7 @@ curl -I -H "Host: nordabiznes.pl" http://85.237.177.83/health
|
||||
|
||||
```bash
|
||||
# 1. Check service status
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
sudo systemctl status nordabiznes
|
||||
|
||||
# 2. Check recent logs
|
||||
@ -609,7 +609,7 @@ curl http://localhost:5000/health
|
||||
# Look for JavaScript errors
|
||||
|
||||
# 2. Check Flask logs
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
sudo journalctl -u nordabiznes -n 50 --no-pager | grep ERROR
|
||||
|
||||
# 3. Check template rendering
|
||||
@ -691,7 +691,7 @@ sudo -u www-data psql -h localhost -U nordabiz_app -d nordabiz -c "SELECT 1;"
|
||||
curl "https://nordabiznes.pl/search?q=test" -v
|
||||
|
||||
# 2. Check search_service.py logs
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
sudo journalctl -u nordabiznes -n 100 | grep -i search
|
||||
|
||||
# 3. Test database FTS
|
||||
@ -786,7 +786,7 @@ Quick check:
|
||||
|
||||
```bash
|
||||
# 1. Verify Gemini API key
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
sudo -u www-data cat /var/www/nordabiznes/.env | grep GEMINI_API_KEY
|
||||
# Should not be empty
|
||||
|
||||
@ -818,7 +818,7 @@ curl -H "x-goog-api-key: YOUR_API_KEY" \
|
||||
|
||||
```bash
|
||||
# 1. Check PostgreSQL service
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
sudo systemctl status postgresql
|
||||
|
||||
# 2. Check PostgreSQL is listening
|
||||
@ -986,7 +986,7 @@ SELECT * FROM companies WHERE name ILIKE '%test%' LIMIT 10;
|
||||
|
||||
```bash
|
||||
# 1. Check disk usage
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
df -h
|
||||
# Check /var/lib/postgresql usage
|
||||
|
||||
@ -1140,7 +1140,7 @@ sudo -u www-data psql -h localhost -U nordabiz_app -d nordabiz
|
||||
|
||||
```bash
|
||||
# 1. Check API usage in database
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
sudo -u www-data psql -h localhost -U nordabiz_app -d nordabiz
|
||||
|
||||
-- Gemini API usage today
|
||||
@ -1248,7 +1248,7 @@ curl "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-pre
|
||||
-d '{"contents":[{"parts":[{"text":"Hello, test"}]}]}'
|
||||
|
||||
# 2. Check Flask logs for Gemini errors
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
sudo journalctl -u nordabiznes -n 100 | grep -i gemini
|
||||
|
||||
# 3. Check conversation ownership
|
||||
@ -1368,7 +1368,7 @@ PAGESPEED_KEY=$(sudo -u www-data grep GOOGLE_PAGESPEED_API_KEY /var/www/nordabiz
|
||||
curl "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=https://nordabiznes.pl&key=$PAGESPEED_KEY"
|
||||
|
||||
# 2. Check audit logs
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
sudo journalctl -u nordabiznes -n 100 | grep -i pagespeed
|
||||
|
||||
# 3. Check recent audits
|
||||
@ -1495,7 +1495,7 @@ sudo systemctl restart nordabiznes
|
||||
|
||||
```bash
|
||||
# 1. Check user exists and is active
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
sudo -u www-data psql -h localhost -U nordabiz_app -d nordabiz
|
||||
|
||||
SELECT id, email, is_active, email_verified, failed_login_attempts
|
||||
@ -1777,7 +1777,7 @@ WHERE email = 'user@example.com';
|
||||
time curl -I https://nordabiznes.pl/
|
||||
|
||||
# 2. Check Gunicorn worker status
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
ps aux | grep gunicorn
|
||||
# Look for: worker processes (should be 4-8)
|
||||
|
||||
@ -1917,7 +1917,7 @@ done
|
||||
|
||||
```bash
|
||||
# 1. Check memory usage
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
free -h
|
||||
|
||||
# 2. Check which process using memory
|
||||
@ -2006,7 +2006,7 @@ echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
|
||||
|
||||
```bash
|
||||
# 1. Check CPU usage
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
top -n 1
|
||||
|
||||
# Look for processes using >80% CPU
|
||||
@ -2093,7 +2093,7 @@ docker ps | grep nginx-proxy-manager
|
||||
# Should show: Up X hours
|
||||
|
||||
# Flask service health
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
sudo systemctl status nordabiznes
|
||||
# Should show: active (running)
|
||||
```
|
||||
@ -2172,7 +2172,7 @@ ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
|
||||
|
||||
```bash
|
||||
# Check last backup
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
ls -lah /backup/nordabiz/ | head -10
|
||||
|
||||
# Expected: Daily backups (.sql files)
|
||||
@ -2204,7 +2204,7 @@ curl -I https://nordabiznes.pl/health
|
||||
# If fails, proceed
|
||||
|
||||
# 2. Check from internal network
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
curl -I http://localhost:5000/health
|
||||
# If this works → Network/NPM issue
|
||||
# If this fails → Application issue
|
||||
@ -2232,7 +2232,7 @@ docker exec nginx-proxy-manager_app_1 \
|
||||
sqlite3 /data/database.sqlite \
|
||||
"SELECT id, forward_host, forward_port FROM proxy_host WHERE id = 27;"
|
||||
|
||||
# Must show: 27|10.22.68.249|5000
|
||||
# Must show: 27|57.128.200.27|5000
|
||||
|
||||
# 3. Check Fortigate NAT
|
||||
# Access Fortigate admin panel
|
||||
@ -2243,7 +2243,7 @@ docker exec nginx-proxy-manager_app_1 \
|
||||
|
||||
```bash
|
||||
# 1. Check Flask service
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
sudo systemctl status nordabiznes
|
||||
|
||||
# If failed, check logs
|
||||
@ -2361,7 +2361,7 @@ pg_restore -t companies -d nordabiz /backup/nordabiz/latest.sql
|
||||
|
||||
```bash
|
||||
# 1. ISOLATE the server
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
|
||||
# Block all incoming traffic except your IP
|
||||
sudo iptables -A INPUT -s YOUR_IP -j ACCEPT
|
||||
@ -2447,11 +2447,11 @@ sudo systemctl restart nordabiznes
|
||||
echo "=== Application Health ===" && \
|
||||
curl -I https://nordabiznes.pl/health && \
|
||||
echo -e "\n=== Service Status ===" && \
|
||||
ssh maciejpi@10.22.68.249 "sudo systemctl status nordabiznes --no-pager | head -5" && \
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl status nordabiznes --no-pager | head -5" && \
|
||||
echo -e "\n=== Database Connection ===" && \
|
||||
ssh maciejpi@10.22.68.249 "sudo -u www-data psql -h localhost -U nordabiz_app -d nordabiz -c 'SELECT count(*) FROM companies;'" && \
|
||||
ssh maciejpi@57.128.200.27 "sudo -u www-data psql -h localhost -U nordabiz_app -d nordabiz -c 'SELECT count(*) FROM companies;'" && \
|
||||
echo -e "\n=== Server Load ===" && \
|
||||
ssh maciejpi@10.22.68.249 "uptime"
|
||||
ssh maciejpi@57.128.200.27 "uptime"
|
||||
```
|
||||
|
||||
### 10.2 NPM Proxy Diagnostics
|
||||
@ -2469,7 +2469,7 @@ ssh maciejpi@10.22.68.250 "docker logs nginx-proxy-manager_app_1 --tail 20 -f"
|
||||
ssh maciejpi@10.22.68.250 "docker ps | grep nginx-proxy-manager"
|
||||
|
||||
# Test backend from NPM server
|
||||
ssh maciejpi@10.22.68.250 "curl -I http://10.22.68.249:5000/health"
|
||||
ssh maciejpi@10.22.68.250 "curl -I http://57.128.200.27:5000/health"
|
||||
```
|
||||
|
||||
### 10.3 Database Diagnostics
|
||||
@ -2508,10 +2508,10 @@ for i in {1..10}; do
|
||||
done
|
||||
|
||||
# Server resource usage
|
||||
ssh maciejpi@10.22.68.249 "top -b -n 1 | head -20"
|
||||
ssh maciejpi@57.128.200.27 "top -b -n 1 | head -20"
|
||||
|
||||
# Disk usage
|
||||
ssh maciejpi@10.22.68.249 "df -h && echo -e '\n=== Top 10 Directories ===\n' && du -sh /* 2>/dev/null | sort -rh | head -10"
|
||||
ssh maciejpi@57.128.200.27 "df -h && echo -e '\n=== Top 10 Directories ===\n' && du -sh /* 2>/dev/null | sort -rh | head -10"
|
||||
|
||||
# Network connectivity
|
||||
ping -c 5 nordabiznes.pl
|
||||
@ -2525,7 +2525,7 @@ echo | openssl s_client -servername nordabiznes.pl -connect nordabiznes.pl:443 2
|
||||
|
||||
```bash
|
||||
# Test all external APIs
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
|
||||
# Gemini API
|
||||
GEMINI_KEY=$(sudo -u www-data grep GEMINI_API_KEY .env | cut -d= -f2)
|
||||
@ -2549,16 +2549,16 @@ curl -s "https://api-krs.ms.gov.pl/api/krs/OdpisAktualny/0000878913" | jq '.odpi
|
||||
|
||||
```bash
|
||||
# Check current deployment version
|
||||
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && git log --oneline -5"
|
||||
ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && git log --oneline -5"
|
||||
|
||||
# Check for uncommitted changes
|
||||
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && git status"
|
||||
ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && git status"
|
||||
|
||||
# Check remote sync
|
||||
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && git remote -v && git fetch && git status"
|
||||
ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && git remote -v && git fetch && git status"
|
||||
|
||||
# Verify file permissions
|
||||
ssh maciejpi@10.22.68.249 "ls -la /var/www/nordabiznes/ | head -10"
|
||||
ssh maciejpi@57.128.200.27 "ls -la /var/www/nordabiznes/ | head -10"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@ -157,12 +157,12 @@ graph TB
|
||||
NPM[NPM Reverse Proxy<br/>:443 SSL/TLS<br/>⚠️ Forwards to :5000]
|
||||
end
|
||||
|
||||
subgraph "Application Zone - 10.22.68.249"
|
||||
subgraph "Application Zone - 57.128.200.27"
|
||||
Flask[Flask/Gunicorn<br/>:5000 HTTP<br/>90+ routes, 7 services]
|
||||
Scripts[Background Scripts<br/>SEO, Social, News]
|
||||
end
|
||||
|
||||
subgraph "Data Zone - 10.22.68.249"
|
||||
subgraph "Data Zone - 57.128.200.27"
|
||||
PostgreSQL[(PostgreSQL<br/>:5432<br/>36 tables, 11 domains)]
|
||||
end
|
||||
|
||||
@ -252,7 +252,7 @@ graph TB
|
||||
> ⚠️ **Database Access**
|
||||
>
|
||||
> PostgreSQL only accepts connections from **localhost (127.0.0.1)** for security.
|
||||
> All scripts must connect via localhost, not the external IP 10.22.68.249.
|
||||
> All scripts must connect via localhost, not the external IP 57.128.200.27.
|
||||
>
|
||||
> **See:** [Critical Configurations](08-critical-configurations.md#database-configuration)
|
||||
|
||||
@ -331,8 +331,8 @@ erDiagram
|
||||
```mermaid
|
||||
graph LR
|
||||
Internet((Internet)) -->|HTTPS| NPM[NPM Proxy<br/>10.22.68.250:443]
|
||||
NPM -->|Port 5000| Flask[Flask App<br/>10.22.68.249:5000]
|
||||
Flask -->|Port 5432| DB[(PostgreSQL<br/>10.22.68.249:5432)]
|
||||
NPM -->|Port 5000| Flask[Flask App<br/>57.128.200.27:5000]
|
||||
Flask -->|Port 5432| DB[(PostgreSQL<br/>57.128.200.27:5432)]
|
||||
```
|
||||
|
||||
#### How to View and Edit Diagrams
|
||||
@ -390,7 +390,7 @@ Create an HTML file with Mermaid script:
|
||||
|
||||
**2. Naming Conventions**
|
||||
- Use descriptive node labels: `[Flask App]` not `[App]`
|
||||
- Include IPs/ports in infrastructure diagrams: `[Server<br/>10.22.68.249:5000]`
|
||||
- Include IPs/ports in infrastructure diagrams: `[Server<br/>57.128.200.27:5000]`
|
||||
- Use consistent colors/styling across related diagrams
|
||||
|
||||
**3. Comments and Documentation**
|
||||
@ -443,8 +443,8 @@ graph LR
|
||||
```mermaid
|
||||
%% Use <br/> for line breaks in labels
|
||||
graph TD
|
||||
A[Flask App<br/>10.22.68.249<br/>Port 5000]
|
||||
B[PostgreSQL<br/>10.22.68.249<br/>Port 5432]
|
||||
A[Flask App<br/>57.128.200.27<br/>Port 5000]
|
||||
B[PostgreSQL<br/>57.128.200.27<br/>Port 5432]
|
||||
A --> B
|
||||
```
|
||||
|
||||
@ -586,14 +586,14 @@ sudo journalctl -u nordabiznes -f
|
||||
sudo systemctl status postgresql
|
||||
|
||||
# Deploy updates (after git push)
|
||||
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes"
|
||||
```
|
||||
|
||||
### Key File Locations
|
||||
- **Application:** `/var/www/nordabiznes/`
|
||||
- **Environment:** `/var/www/nordabiznes/.env`
|
||||
- **Logs:** `/var/log/nordabiznes/` (check systemd journal)
|
||||
- **Database:** PostgreSQL on 10.22.68.249:5432
|
||||
- **Database:** PostgreSQL on 57.128.200.27:5432
|
||||
- **Backups:** Proxmox Backup Server (VM snapshots)
|
||||
|
||||
## Quick Links
|
||||
|
||||
@ -865,7 +865,7 @@ PageSpeed API quota remaining: 24,950
|
||||
|
||||
```bash
|
||||
# Connect to server
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
|
||||
# Navigate to application directory
|
||||
cd /var/www/nordabiznes
|
||||
@ -895,7 +895,7 @@ Scripts in `scripts/` must use **localhost (127.0.0.1)** for PostgreSQL:
|
||||
DATABASE_URL = 'postgresql://nordabiz_app:NordaBiz2025Secure@127.0.0.1:5432/nordabiz'
|
||||
|
||||
# WRONG (PostgreSQL doesn't accept external connections):
|
||||
DATABASE_URL = 'postgresql://nordabiz_app:NordaBiz2025Secure@10.22.68.249:5432/nordabiz'
|
||||
DATABASE_URL = 'postgresql://nordabiz_app:NordaBiz2025Secure@57.128.200.27:5432/nordabiz'
|
||||
```
|
||||
|
||||
### 7.5 Cron Job (Automated Audits)
|
||||
|
||||
@ -20,7 +20,7 @@ This document describes the **complete HTTP request flow** for the Norda Biznes
|
||||
**Key Infrastructure:**
|
||||
- **Public Entry:** 85.237.177.83:443 (Fortigate NAT)
|
||||
- **Reverse Proxy:** NPM on 10.22.68.250:443 (SSL termination)
|
||||
- **Backend Application:** Flask/Gunicorn on 10.22.68.249:5000
|
||||
- **Backend Application:** Flask/Gunicorn on 57.128.200.27:5000
|
||||
- **Protocol Flow:** HTTPS → NPM → HTTP → Flask → HTTP → NPM → HTTPS
|
||||
|
||||
**⚠️ CRITICAL CONFIGURATION:**
|
||||
@ -47,7 +47,7 @@ sequenceDiagram
|
||||
participant Browser
|
||||
participant Fortigate as 🛡️ Fortigate Firewall<br/>85.237.177.83
|
||||
participant NPM as 🔒 NPM Reverse Proxy<br/>10.22.68.250:443
|
||||
participant Flask as 🌐 Flask/Gunicorn<br/>10.22.68.249:5000
|
||||
participant Flask as 🌐 Flask/Gunicorn<br/>57.128.200.27:5000
|
||||
participant DB as 💾 PostgreSQL<br/>localhost:5432
|
||||
|
||||
Note over User,DB: SUCCESSFUL REQUEST FLOW
|
||||
@ -87,8 +87,8 @@ sequenceDiagram
|
||||
actor User
|
||||
participant Browser
|
||||
participant NPM as 🔒 NPM Reverse Proxy<br/>10.22.68.250:443
|
||||
participant NginxSys as ⚠️ Nginx System<br/>10.22.68.249:80
|
||||
participant Flask as 🌐 Flask/Gunicorn<br/>10.22.68.249:5000
|
||||
participant NginxSys as ⚠️ Nginx System<br/>57.128.200.27:80
|
||||
participant Flask as 🌐 Flask/Gunicorn<br/>57.128.200.27:5000
|
||||
|
||||
Note over User,Flask: FAILED REQUEST FLOW (REDIRECT LOOP)
|
||||
|
||||
@ -201,7 +201,7 @@ Firewall: ALLOW from any to 85.237.177.83:443 (state: NEW,ESTABLISHED)
|
||||
-- Result:
|
||||
domain_names: ["nordabiznes.pl", "www.nordabiznes.pl"]
|
||||
forward_scheme: "http"
|
||||
forward_host: "10.22.68.249"
|
||||
forward_host: "57.128.200.27"
|
||||
forward_port: 5000 ← CRITICAL!
|
||||
ssl_forced: true
|
||||
certificate_id: 27
|
||||
@ -209,8 +209,8 @@ Firewall: ALLOW from any to 85.237.177.83:443 (state: NEW,ESTABLISHED)
|
||||
|
||||
5. **⚠️ CRITICAL ROUTING DECISION:**
|
||||
```
|
||||
✓ CORRECT: Forward to http://10.22.68.249:5000
|
||||
❌ WRONG: Forward to http://10.22.68.249:80 (causes redirect loop!)
|
||||
✓ CORRECT: Forward to http://57.128.200.27:5000
|
||||
❌ WRONG: Forward to http://57.128.200.27:80 (causes redirect loop!)
|
||||
```
|
||||
|
||||
6. **Forward to Backend (HTTP, unencrypted):**
|
||||
@ -230,7 +230,7 @@ Firewall: ALLOW from any to 85.237.177.83:443 (state: NEW,ESTABLISHED)
|
||||
|-----------|-------|-------|
|
||||
| Domain Names | nordabiznes.pl, www.nordabiznes.pl | Primary + www alias |
|
||||
| Forward Scheme | http | NPM→Backend uses HTTP (secure internal network) |
|
||||
| Forward Host | 10.22.68.249 | NORDABIZ-01 backend server |
|
||||
| Forward Host | 57.128.200.27 | NORDABIZ-01 backend server |
|
||||
| **Forward Port** | **5000** | **Flask/Gunicorn port (CRITICAL!)** |
|
||||
| SSL Certificate | 27 (Let's Encrypt) | Auto-renewal enabled |
|
||||
| SSL Forced | Yes | Redirect HTTP→HTTPS |
|
||||
@ -247,7 +247,7 @@ ssh maciejpi@10.22.68.250 "docker exec nginx-proxy-manager_app_1 \
|
||||
\"SELECT id, domain_names, forward_host, forward_port FROM proxy_host WHERE id = 27;\""
|
||||
|
||||
# Expected output:
|
||||
# 27|["nordabiznes.pl","www.nordabiznes.pl"]|10.22.68.249|5000
|
||||
# 27|["nordabiznes.pl","www.nordabiznes.pl"]|57.128.200.27|5000
|
||||
```
|
||||
|
||||
---
|
||||
@ -255,7 +255,7 @@ ssh maciejpi@10.22.68.250 "docker exec nginx-proxy-manager_app_1 \
|
||||
### 2.3 Layer 3: Flask/Gunicorn Application (Request Processing)
|
||||
|
||||
**Server:** NORDABIZ-01 (VM 249)
|
||||
**IP:** 10.22.68.249
|
||||
**IP:** 57.128.200.27
|
||||
**Port:** 5000
|
||||
**Technology:** Gunicorn 20.1.0 + Flask 3.0
|
||||
|
||||
@ -398,11 +398,11 @@ ExecStart=/var/www/nordabiznes/venv/bin/gunicorn \
|
||||
**Verification Command:**
|
||||
```bash
|
||||
# Test Flask directly (from server)
|
||||
curl -I http://10.22.68.249:5000/health
|
||||
curl -I http://57.128.200.27:5000/health
|
||||
# Expected: HTTP/1.1 200 OK
|
||||
|
||||
# Check Gunicorn workers
|
||||
ssh maciejpi@10.22.68.249 "ps aux | grep gunicorn"
|
||||
ssh maciejpi@57.128.200.27 "ps aux | grep gunicorn"
|
||||
# Expected: 1 master + 4 worker processes
|
||||
```
|
||||
|
||||
@ -467,10 +467,10 @@ engine = create_engine(DATABASE_URL,
|
||||
**Verification:**
|
||||
```bash
|
||||
# Check PostgreSQL status
|
||||
ssh maciejpi@10.22.68.249 "sudo systemctl status postgresql"
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl status postgresql"
|
||||
|
||||
# Test connection (from server)
|
||||
ssh maciejpi@10.22.68.249 "psql -U nordabiz_app -h 127.0.0.1 -d nordabiz -c 'SELECT COUNT(*) FROM companies;'"
|
||||
ssh maciejpi@57.128.200.27 "psql -U nordabiz_app -h 127.0.0.1 -d nordabiz -c 'SELECT COUNT(*) FROM companies;'"
|
||||
# Expected: 80
|
||||
```
|
||||
|
||||
@ -482,7 +482,7 @@ ssh maciejpi@10.22.68.249 "psql -U nordabiz_app -h 127.0.0.1 -d nordabiz -c 'SEL
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Flask as 🌐 Flask/Gunicorn<br/>10.22.68.249:5000
|
||||
participant Flask as 🌐 Flask/Gunicorn<br/>57.128.200.27:5000
|
||||
participant NPM as 🔒 NPM Reverse Proxy<br/>10.22.68.250:443
|
||||
participant Fortigate as 🛡️ Fortigate Firewall<br/>85.237.177.83
|
||||
participant Browser
|
||||
@ -573,14 +573,14 @@ x-request-id: abc123def456
|
||||
│ Certificate: Let's Encrypt (nordabiznes.pl) │
|
||||
│ │
|
||||
│ ⚠️ CRITICAL ROUTING DECISION: │
|
||||
│ ✓ Forward to: http://10.22.68.249:5000 (CORRECT) │
|
||||
│ ❌ DO NOT use: http://10.22.68.249:80 (WRONG!) │
|
||||
│ ✓ Forward to: http://57.128.200.27:5000 (CORRECT) │
|
||||
│ ❌ DO NOT use: http://57.128.200.27:80 (WRONG!) │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│ HTTP (Port 5000) ✓
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ FLASK/GUNICORN (NORDABIZ-01) │
|
||||
│ IP: 10.22.68.249:5000 │
|
||||
│ IP: 57.128.200.27:5000 │
|
||||
│ Binding: 0.0.0.0:5000 │
|
||||
│ Workers: 4 (Gunicorn) │
|
||||
│ Function: Application logic, template rendering │
|
||||
@ -858,10 +858,10 @@ ssh maciejpi@10.22.68.250 "docker exec nginx-proxy-manager_app_1 \
|
||||
# If shows: 80 ← PROBLEM!
|
||||
|
||||
# 2. Test direct backend access
|
||||
curl -I http://10.22.68.249:80/
|
||||
curl -I http://57.128.200.27:80/
|
||||
# If returns: HTTP 301 → Problem confirmed
|
||||
|
||||
curl -I http://10.22.68.249:5000/
|
||||
curl -I http://57.128.200.27:5000/
|
||||
# Should return: HTTP 200 OK
|
||||
```
|
||||
|
||||
@ -893,25 +893,25 @@ curl -I https://nordabiznes.pl/health
|
||||
**Diagnosis:**
|
||||
```bash
|
||||
# 1. Check if Gunicorn is running
|
||||
ssh maciejpi@10.22.68.249 "sudo systemctl status nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl status nordabiznes"
|
||||
# Expected: Active (running)
|
||||
|
||||
# 2. Check if port 5000 is listening
|
||||
ssh maciejpi@10.22.68.249 "sudo netstat -tlnp | grep 5000"
|
||||
ssh maciejpi@57.128.200.27 "sudo netstat -tlnp | grep 5000"
|
||||
# Expected: 0.0.0.0:5000 ... gunicorn
|
||||
|
||||
# 3. Test direct connection
|
||||
curl -I http://10.22.68.249:5000/health
|
||||
curl -I http://57.128.200.27:5000/health
|
||||
# Expected: HTTP 200 OK
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Restart Gunicorn
|
||||
ssh maciejpi@10.22.68.249 "sudo systemctl restart nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl restart nordabiznes"
|
||||
|
||||
# Check logs
|
||||
ssh maciejpi@10.22.68.249 "sudo journalctl -u nordabiznes -n 50"
|
||||
ssh maciejpi@57.128.200.27 "sudo journalctl -u nordabiznes -n 50"
|
||||
```
|
||||
|
||||
---
|
||||
@ -930,14 +930,14 @@ ssh maciejpi@10.22.68.249 "sudo journalctl -u nordabiznes -n 50"
|
||||
**Diagnosis:**
|
||||
```bash
|
||||
# 1. Check Gunicorn worker status
|
||||
ssh maciejpi@10.22.68.249 "ps aux | grep gunicorn"
|
||||
ssh maciejpi@57.128.200.27 "ps aux | grep gunicorn"
|
||||
# Look for workers in state 'R' (running) vs 'S' (sleeping)
|
||||
|
||||
# 2. Check application logs
|
||||
ssh maciejpi@10.22.68.249 "tail -f /var/log/nordabiznes/error.log"
|
||||
ssh maciejpi@57.128.200.27 "tail -f /var/log/nordabiznes/error.log"
|
||||
|
||||
# 3. Check database connections
|
||||
ssh maciejpi@10.22.68.249 "sudo -u postgres psql -c \
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres psql -c \
|
||||
\"SELECT count(*) FROM pg_stat_activity WHERE datname='nordabiz';\""
|
||||
```
|
||||
|
||||
@ -985,7 +985,7 @@ curl -I https://nordabiznes.pl/health
|
||||
# Expected: HTTP/2 200 OK
|
||||
|
||||
# Internal access test (from INPI network)
|
||||
curl -I http://10.22.68.249:5000/health
|
||||
curl -I http://57.128.200.27:5000/health
|
||||
# Expected: HTTP/1.1 200 OK
|
||||
```
|
||||
|
||||
@ -1001,36 +1001,36 @@ ssh maciejpi@10.22.68.250 "docker exec nginx-proxy-manager_app_1 \
|
||||
**Application Status:**
|
||||
```bash
|
||||
# Service status
|
||||
ssh maciejpi@10.22.68.249 "sudo systemctl status nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl status nordabiznes"
|
||||
|
||||
# Worker processes
|
||||
ssh maciejpi@10.22.68.249 "ps aux | grep gunicorn | grep -v grep"
|
||||
ssh maciejpi@57.128.200.27 "ps aux | grep gunicorn | grep -v grep"
|
||||
|
||||
# Recent logs
|
||||
ssh maciejpi@10.22.68.249 "sudo journalctl -u nordabiznes -n 20 --no-pager"
|
||||
ssh maciejpi@57.128.200.27 "sudo journalctl -u nordabiznes -n 20 --no-pager"
|
||||
```
|
||||
|
||||
**Database Status:**
|
||||
```bash
|
||||
# PostgreSQL status
|
||||
ssh maciejpi@10.22.68.249 "sudo systemctl status postgresql"
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl status postgresql"
|
||||
|
||||
# Connection count
|
||||
ssh maciejpi@10.22.68.249 "sudo -u postgres psql -c \
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres psql -c \
|
||||
\"SELECT count(*) FROM pg_stat_activity WHERE datname='nordabiz';\""
|
||||
|
||||
# Database size
|
||||
ssh maciejpi@10.22.68.249 "sudo -u postgres psql -c \
|
||||
ssh maciejpi@57.128.200.27 "sudo -u postgres psql -c \
|
||||
\"SELECT pg_size_pretty(pg_database_size('nordabiz'));\""
|
||||
```
|
||||
|
||||
**Network Connectivity:**
|
||||
```bash
|
||||
# Test NPM → Flask connectivity
|
||||
ssh maciejpi@10.22.68.250 "curl -I http://10.22.68.249:5000/health"
|
||||
ssh maciejpi@10.22.68.250 "curl -I http://57.128.200.27:5000/health"
|
||||
|
||||
# Test Flask → Database connectivity
|
||||
ssh maciejpi@10.22.68.249 "psql -U nordabiz_app -h 127.0.0.1 \
|
||||
ssh maciejpi@57.128.200.27 "psql -U nordabiz_app -h 127.0.0.1 \
|
||||
-d nordabiz -c 'SELECT 1;'"
|
||||
```
|
||||
|
||||
@ -1050,7 +1050,7 @@ ssh maciejpi@10.22.68.249 "psql -U nordabiz_app -h 127.0.0.1 \
|
||||
"id": 27,
|
||||
"domain_names": ["nordabiznes.pl", "www.nordabiznes.pl"],
|
||||
"forward_scheme": "http",
|
||||
"forward_host": "10.22.68.249",
|
||||
"forward_host": "57.128.200.27",
|
||||
"forward_port": 5000,
|
||||
"access_list_id": 0,
|
||||
"certificate_id": 27,
|
||||
@ -1075,7 +1075,7 @@ NPM_URL = "http://10.22.68.250:81/api"
|
||||
data = {
|
||||
"domain_names": ["nordabiznes.pl", "www.nordabiznes.pl"],
|
||||
"forward_scheme": "http",
|
||||
"forward_host": "10.22.68.249",
|
||||
"forward_host": "57.128.200.27",
|
||||
"forward_port": 5000, # CRITICAL!
|
||||
"certificate_id": 27,
|
||||
"ssl_forced": True,
|
||||
@ -1186,19 +1186,19 @@ ssh maciejpi@10.22.68.250 "docker logs -f nginx-proxy-manager_app_1"
|
||||
**Gunicorn Logs:**
|
||||
```bash
|
||||
# Application logs (systemd journal)
|
||||
ssh maciejpi@10.22.68.249 "sudo journalctl -u nordabiznes -f"
|
||||
ssh maciejpi@57.128.200.27 "sudo journalctl -u nordabiznes -f"
|
||||
|
||||
# Access logs (file-based)
|
||||
ssh maciejpi@10.22.68.249 "tail -f /var/log/nordabiznes/access.log"
|
||||
ssh maciejpi@57.128.200.27 "tail -f /var/log/nordabiznes/access.log"
|
||||
|
||||
# Error logs
|
||||
ssh maciejpi@10.22.68.249 "tail -f /var/log/nordabiznes/error.log"
|
||||
ssh maciejpi@57.128.200.27 "tail -f /var/log/nordabiznes/error.log"
|
||||
```
|
||||
|
||||
**PostgreSQL Logs:**
|
||||
```bash
|
||||
# Query logs (if enabled)
|
||||
ssh maciejpi@10.22.68.249 "sudo tail -f /var/log/postgresql/postgresql-14-main.log"
|
||||
ssh maciejpi@57.128.200.27 "sudo tail -f /var/log/postgresql/postgresql-14-main.log"
|
||||
```
|
||||
|
||||
### 10.2 Key Metrics to Monitor
|
||||
|
||||
@ -61,7 +61,7 @@ Internet → FortiGate (NAT/VPN) → [OPNsense Bridge VM 155] → Serwery INPI
|
||||
- **Scenariusze:** HTTP brute-force, path traversal, scanner detection, bad user agents
|
||||
- **Instalacja:** Docker sidecar obok NPM
|
||||
|
||||
#### Agent na NordaBiz (VM 249, 10.22.68.249)
|
||||
#### Agent na NordaBiz (VM 249, 57.128.200.27)
|
||||
- **Parser:** Flask/Gunicorn access logs
|
||||
- **Scenariusze:** Login brute-force, honeypot triggers, rate limit abuse
|
||||
- **Instalacja:** Pakiet systemowy
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
|
||||
## Task 1: Install CrowdSec on NordaBiz (VM 249)
|
||||
|
||||
**Target:** 10.22.68.249 (NORDABIZ-01)
|
||||
**Target:** 57.128.200.27 (NORDABIZ-01)
|
||||
|
||||
CrowdSec agent na serwerze NordaBiz — parsuje logi Flask/Gunicorn, wykrywa brute-force i honeypot.
|
||||
|
||||
@ -25,14 +25,14 @@ CrowdSec agent na serwerze NordaBiz — parsuje logi Flask/Gunicorn, wykrywa bru
|
||||
**Step 1: Install CrowdSec repository and package**
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "curl -s https://install.crowdsec.net | sudo sh"
|
||||
ssh maciejpi@10.22.68.249 "sudo apt install -y crowdsec"
|
||||
ssh maciejpi@57.128.200.27 "curl -s https://install.crowdsec.net | sudo sh"
|
||||
ssh maciejpi@57.128.200.27 "sudo apt install -y crowdsec"
|
||||
```
|
||||
|
||||
**Step 2: Verify CrowdSec is running**
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "sudo cscli version && sudo systemctl status crowdsec --no-pager"
|
||||
ssh maciejpi@57.128.200.27 "sudo cscli version && sudo systemctl status crowdsec --no-pager"
|
||||
```
|
||||
|
||||
Expected: CrowdSec running, version displayed.
|
||||
@ -42,14 +42,14 @@ Expected: CrowdSec running, version displayed.
|
||||
NordaBiz runs behind nginx on port 80, Flask/Gunicorn on port 5000.
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "sudo cscli collections install crowdsecurity/nginx"
|
||||
ssh maciejpi@10.22.68.249 "sudo cscli collections install crowdsecurity/base-http-scenarios"
|
||||
ssh maciejpi@57.128.200.27 "sudo cscli collections install crowdsecurity/nginx"
|
||||
ssh maciejpi@57.128.200.27 "sudo cscli collections install crowdsecurity/base-http-scenarios"
|
||||
```
|
||||
|
||||
**Step 4: Configure acquisition for nginx access logs**
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "sudo tee /etc/crowdsec/acquis.d/nordabiz.yaml << 'EOF'
|
||||
ssh maciejpi@57.128.200.27 "sudo tee /etc/crowdsec/acquis.d/nordabiz.yaml << 'EOF'
|
||||
filenames:
|
||||
- /var/log/nginx/access.log
|
||||
labels:
|
||||
@ -60,7 +60,7 @@ EOF"
|
||||
**Step 5: Configure acquisition for NordaBiz security log (honeypot)**
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "sudo tee /etc/crowdsec/acquis.d/nordabiz-security.yaml << 'EOF'
|
||||
ssh maciejpi@57.128.200.27 "sudo tee /etc/crowdsec/acquis.d/nordabiz-security.yaml << 'EOF'
|
||||
filenames:
|
||||
- /var/log/nordabiznes/security.log
|
||||
labels:
|
||||
@ -71,8 +71,8 @@ EOF"
|
||||
**Step 6: Restart CrowdSec and verify**
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "sudo systemctl restart crowdsec"
|
||||
ssh maciejpi@10.22.68.249 "sudo cscli metrics"
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl restart crowdsec"
|
||||
ssh maciejpi@57.128.200.27 "sudo cscli metrics"
|
||||
```
|
||||
|
||||
Expected: Metrics show nginx parser processing lines from access.log.
|
||||
@ -80,8 +80,8 @@ Expected: Metrics show nginx parser processing lines from access.log.
|
||||
**Step 7: Verify installed collections and scenarios**
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "sudo cscli collections list"
|
||||
ssh maciejpi@10.22.68.249 "sudo cscli scenarios list"
|
||||
ssh maciejpi@57.128.200.27 "sudo cscli collections list"
|
||||
ssh maciejpi@57.128.200.27 "sudo cscli scenarios list"
|
||||
```
|
||||
|
||||
Expected: crowdsecurity/nginx, crowdsecurity/base-http-scenarios, crowdsecurity/linux listed.
|
||||
@ -172,7 +172,7 @@ Navigate to https://app.crowdsec.net and create an account. Copy the enrollment
|
||||
**Step 2: Enroll NordaBiz instance**
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "sudo cscli console enroll --name NORDABIZ-01 --tags nordabiz --tags inpi YOUR-ENROLL-KEY"
|
||||
ssh maciejpi@57.128.200.27 "sudo cscli console enroll --name NORDABIZ-01 --tags nordabiz --tags inpi YOUR-ENROLL-KEY"
|
||||
```
|
||||
|
||||
**Step 3: Enroll NPM instance**
|
||||
@ -192,7 +192,7 @@ In Console → Blocklists tab → subscribe both engines to relevant blocklists.
|
||||
**Step 6: Verify blocklist decisions**
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "sudo cscli metrics show decisions"
|
||||
ssh maciejpi@57.128.200.27 "sudo cscli metrics show decisions"
|
||||
ssh maciejpi@10.22.68.250 "sudo cscli metrics show decisions"
|
||||
```
|
||||
|
||||
@ -643,7 +643,7 @@ ssh root@10.22.68.123 "qm set 119 --net0 virtio=CURRENT_MAC,bridge=vmbr1"
|
||||
# From management network
|
||||
curl -sI https://nordabiznes.pl/health | head -3
|
||||
ping -c 3 10.22.68.250
|
||||
ping -c 3 10.22.68.249
|
||||
ping -c 3 57.128.200.27
|
||||
```
|
||||
|
||||
Expected: All services reachable through bridge.
|
||||
@ -677,7 +677,7 @@ done
|
||||
# OPNsense WebGUI → Services → Intrusion Detection → Alerts
|
||||
|
||||
# 2. Check CrowdSec decisions on all three servers
|
||||
ssh maciejpi@10.22.68.249 "sudo cscli decisions list"
|
||||
ssh maciejpi@57.128.200.27 "sudo cscli decisions list"
|
||||
ssh maciejpi@10.22.68.250 "sudo cscli decisions list"
|
||||
ssh maciejpi@10.22.68.155 "sudo cscli decisions list"
|
||||
|
||||
|
||||
@ -1191,25 +1191,25 @@ Test messaging features manually on staging.
|
||||
- [ ] **Step 1: Deploy**
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && sudo -u www-data git pull"
|
||||
ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && sudo -u www-data git pull"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run migration**
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && /var/www/nordabiznes/venv/bin/python3 scripts/run_migration.py database/migrations/063_message_attachments.sql"
|
||||
ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && /var/www/nordabiznes/venv/bin/python3 scripts/run_migration.py database/migrations/063_message_attachments.sql"
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create upload directory**
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "mkdir -p /var/www/nordabiznes/static/uploads/messages && sudo chown -R maciejpi:maciejpi /var/www/nordabiznes/static/uploads/messages"
|
||||
ssh maciejpi@57.128.200.27 "mkdir -p /var/www/nordabiznes/static/uploads/messages && sudo chown -R maciejpi:maciejpi /var/www/nordabiznes/static/uploads/messages"
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Restart and verify**
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "sudo systemctl restart nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl restart nordabiznes"
|
||||
curl -sI https://nordabiznes.pl/health | head -3
|
||||
```
|
||||
|
||||
|
||||
@ -1139,6 +1139,6 @@ Then manually test `/pej`, `/pej/local-content`, `/pej/aktualnosci` on staging.
|
||||
- [ ] **Step 4: Deploy to production (AFTER staging verification)**
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes"
|
||||
curl -sI https://nordabiznes.pl/health | head -3
|
||||
```
|
||||
|
||||
1119
docs/superpowers/plans/2026-03-27-messaging-redesign.md
Normal file
1119
docs/superpowers/plans/2026-03-27-messaging-redesign.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -257,7 +257,7 @@ Type "Co wiesz o mnie?" — verify AI lists your profile data.
|
||||
- [ ] **Step 4: Deploy to production**
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes"
|
||||
curl -sI https://nordabiznes.pl/health | head -3
|
||||
```
|
||||
|
||||
@ -918,7 +918,7 @@ ssh maciejpi@10.22.68.248 "journalctl -u nordabiznes -n 30 --no-pager | grep 'Ro
|
||||
- [ ] **Step 3: Deploy to production**
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes"
|
||||
curl -sI https://nordabiznes.pl/health | head -3
|
||||
```
|
||||
|
||||
@ -1343,7 +1343,7 @@ git commit -m "feat(nordagpt): streaming UI — word-by-word response with think
|
||||
SSE requires Nginx to NOT buffer the response. The streaming endpoint sets `X-Accel-Buffering: no` header. Verify NPM custom config allows this:
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "cat /etc/nginx/sites-enabled/nordabiznes.conf 2>/dev/null || echo 'Using NPM proxy'"
|
||||
ssh maciejpi@57.128.200.27 "cat /etc/nginx/sites-enabled/nordabiznes.conf 2>/dev/null || echo 'Using NPM proxy'"
|
||||
```
|
||||
|
||||
If using NPM, the `X-Accel-Buffering: no` header should be sufficient. If not, add to NPM custom Nginx config for nordabiznes.pl:
|
||||
@ -1364,7 +1364,7 @@ Test on staging: open chat, send message, verify text appears word-by-word.
|
||||
- [ ] **Step 3: Deploy to production**
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes"
|
||||
curl -sI https://nordabiznes.pl/health | head -3
|
||||
```
|
||||
|
||||
@ -1901,10 +1901,10 @@ ssh maciejpi@10.22.68.248 "sudo systemctl restart nordabiznes"
|
||||
- [ ] **Step 4: Deploy to production**
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && sudo -u www-data git pull"
|
||||
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && DATABASE_URL=\$(grep DATABASE_URL .env | cut -d'=' -f2) /var/www/nordabiznes/venv/bin/python3 scripts/run_migration.py database/migrations/092_ai_user_memory.sql"
|
||||
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && DATABASE_URL=\$(grep DATABASE_URL .env | cut -d'=' -f2) /var/www/nordabiznes/venv/bin/python3 scripts/run_migration.py database/migrations/093_ai_conversation_summary.sql"
|
||||
ssh maciejpi@10.22.68.249 "sudo systemctl restart nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && sudo -u www-data git pull"
|
||||
ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && DATABASE_URL=\$(grep DATABASE_URL .env | cut -d'=' -f2) /var/www/nordabiznes/venv/bin/python3 scripts/run_migration.py database/migrations/092_ai_user_memory.sql"
|
||||
ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && DATABASE_URL=\$(grep DATABASE_URL .env | cut -d'=' -f2) /var/www/nordabiznes/venv/bin/python3 scripts/run_migration.py database/migrations/093_ai_conversation_summary.sql"
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl restart nordabiznes"
|
||||
curl -sI https://nordabiznes.pl/health | head -3
|
||||
```
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-03-31-event-guests-design.md`
|
||||
|
||||
**Deployment:** Staging first (`10.22.68.248`), user tests, then production (`10.22.68.249`).
|
||||
**Deployment:** Staging first (`10.22.68.248`), user tests, then production (`57.128.200.27`).
|
||||
|
||||
---
|
||||
|
||||
@ -749,19 +749,19 @@ Expected: `HTTP/2 200`
|
||||
- [ ] **Step 1: Deploy to production**
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && sudo -u www-data git pull"
|
||||
ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && sudo -u www-data git pull"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run migration on production**
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && /var/www/nordabiznes/venv/bin/python3 scripts/run_migration.py database/migrations/096_event_guests.sql"
|
||||
ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && /var/www/nordabiznes/venv/bin/python3 scripts/run_migration.py database/migrations/096_event_guests.sql"
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Reload service**
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249 "sudo systemctl reload nordabiznes"
|
||||
ssh maciejpi@57.128.200.27 "sudo systemctl reload nordabiznes"
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Verify production**
|
||||
|
||||
@ -17,7 +17,7 @@ Podejście B: UptimeRobot (zewnętrzny monitoring) + wewnętrzny health logger z
|
||||
## Architektura
|
||||
|
||||
```
|
||||
UptimeRobot.com (free) NORDABIZ-01 (10.22.68.249)
|
||||
UptimeRobot.com (free) NORDABIZ-01 (57.128.200.27)
|
||||
│ sprawdza co 5 min │ wewnętrzny logger co 5 min
|
||||
│ HTTPS → nordabiznes.pl │ app/db/cpu/ram/disk → PostgreSQL
|
||||
│ │
|
||||
|
||||
189
docs/superpowers/specs/2026-03-27-messaging-redesign-design.md
Normal file
189
docs/superpowers/specs/2026-03-27-messaging-redesign-design.md
Normal file
@ -0,0 +1,189 @@
|
||||
# Messaging Redesign — Conversation-Based System
|
||||
|
||||
**Data:** 2026-03-27
|
||||
**Status:** Zaakceptowany
|
||||
**Zakres:** Przebudowa systemu wiadomości z email-like (Odebrane/Wysłane) na konwersacyjny (Messenger/WhatsApp)
|
||||
|
||||
## Decyzje architektoniczne
|
||||
|
||||
1. **Ujednolicony model** — 1:1 i grupy w jednym modelu `Conversation` + `Message`. Rozmowa 1:1 = konwersacja z 2 uczestnikami.
|
||||
2. **Real-time: SSE** — Server-Sent Events + Redis pub/sub. Jedno połączenie SSE per użytkownik.
|
||||
3. **Migracja danych** — istniejące wiadomości migrowane do nowego modelu. Stare tabele zostają jako backup.
|
||||
|
||||
## Model danych
|
||||
|
||||
### conversations
|
||||
| Kolumna | Typ | Opis |
|
||||
|---------|-----|------|
|
||||
| id | Serial PK | |
|
||||
| name | String(255), nullable | Null dla 1:1, nadana nazwa dla grup |
|
||||
| is_group | Boolean | False = 1:1, True = grupa |
|
||||
| owner_id | FK → users | Twórca |
|
||||
| created_at | DateTime | |
|
||||
| updated_at | DateTime | Aktualizowane przy każdej wiadomości |
|
||||
| last_message_id | FK → messages, nullable | Denormalizacja dla listy |
|
||||
|
||||
### conversation_members
|
||||
| Kolumna | Typ | Opis |
|
||||
|---------|-----|------|
|
||||
| conversation_id | FK → conversations, PK | |
|
||||
| user_id | FK → users, PK | |
|
||||
| role | String(20) | 'owner', 'member' |
|
||||
| last_read_at | DateTime | Read receipts |
|
||||
| is_muted | Boolean | Wyciszenie email + push |
|
||||
| is_archived | Boolean | Ukrycie z listy |
|
||||
| joined_at | DateTime | |
|
||||
| added_by_id | FK → users, nullable | |
|
||||
|
||||
### messages
|
||||
| Kolumna | Typ | Opis |
|
||||
|---------|-----|------|
|
||||
| id | Serial PK | |
|
||||
| conversation_id | FK → conversations | |
|
||||
| sender_id | FK → users | |
|
||||
| content | Text | HTML (Quill) |
|
||||
| reply_to_id | FK → messages, nullable | Cytowanie |
|
||||
| edited_at | DateTime, nullable | |
|
||||
| is_deleted | Boolean | Soft delete |
|
||||
| link_preview | JSONB, nullable | {url, title, description, image} |
|
||||
| created_at | DateTime | |
|
||||
|
||||
### message_reactions
|
||||
| Kolumna | Typ | Opis |
|
||||
|---------|-----|------|
|
||||
| id | Serial PK | |
|
||||
| message_id | FK → messages | |
|
||||
| user_id | FK → users | |
|
||||
| emoji | String(10) | |
|
||||
| created_at | DateTime | |
|
||||
| UNIQUE | (message_id, user_id, emoji) | |
|
||||
|
||||
### message_pins
|
||||
| Kolumna | Typ | Opis |
|
||||
|---------|-----|------|
|
||||
| id | Serial PK | |
|
||||
| conversation_id | FK → conversations | |
|
||||
| message_id | FK → messages | |
|
||||
| pinned_by_id | FK → users | |
|
||||
| created_at | DateTime | |
|
||||
|
||||
### message_attachments
|
||||
Istniejąca tabela. Nowa kolumna `new_message_id` FK → messages, nullable.
|
||||
|
||||
## SSE Real-time
|
||||
|
||||
### Endpoint
|
||||
`GET /api/messages/stream` — jedno połączenie per użytkownik.
|
||||
|
||||
### Zdarzenia
|
||||
| Event | Dane | Kiedy |
|
||||
|-------|------|-------|
|
||||
| new_message | conversation_id, message JSON | Nowa wiadomość |
|
||||
| message_read | conversation_id, user_id, read_at | Przeczytano |
|
||||
| typing | conversation_id, user_id, user_name | Ktoś pisze (TTL 3s) |
|
||||
| reaction | message_id, user_id, emoji, action | Reakcja |
|
||||
| message_edited | message_id, new_content, edited_at | Edycja |
|
||||
| message_deleted | message_id, conversation_id | Usunięcie |
|
||||
| message_pinned | message_id, conversation_id, pinned_by | Przypięcie |
|
||||
| presence | user_id, status, last_seen | Online/offline |
|
||||
|
||||
### Infrastruktura
|
||||
- Redis pub/sub do rozgłaszania między workerami Gunicorn
|
||||
- Online status: Redis SETEX z TTL 60s, heartbeat co 30s
|
||||
- Typing: POST /api/conversations/<id>/typing → Redis publish, TTL 3s
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Konwersacje
|
||||
| Method | URL | Opis |
|
||||
|--------|-----|------|
|
||||
| GET | /wiadomosci | Widok konwersacyjny (HTML) |
|
||||
| GET | /api/conversations | Lista konwersacji JSON |
|
||||
| POST | /api/conversations | Nowa (deduplikacja 1:1) |
|
||||
| GET | /api/conversations/<id> | Szczegóły + członkowie |
|
||||
| PATCH | /api/conversations/<id> | Edytuj nazwę/opis |
|
||||
| DELETE | /api/conversations/<id> | Usuń (owner) |
|
||||
| POST | /api/conversations/<id>/members | Dodaj członka |
|
||||
| DELETE | /api/conversations/<id>/members/<uid> | Usuń członka |
|
||||
| PATCH | /api/conversations/<id>/settings | Mute/archive |
|
||||
|
||||
### Wiadomości
|
||||
| Method | URL | Opis |
|
||||
|--------|-----|------|
|
||||
| GET | /api/conversations/<id>/messages | Paginacja cursor-based |
|
||||
| POST | /api/conversations/<id>/messages | Wyślij |
|
||||
| PATCH | /api/messages/<id> | Edytuj (swoje, max 24h) |
|
||||
| DELETE | /api/messages/<id> | Soft delete (swoje) |
|
||||
| POST | /api/messages/<id>/forward | Przekaż |
|
||||
| POST | /api/conversations/<id>/read | Oznacz przeczytane |
|
||||
| POST | /api/conversations/<id>/typing | Typing indicator |
|
||||
|
||||
### Reakcje i przypięcia
|
||||
| Method | URL | Opis |
|
||||
|--------|-----|------|
|
||||
| POST | /api/messages/<id>/reactions | Dodaj |
|
||||
| DELETE | /api/messages/<id>/reactions/<emoji> | Usuń |
|
||||
| POST | /api/messages/<id>/pin | Przypnij |
|
||||
| DELETE | /api/messages/<id>/pin | Odepnij |
|
||||
| GET | /api/conversations/<id>/pins | Lista przypiętych |
|
||||
|
||||
### Inne
|
||||
| Method | URL | Opis |
|
||||
|--------|-----|------|
|
||||
| GET | /api/messages/stream | SSE |
|
||||
| GET | /api/users/presence | Online status (batch) |
|
||||
| POST | /api/messages/upload | Upload pliku |
|
||||
|
||||
## Frontend
|
||||
|
||||
### Desktop
|
||||
- Lewy panel (380px): lista konwersacji posortowana po updated_at
|
||||
- Prawy panel: nagłówek (avatar, imię, status, typing) + wiadomości (bąbelki) + input (Quill)
|
||||
|
||||
### Wiadomości
|
||||
- Bąbelki: moje (niebieskie, prawo) / cudze (szare, lewo)
|
||||
- Separatory dat
|
||||
- Reply-to: cytat nad odpowiedzią
|
||||
- Edytowane: etykieta "(edytowano)"
|
||||
- Usunięte: "Wiadomość usunięta"
|
||||
- Załączniki inline (obrazy jako podgląd, pliki jako pill)
|
||||
- Reakcje: pill badges pod bąbelkiem
|
||||
- Link preview: karta z tytułem + opisem
|
||||
- Read receipts 1:1: ptaszki (wysłano/doręczono/przeczytano), hover → timestampy
|
||||
- Read receipts grupa: awatary (max 4 + "+N")
|
||||
|
||||
### Menu kontekstowe (hover/long-press)
|
||||
Odpowiedz, Reaguj (6 emoji), Przekaż, Przypnij, Edytuj, Usuń
|
||||
|
||||
### Mobile (< 768px)
|
||||
Lista LUB chat (nie oba). Przycisk "Wróć". Menu kontekstowe jako bottom sheet.
|
||||
|
||||
## Email notifications
|
||||
|
||||
```
|
||||
if member.is_muted → nie wysyłaj
|
||||
elif not user.notify_email_messages → nie wysyłaj
|
||||
else → wysyłaj
|
||||
```
|
||||
|
||||
Wyciszona konwersacja: ikona 🔇 na liście.
|
||||
|
||||
## Link preview
|
||||
|
||||
- Backend wykrywa URL, pobiera stronę (timeout 3s), parsuje og:title/og:description/og:image
|
||||
- Fallback: <title> + <meta description>
|
||||
- Tylko pierwszy URL, brak preview dla wewnętrznych linków
|
||||
- Zapisane w messages.link_preview (JSONB)
|
||||
|
||||
## Migracja danych
|
||||
|
||||
1. Prywatne wiadomości: grupowanie po parach sender/recipient → conversation (is_group=False)
|
||||
2. Grupy: message_group → conversation (is_group=True)
|
||||
3. Załączniki: nowy FK new_message_id
|
||||
4. Walidacja: count before = count after, read receipts zachowane
|
||||
5. Stare tabele zostają jako backup
|
||||
|
||||
## Zależności infrastrukturalne
|
||||
|
||||
- Redis na VM produkcyjnej (pub/sub + presence cache)
|
||||
- Nginx: SSE wymaga wyłączenia buforowania (`proxy_buffering off`) dla /api/messages/stream
|
||||
@ -1,12 +1,11 @@
|
||||
# Zabbix Monitoring Setup - NORDABIZ-01
|
||||
# Zabbix Monitoring Setup - OVH VPS (inpi-vps-waw01)
|
||||
|
||||
## Informacje o serwerze
|
||||
|
||||
| Parametr | Wartosc |
|
||||
|----------|---------|
|
||||
| **Nazwa hosta** | NORDABIZ-01 |
|
||||
| **VM ID** | 249 |
|
||||
| **IP** | 10.22.68.249 |
|
||||
| **Nazwa hosta** | inpi-vps-waw01 |
|
||||
| **IP** | 57.128.200.27 |
|
||||
| **OS** | Ubuntu 22.04 LTS |
|
||||
| **Aplikacja** | Flask (NordaBiznes Partner) |
|
||||
| **Baza danych** | PostgreSQL 15 (localhost:5432) |
|
||||
@ -29,7 +28,7 @@
|
||||
Polacz sie z serwerem i sprawdz czy agent jest zainstalowany:
|
||||
|
||||
```bash
|
||||
ssh maciejpi@10.22.68.249
|
||||
ssh maciejpi@57.128.200.27
|
||||
|
||||
# Sprawdz status agenta (Zabbix Agent 2 - nowsza wersja)
|
||||
systemctl status zabbix-agent2
|
||||
@ -100,7 +99,7 @@ Server=10.22.68.126
|
||||
ServerActive=10.22.68.126
|
||||
|
||||
# Nazwa hosta - MUSI byc identyczna jak w Zabbix Server
|
||||
Hostname=NORDABIZ-01
|
||||
Hostname=inpi-vps-waw01
|
||||
|
||||
# Port nasluchiwania (domyslny)
|
||||
ListenPort=10050
|
||||
@ -165,7 +164,7 @@ sudo nano /etc/zabbix/zabbix_agent2.d/nordabiznes.conf
|
||||
```ini
|
||||
# ============================================
|
||||
# NordaBiznes Partner - Custom Zabbix Monitoring
|
||||
# Server: NORDABIZ-01 (10.22.68.249)
|
||||
# Server: OVH VPS inpi-vps-waw01 (57.128.200.27)
|
||||
# ============================================
|
||||
|
||||
# --- FLASK APPLICATION ---
|
||||
@ -283,11 +282,11 @@ zabbix_agent2 -t nordabiznes.disk_usage_mb
|
||||
|
||||
| Pole | Wartosc |
|
||||
|------|---------|
|
||||
| Host name | NORDABIZ-01 |
|
||||
| Visible name | NordaBiznes Partner (10.22.68.249) |
|
||||
| Host name | inpi-vps-waw01 |
|
||||
| Visible name | NordaBiznes Partner (57.128.200.27) |
|
||||
| Templates | Linux by Zabbix agent, PostgreSQL by Zabbix agent 2 |
|
||||
| Host groups | Linux servers, Web servers, Databases |
|
||||
| Interfaces | Agent: 10.22.68.249:10050 |
|
||||
| Interfaces | Agent: 57.128.200.27:10050 |
|
||||
|
||||
### 5.2 Utworzenie dedykowanego template'u
|
||||
|
||||
@ -319,14 +318,14 @@ Utworz nowy template dla NordaBiznes:
|
||||
|
||||
| Name | Expression | Severity |
|
||||
|------|------------|----------|
|
||||
| NordaBiznes app is down | last(/NORDABIZ-01/nordabiznes.health)=0 | High |
|
||||
| NordaBiznes service stopped | last(/NORDABIZ-01/nordabiznes.service_status)=0 | High |
|
||||
| PostgreSQL is down | last(/NORDABIZ-01/postgresql.status)=0 | Disaster |
|
||||
| High response time (>2s) | last(/NORDABIZ-01/nordabiznes.response_time)>2000 | Warning |
|
||||
| Low Gunicorn workers | last(/NORDABIZ-01/nordabiznes.workers)<2 | Warning |
|
||||
| High DB connections (>80) | last(/NORDABIZ-01/postgresql.connections)>80 | Warning |
|
||||
| High memory usage (>500MB) | last(/NORDABIZ-01/nordabiznes.memory_mb)>500 | Warning |
|
||||
| Disk usage >5GB | last(/NORDABIZ-01/nordabiznes.disk_usage_mb)>5120 | Warning |
|
||||
| NordaBiznes app is down | last(/inpi-vps-waw01/nordabiznes.health)=0 | High |
|
||||
| NordaBiznes service stopped | last(/inpi-vps-waw01/nordabiznes.service_status)=0 | High |
|
||||
| PostgreSQL is down | last(/inpi-vps-waw01/postgresql.status)=0 | Disaster |
|
||||
| High response time (>2s) | last(/inpi-vps-waw01/nordabiznes.response_time)>2000 | Warning |
|
||||
| Low Gunicorn workers | last(/inpi-vps-waw01/nordabiznes.workers)<2 | Warning |
|
||||
| High DB connections (>80) | last(/inpi-vps-waw01/postgresql.connections)>80 | Warning |
|
||||
| High memory usage (>500MB) | last(/inpi-vps-waw01/nordabiznes.memory_mb)>500 | Warning |
|
||||
| Disk usage >5GB | last(/inpi-vps-waw01/nordabiznes.disk_usage_mb)>5120 | Warning |
|
||||
|
||||
---
|
||||
|
||||
@ -336,20 +335,20 @@ Utworz nowy template dla NordaBiznes:
|
||||
|
||||
```bash
|
||||
# Na serwerze Zabbix (10.22.68.126)
|
||||
zabbix_get -s 10.22.68.249 -k agent.ping
|
||||
zabbix_get -s 57.128.200.27 -k agent.ping
|
||||
# Oczekiwany wynik: 1
|
||||
|
||||
zabbix_get -s 10.22.68.249 -k nordabiznes.health
|
||||
zabbix_get -s 57.128.200.27 -k nordabiznes.health
|
||||
# Oczekiwany wynik: 1
|
||||
|
||||
zabbix_get -s 10.22.68.249 -k postgresql.status
|
||||
zabbix_get -s 57.128.200.27 -k postgresql.status
|
||||
# Oczekiwany wynik: 1
|
||||
```
|
||||
|
||||
### 6.2 Test lokalnie na NORDABIZ-01
|
||||
### 6.2 Test lokalnie na inpi-vps-waw01
|
||||
|
||||
```bash
|
||||
# Na serwerze NORDABIZ-01
|
||||
# Na serwerze inpi-vps-waw01
|
||||
zabbix_agent2 -t agent.ping
|
||||
zabbix_agent2 -t nordabiznes.health
|
||||
zabbix_agent2 -t nordabiznes.service_status
|
||||
@ -384,7 +383,7 @@ Uzycie: `ssl.days_until_expiry[nordabiznes.pl]`
|
||||
4. **Graph: Memory Usage** - nordabiznes.memory_mb (trend)
|
||||
5. **Graph: DB Connections** - postgresql.connections (trend)
|
||||
6. **Counter: Chat Messages Today** - nordabiznes.chat_messages_today
|
||||
7. **Top Hosts: Active Problems** - filtry dla NORDABIZ-01
|
||||
7. **Top Hosts: Active Problems** - filtry dla inpi-vps-waw01
|
||||
|
||||
---
|
||||
|
||||
|
||||
827
mockups/messages_chat_view.html
Normal file
827
mockups/messages_chat_view.html
Normal file
@ -0,0 +1,827 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NordaBiznes — Wiadomości (mockup konwersacyjny)</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300..700;1,9..40,300..700&display=swap');
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
:root {
|
||||
--bg: #f5f6f8;
|
||||
--panel: #ffffff;
|
||||
--border: #e4e7ec;
|
||||
--text: #1a1d23;
|
||||
--text-secondary: #6b7280;
|
||||
--text-muted: #9ca3af;
|
||||
--accent: #2563eb;
|
||||
--accent-light: #eff4ff;
|
||||
--accent-hover: #1d4ed8;
|
||||
--bubble-mine: #2563eb;
|
||||
--bubble-mine-text: #ffffff;
|
||||
--bubble-theirs: #f0f1f3;
|
||||
--bubble-theirs-text: #1a1d23;
|
||||
--unread: #ef4444;
|
||||
--online: #22c55e;
|
||||
--hover: #f8f9fb;
|
||||
--active: #eff4ff;
|
||||
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
|
||||
--shadow-md: 0 4px 12px rgba(0,0,0,0.08);
|
||||
--radius: 12px;
|
||||
--radius-sm: 8px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'DM Sans', -apple-system, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Top bar (simulating portal nav) ── */
|
||||
.topbar {
|
||||
height: 56px;
|
||||
background: #1e3a5f;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24px;
|
||||
gap: 16px;
|
||||
}
|
||||
.topbar-logo {
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
.topbar-logo span { color: #60a5fa; }
|
||||
.topbar-nav {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-left: 32px;
|
||||
}
|
||||
.topbar-nav a {
|
||||
color: rgba(255,255,255,0.65);
|
||||
text-decoration: none;
|
||||
font-size: 13.5px;
|
||||
padding: 6px 14px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.15s;
|
||||
font-weight: 500;
|
||||
}
|
||||
.topbar-nav a:hover { color: white; background: rgba(255,255,255,0.08); }
|
||||
.topbar-nav a.active {
|
||||
color: white;
|
||||
background: rgba(255,255,255,0.12);
|
||||
}
|
||||
.topbar-badge {
|
||||
background: var(--unread);
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 1px 5px;
|
||||
border-radius: 10px;
|
||||
margin-left: 4px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* ── Main layout ── */
|
||||
.messages-container {
|
||||
display: flex;
|
||||
height: calc(100vh - 56px);
|
||||
}
|
||||
|
||||
/* ── Left panel: conversation list ── */
|
||||
.conversations-panel {
|
||||
width: 380px;
|
||||
min-width: 380px;
|
||||
background: var(--panel);
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.conversations-header {
|
||||
padding: 20px 20px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.conversations-header h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.4px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
position: relative;
|
||||
}
|
||||
.search-box input {
|
||||
width: 100%;
|
||||
padding: 9px 12px 9px 36px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 13.5px;
|
||||
font-family: inherit;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.search-box input:focus { border-color: var(--accent); }
|
||||
.search-box input::placeholder { color: var(--text-muted); }
|
||||
.search-box svg {
|
||||
position: absolute;
|
||||
left: 11px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.new-message-btn {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.new-message-btn:hover { background: var(--accent-hover); }
|
||||
|
||||
.conversation-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.conversation-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: background 0.12s;
|
||||
position: relative;
|
||||
}
|
||||
.conversation-item:hover { background: var(--hover); }
|
||||
.conversation-item.active { background: var(--active); }
|
||||
.conversation-item.unread .conv-name { font-weight: 700; }
|
||||
.conversation-item.unread .conv-preview { color: var(--text); font-weight: 500; }
|
||||
|
||||
.conv-avatar {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
.conv-avatar.green { background: #059669; }
|
||||
.conv-avatar.blue { background: #2563eb; }
|
||||
.conv-avatar.purple { background: #7c3aed; }
|
||||
.conv-avatar.orange { background: #ea580c; }
|
||||
.conv-avatar.teal { background: #0d9488; }
|
||||
.conv-avatar.rose { background: #e11d48; }
|
||||
.conv-avatar.group { background: #475569; font-size: 14px; }
|
||||
|
||||
.conv-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.conv-avatar .online-dot {
|
||||
position: absolute;
|
||||
bottom: 1px;
|
||||
right: 1px;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
background: var(--online);
|
||||
border: 2px solid var(--panel);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.conv-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.conv-top {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.conv-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.conv-time {
|
||||
font-size: 11.5px;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
margin-left: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.conversation-item.unread .conv-time { color: var(--accent); font-weight: 600; }
|
||||
|
||||
.conv-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.conv-preview {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.conv-preview .you { color: var(--text-muted); }
|
||||
|
||||
.unread-badge {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 6px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.conv-group-tag {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
margin-left: 6px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* ── Right panel: chat view ── */
|
||||
.chat-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
background: var(--panel);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 14px 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.chat-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.chat-header-avatar {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
.chat-header-info h3 {
|
||||
font-size: 15px;
|
||||
font-weight: 650;
|
||||
letter-spacing: -0.2px;
|
||||
}
|
||||
|
||||
.chat-header-info .subtitle {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.chat-header-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.chat-header-actions button {
|
||||
background: none;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-secondary);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.12s;
|
||||
}
|
||||
.chat-header-actions button:hover {
|
||||
background: var(--hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ── Messages area ── */
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px 24px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.date-separator {
|
||||
text-align: center;
|
||||
margin: 16px 0;
|
||||
position: relative;
|
||||
}
|
||||
.date-separator span {
|
||||
background: var(--bg);
|
||||
padding: 0 14px;
|
||||
font-size: 11.5px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.date-separator::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 10%;
|
||||
right: 10%;
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.message-row {
|
||||
display: flex;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.message-row.mine { justify-content: flex-end; }
|
||||
.message-row.theirs { justify-content: flex-start; }
|
||||
|
||||
.message-bubble {
|
||||
max-width: 520px;
|
||||
padding: 10px 14px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
position: relative;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.message-row.mine .message-bubble {
|
||||
background: var(--bubble-mine);
|
||||
color: var(--bubble-mine-text);
|
||||
border-radius: 16px 16px 4px 16px;
|
||||
}
|
||||
.message-row.theirs .message-bubble {
|
||||
background: var(--bubble-theirs);
|
||||
color: var(--bubble-theirs-text);
|
||||
border-radius: 16px 16px 16px 4px;
|
||||
}
|
||||
|
||||
/* consecutive messages get flat edges */
|
||||
.message-row.mine + .message-row.mine .message-bubble {
|
||||
border-radius: 16px 4px 4px 16px;
|
||||
}
|
||||
.message-row.mine:has(+ .message-row.mine) .message-bubble {
|
||||
border-radius: 16px 16px 4px 16px;
|
||||
}
|
||||
.message-row.theirs + .message-row.theirs .message-bubble {
|
||||
border-radius: 16px 16px 16px 4px;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.message-row.mine .message-time {
|
||||
color: rgba(255,255,255,0.6);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.message-row.theirs .message-time {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.read-check {
|
||||
color: rgba(255,255,255,0.6);
|
||||
}
|
||||
.read-check.read { color: #93c5fd; }
|
||||
|
||||
.message-subject {
|
||||
font-size: 11.5px;
|
||||
font-weight: 600;
|
||||
opacity: 0.7;
|
||||
margin-bottom: 4px;
|
||||
letter-spacing: 0.2px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.message-row.theirs .message-subject { color: var(--text-muted); }
|
||||
|
||||
/* ── Input area ── */
|
||||
.chat-input-area {
|
||||
background: var(--panel);
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.chat-input-wrapper {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 8px 12px;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.chat-input-wrapper:focus-within {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.08);
|
||||
}
|
||||
|
||||
.chat-input-wrapper textarea {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: none;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
color: var(--text);
|
||||
resize: none;
|
||||
outline: none;
|
||||
min-height: 22px;
|
||||
max-height: 120px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.chat-input-wrapper textarea::placeholder { color: var(--text-muted); }
|
||||
|
||||
.input-actions {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-actions button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.12s;
|
||||
}
|
||||
.input-actions button:hover { color: var(--text-secondary); background: rgba(0,0,0,0.04); }
|
||||
|
||||
.send-btn {
|
||||
background: var(--accent) !important;
|
||||
color: white !important;
|
||||
border-radius: 8px !important;
|
||||
width: 36px !important;
|
||||
height: 32px !important;
|
||||
}
|
||||
.send-btn:hover { background: var(--accent-hover) !important; }
|
||||
|
||||
/* ── Empty state (no conversation selected) ── */
|
||||
.chat-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-muted);
|
||||
gap: 12px;
|
||||
}
|
||||
.chat-empty svg { opacity: 0.3; }
|
||||
.chat-empty p { font-size: 14px; }
|
||||
|
||||
/* ── Scrollbar ── */
|
||||
.conversation-list::-webkit-scrollbar,
|
||||
.chat-messages::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.conversation-list::-webkit-scrollbar-thumb,
|
||||
.chat-messages::-webkit-scrollbar-thumb {
|
||||
background: rgba(0,0,0,0.12);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* ── Watermark ── */
|
||||
.mockup-badge {
|
||||
position: fixed;
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
background: rgba(0,0,0,0.7);
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
font-weight: 500;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Top bar -->
|
||||
<div class="topbar">
|
||||
<div class="topbar-logo">Norda<span>Biznes</span></div>
|
||||
<nav class="topbar-nav">
|
||||
<a href="#">Firmy</a>
|
||||
<a href="#">Forum</a>
|
||||
<a href="#">Kalendarz</a>
|
||||
<a href="#" class="active">Wiadomości<span class="topbar-badge">2</span></a>
|
||||
<a href="#">NordaGPT</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="messages-container">
|
||||
|
||||
<!-- Left: Conversation list -->
|
||||
<div class="conversations-panel">
|
||||
<div class="conversations-header">
|
||||
<h2>Wiadomości</h2>
|
||||
<div class="search-box">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
<input type="text" placeholder="Szukaj rozmów...">
|
||||
<button class="new-message-btn" title="Nowa wiadomość">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="conversation-list">
|
||||
|
||||
<!-- Active conversation: Magdalena -->
|
||||
<div class="conversation-item active unread" onclick="selectConv(this)">
|
||||
<div class="conv-avatar green">MK</div>
|
||||
<div class="conv-content">
|
||||
<div class="conv-top">
|
||||
<span class="conv-name">Magdalena Kloska</span>
|
||||
<span class="conv-time">11:54</span>
|
||||
</div>
|
||||
<div class="conv-bottom">
|
||||
<span class="conv-preview">witam, weszlam w skladki i mam pytanie...</span>
|
||||
<span class="unread-badge">2</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group conversation -->
|
||||
<div class="conversation-item" onclick="selectConv(this)">
|
||||
<div class="conv-avatar group">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||
</div>
|
||||
<div class="conv-content">
|
||||
<div class="conv-top">
|
||||
<span class="conv-name">Modul skladek<span class="conv-group-tag">7 os.</span></span>
|
||||
<span class="conv-time">24.03</span>
|
||||
</div>
|
||||
<div class="conv-bottom">
|
||||
<span class="conv-preview"><span class="you">Ty: </span>Jest gotowa taka funkcjonalnosc dla roli kierownika...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Artur -->
|
||||
<div class="conversation-item" onclick="selectConv(this)">
|
||||
<div class="conv-avatar blue">AW</div>
|
||||
<div class="conv-content">
|
||||
<div class="conv-top">
|
||||
<span class="conv-name">Artur Wiertel</span>
|
||||
<span class="conv-time">20.03</span>
|
||||
</div>
|
||||
<div class="conv-bottom">
|
||||
<span class="conv-preview">fajnie to wyglada.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Roman -->
|
||||
<div class="conversation-item" onclick="selectConv(this)">
|
||||
<div class="conv-avatar purple">RW</div>
|
||||
<div class="conv-content">
|
||||
<div class="conv-top">
|
||||
<span class="conv-name">Roman Wiercinski</span>
|
||||
<span class="conv-time">18.03</span>
|
||||
</div>
|
||||
<div class="conv-bottom">
|
||||
<span class="conv-preview"><span class="you">Ty: </span>Dzien dobry, przesylam podsumowanie...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leszek -->
|
||||
<div class="conversation-item" onclick="selectConv(this)">
|
||||
<div class="conv-avatar teal">LG</div>
|
||||
<div class="conv-content">
|
||||
<div class="conv-top">
|
||||
<span class="conv-name">Leszek Glaza</span>
|
||||
<span class="conv-time">15.03</span>
|
||||
</div>
|
||||
<div class="conv-bottom">
|
||||
<span class="conv-preview"><span class="you">Ty: </span>Lista firm z kontaktami gotowa.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Chat view -->
|
||||
<div class="chat-panel">
|
||||
|
||||
<div class="chat-header">
|
||||
<div class="chat-header-left">
|
||||
<div class="chat-header-avatar">MK</div>
|
||||
<div class="chat-header-info">
|
||||
<h3>Magdalena Kloska</h3>
|
||||
<div class="subtitle">Kierownik Biura · Izba Norda Biznes</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-header-actions">
|
||||
<button title="Szukaj w rozmowie">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
</button>
|
||||
<button title="Wiecej">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-messages">
|
||||
|
||||
<div class="date-separator"><span>19 marca 2026</span></div>
|
||||
|
||||
<!-- My message -->
|
||||
<div class="message-row mine">
|
||||
<div class="message-bubble">
|
||||
<div class="message-subject">Skladki i NIP-y nowych czlonkow</div>
|
||||
Pani Magdaleno, importuje dane o skladkach z pliku Excel do systemu portalu. Wiekszosc firm dopasowala sie automatycznie, ale kilka nazw wymaga weryfikacji.
|
||||
|
||||
Prosze o sprawdzenie — czy te firmy maja profile na portalu pod inna nazwa?
|
||||
|
||||
EKOZUK, FRESH BIKE, JANTAR, MACIEJ HALAS, MARKISOL, N33, PGK, SKLEPY LORD, WW GLASS
|
||||
<div class="message-time">
|
||||
19:49
|
||||
<svg class="read-check read" width="16" height="12" viewBox="0 0 16 12" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 6l3.5 4L11 2"/><path d="M5 6l3.5 4L15 2"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-row mine">
|
||||
<div class="message-bubble">
|
||||
<div class="message-subject">Kalendarz — wydarzenia zewnetrzne</div>
|
||||
Pani Magdo, dodalismy przed chwila tez informacje o wydarzeniach zewnetrznych. One w kalendarzu sa widoczne...
|
||||
<div class="message-time">
|
||||
12:06
|
||||
<svg class="read-check read" width="16" height="12" viewBox="0 0 16 12" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 6l3.5 4L11 2"/><path d="M5 6l3.5 4L15 2"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-row theirs">
|
||||
<div class="message-bubble">
|
||||
z teog co widze na szybko i odnotowalam to firm ted jest w3pisana dwa razy ale nip ten sam tutaj...
|
||||
<div class="message-time">13:30</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-row mine">
|
||||
<div class="message-bubble">
|
||||
Ok, pani Magdo, ogarne te tematy, jak tylko wroce do biura.
|
||||
<div class="message-time">
|
||||
13:54
|
||||
<svg class="read-check read" width="16" height="12" viewBox="0 0 16 12" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 6l3.5 4L11 2"/><path d="M5 6l3.5 4L15 2"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="date-separator"><span>25 marca 2026</span></div>
|
||||
|
||||
<div class="message-row theirs">
|
||||
<div class="message-bubble">
|
||||
dzien dobry do jutra wlacznie mam opieke ale staram sie ogrniac tematy ile sie da.
|
||||
|
||||
EKOZUK- EKOFABRYKA TO JEST TO SAMO
|
||||
|
||||
FRESH BIKE fresh bike oraz jantar- to jest podspolka da Eura Tech - te na niebiesko nie placa skladeek indywidualnych ale firma wchodzaca do Nordy moze wniesc kilka swoich spolek placac wieksza skladke.
|
||||
|
||||
MACIEJ HALAS- prosze ich nie uwzlgedniac - rezygnacja z czlonkostwa.
|
||||
|
||||
MARKISOL tak samo rezygnajca
|
||||
|
||||
N33 to tezs jest pod spolk awraz z Termo
|
||||
|
||||
PGK Pucka Gospodarka Komunalna Sp. z o.o. NIP: 587-02-00-062
|
||||
|
||||
SKLEPY LORD: Lord sp. z o.o. nip 5882533102
|
||||
|
||||
WW GLASS tak samo podspolka innej firmy glownje - tutaj lenap hale
|
||||
<div class="message-time">11:47</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-row theirs">
|
||||
<div class="message-bubble">
|
||||
witam, weszlam w skladki i mam pytanie jesli ktos np zaplaci zaleglosc lub kolejna rate jaqk to zmienic w systemie ?
|
||||
<div class="message-time">11:54</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="date-separator"><span>27 marca 2026</span></div>
|
||||
|
||||
<div class="message-row mine">
|
||||
<div class="message-bubble">
|
||||
Pani Magdaleno, informacje o firmach przyjete, wprowadzam poprawki w systemie.
|
||||
|
||||
Dwa pytania:
|
||||
|
||||
1. N33 i Termo — pod jaka firme glowna wchodza?
|
||||
|
||||
2. Potrzebuje NIP-y nowych czlonkow: Audioline, Coach 4 You, Digital Technik, Ekonsult, GoodWill, IBET, Prospoland, Steamset.
|
||||
|
||||
Co do skladek — panel Administracja, Skladki, przy danym miesiacu "Oznacz jako oplacone". Mozna wpisac kwote i date wplaty.
|
||||
<div class="message-time">
|
||||
11:07
|
||||
<svg class="read-check" width="16" height="12" viewBox="0 0 16 12" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 6l3.5 4L11 2"/><path d="M5 6l3.5 4L15 2"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Input -->
|
||||
<div class="chat-input-area">
|
||||
<div class="chat-input-wrapper">
|
||||
<textarea rows="1" placeholder="Napisz wiadomosc..."></textarea>
|
||||
<div class="input-actions">
|
||||
<button title="Dolacz plik">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
|
||||
</button>
|
||||
<button class="send-btn" title="Wyslij">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mockup-badge">MOCKUP — widok konwersacyjny</div>
|
||||
|
||||
<script>
|
||||
function selectConv(el) {
|
||||
document.querySelectorAll('.conversation-item').forEach(i => i.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -186,7 +186,7 @@ blog, contact_form, detailed_services, analytics, live_chat
|
||||
---
|
||||
|
||||
**Report generated by Norda Biznes Digital Maturity Platform**
|
||||
*Data source: PostgreSQL database on NORDABIZ-01 (10.22.68.249)*
|
||||
*Data source: PostgreSQL database on NORDABIZ-01 (57.128.200.27)*
|
||||
*Phase: ETAP 1 - Foundation*
|
||||
|
||||
---
|
||||
|
||||
6
scripts/.ovh_vps_monitor_state.json
Normal file
6
scripts/.ovh_vps_monitor_state.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"vps-2025-model1": "available",
|
||||
"vps-2025-model2": "available",
|
||||
"vps-2025-model3": "available",
|
||||
"vps-2025-model4": "available"
|
||||
}
|
||||
54
scripts/audit_company_data.py
Normal file
54
scripts/audit_company_data.py
Normal file
@ -0,0 +1,54 @@
|
||||
"""Audit active companies for missing KRS/CEIDG data."""
|
||||
import os, sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from sqlalchemy import create_engine, text
|
||||
|
||||
db_url = os.environ.get('DATABASE_URL', 'postgresql://nordabiz_app:nordabiz_pass@localhost:5433/nordabiz')
|
||||
engine = create_engine(db_url)
|
||||
|
||||
with engine.connect() as conn:
|
||||
r = conn.execute(text(
|
||||
"SELECT id, name, nip, krs, legal_form, address_street, address_city, "
|
||||
"email, phone, website, data_quality, fee_included_in_parent "
|
||||
"FROM companies WHERE status = 'active' ORDER BY name"
|
||||
))
|
||||
|
||||
no_nip = []
|
||||
no_address = []
|
||||
no_contact = []
|
||||
no_legal_form = []
|
||||
basic_quality = []
|
||||
|
||||
for row in r:
|
||||
cid, name, nip, krs, legal_form, street, city, email, phone, website, quality, fee_in_parent = row
|
||||
if not nip and not fee_in_parent:
|
||||
no_nip.append((cid, name))
|
||||
if not street and not city:
|
||||
no_address.append((cid, name))
|
||||
if not email and not phone:
|
||||
no_contact.append((cid, name))
|
||||
if not legal_form:
|
||||
no_legal_form.append((cid, name))
|
||||
if quality == 'basic':
|
||||
basic_quality.append((cid, name, nip))
|
||||
|
||||
print(f'=== Firmy BEZ NIP (nie-podspolki): {len(no_nip)} ===')
|
||||
for cid, name in no_nip:
|
||||
print(f' id={cid:4d} {name}')
|
||||
|
||||
print(f'\n=== Firmy BEZ adresu: {len(no_address)} ===')
|
||||
for cid, name in no_address:
|
||||
print(f' id={cid:4d} {name}')
|
||||
|
||||
print(f'\n=== Firmy BEZ kontaktu (email+telefon): {len(no_contact)} ===')
|
||||
for cid, name in no_contact:
|
||||
print(f' id={cid:4d} {name}')
|
||||
|
||||
print(f'\n=== Firmy BEZ formy prawnej: {len(no_legal_form)} ===')
|
||||
for cid, name in no_legal_form:
|
||||
print(f' id={cid:4d} {name}')
|
||||
|
||||
print(f'\n=== Firmy z data_quality=basic (minimalne dane): {len(basic_quality)} ===')
|
||||
for cid, name, nip in basic_quality:
|
||||
print(f' id={cid:4d} nip={str(nip or "BRAK"):15s} {name}')
|
||||
1112
scripts/create_presentation.js
Normal file
1112
scripts/create_presentation.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -14,7 +14,7 @@ from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from database import Company, Category
|
||||
|
||||
# DEV: localhost:5433, PROD: 10.22.68.249:5432
|
||||
# DEV: localhost:5433, PROD: 57.128.200.27:5432
|
||||
DATABASE_URL = os.environ.get('DATABASE_URL', 'postgresql://nordabiz_app:dev_password@localhost:5433/nordabiz')
|
||||
|
||||
|
||||
|
||||
340
scripts/ovh_vps_monitor.py
Normal file
340
scripts/ovh_vps_monitor.py
Normal file
@ -0,0 +1,340 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
OVH VPS Availability Monitor for Warsaw (WAW)
|
||||
Sprawdza dostępność VPS-1, VPS-2, VPS-3, VPS-4 w Warszawie.
|
||||
Powiadamia przez macOS notification + opcjonalnie Telegram.
|
||||
|
||||
Użycie:
|
||||
python3 scripts/ovh_vps_monitor.py # jednorazowe sprawdzenie
|
||||
python3 scripts/ovh_vps_monitor.py --daemon # co 10 minut
|
||||
|
||||
Konfiguracja OVH API (jednorazowo):
|
||||
1. Wejdź na https://eu.api.ovh.com/createToken/
|
||||
2. Zaloguj się kontem pm861830-ovh
|
||||
3. Ustaw:
|
||||
- GET /order/cart/*
|
||||
- POST /order/cart/*
|
||||
- DELETE /order/cart/*
|
||||
4. Wpisz klucze do ~/.ovh.conf (format poniżej)
|
||||
|
||||
~/.ovh.conf:
|
||||
[default]
|
||||
endpoint=ovh-eu
|
||||
application_key=TWÓJ_APP_KEY
|
||||
application_secret=TWÓJ_APP_SECRET
|
||||
consumer_key=TWÓJ_CONSUMER_KEY
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
# --- Configuration ---
|
||||
MODELS = {
|
||||
'vps-2025-model1': 'VPS-1 (4 vCores, 8 GB, 75 GB SSD) — 23,50 PLN/m',
|
||||
'vps-2025-model2': 'VPS-2 (6 vCores, 12 GB, 100 GB NVMe) — 36,21 PLN/m',
|
||||
'vps-2025-model3': 'VPS-3 (8 vCores, 24 GB, 200 GB NVMe) — 72,42 PLN/m',
|
||||
'vps-2025-model4': 'VPS-4 (12 vCores, 48 GB, 300 GB NVMe) — 133,96 PLN/m',
|
||||
}
|
||||
|
||||
TARGET_DC = 'WAW'
|
||||
CHECK_INTERVAL = 600 # 10 minut
|
||||
STATE_FILE = Path(__file__).parent / '.ovh_vps_monitor_state.json'
|
||||
TELEGRAM_BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', '')
|
||||
TELEGRAM_CHAT_ID = os.environ.get('TELEGRAM_CHAT_ID', '')
|
||||
|
||||
|
||||
def check_availability_curl():
|
||||
"""Sprawdza dostępność VPS w WAW przez publiczne API OVH (cart flow)."""
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
results = {}
|
||||
|
||||
# 1. Utwórz koszyk
|
||||
req = urllib.request.Request(
|
||||
'https://eu.api.ovh.com/1.0/order/cart',
|
||||
data=json.dumps({'ovhSubsidiary': 'PL'}).encode(),
|
||||
headers={'Content-Type': 'application/json'},
|
||||
method='POST'
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
cart = json.loads(resp.read())
|
||||
cart_id = cart['cartId']
|
||||
except Exception as e:
|
||||
print(f'[ERROR] Nie mogę utworzyć koszyka: {e}')
|
||||
return {}
|
||||
|
||||
# 2. Dla każdego modelu — dodaj do koszyka i sprawdź DC
|
||||
for plan_code, plan_name in MODELS.items():
|
||||
try:
|
||||
# Dodaj VPS do koszyka
|
||||
add_req = urllib.request.Request(
|
||||
f'https://eu.api.ovh.com/1.0/order/cart/{cart_id}/vps',
|
||||
data=json.dumps({
|
||||
'planCode': plan_code,
|
||||
'duration': 'P1M',
|
||||
'pricingMode': 'default',
|
||||
'quantity': 1
|
||||
}).encode(),
|
||||
headers={'Content-Type': 'application/json'},
|
||||
method='POST'
|
||||
)
|
||||
with urllib.request.urlopen(add_req, timeout=15) as resp:
|
||||
item = json.loads(resp.read())
|
||||
item_id = item['itemId']
|
||||
|
||||
# Skonfiguruj WAW
|
||||
dc_req = urllib.request.Request(
|
||||
f'https://eu.api.ovh.com/1.0/order/cart/{cart_id}/item/{item_id}/configuration',
|
||||
data=json.dumps({'label': 'vps_datacenter', 'value': TARGET_DC}).encode(),
|
||||
headers={'Content-Type': 'application/json'},
|
||||
method='POST'
|
||||
)
|
||||
with urllib.request.urlopen(dc_req, timeout=15) as resp:
|
||||
dc_result = json.loads(resp.read())
|
||||
|
||||
# Skonfiguruj OS i region
|
||||
for cfg in [
|
||||
{'label': 'vps_os', 'value': 'Ubuntu 24.04'},
|
||||
{'label': 'region', 'value': 'europe'},
|
||||
]:
|
||||
cfg_req = urllib.request.Request(
|
||||
f'https://eu.api.ovh.com/1.0/order/cart/{cart_id}/item/{item_id}/configuration',
|
||||
data=json.dumps(cfg).encode(),
|
||||
headers={'Content-Type': 'application/json'},
|
||||
method='POST'
|
||||
)
|
||||
with urllib.request.urlopen(cfg_req, timeout=15) as resp:
|
||||
pass
|
||||
|
||||
# Sprawdź podsumowanie koszyka (publiczne, nie wymaga auth)
|
||||
summary_req = urllib.request.Request(
|
||||
f'https://eu.api.ovh.com/1.0/order/cart/{cart_id}/summary',
|
||||
method='GET'
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(summary_req, timeout=15) as resp:
|
||||
summary = json.loads(resp.read())
|
||||
results[plan_code] = 'available'
|
||||
except urllib.error.HTTPError as e:
|
||||
error_body = e.read().decode() if e.fp else ''
|
||||
if 'stock' in error_body.lower() or 'unavailable' in error_body.lower():
|
||||
results[plan_code] = 'out_of_stock'
|
||||
elif e.code == 401:
|
||||
# Auth wymagane — nie wiemy na pewno, spróbuj auth flow
|
||||
results[plan_code] = 'needs_auth'
|
||||
else:
|
||||
results[plan_code] = 'unknown'
|
||||
|
||||
# Usuń item z koszyka
|
||||
del_req = urllib.request.Request(
|
||||
f'https://eu.api.ovh.com/1.0/order/cart/{cart_id}/item/{item_id}',
|
||||
method='DELETE'
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(del_req, timeout=15) as resp:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except urllib.error.HTTPError as e:
|
||||
error_body = e.read().decode() if e.fp else ''
|
||||
if 'stock' in error_body.lower() or 'not available' in error_body.lower():
|
||||
results[plan_code] = 'out_of_stock'
|
||||
else:
|
||||
results[plan_code] = f'error: {e.code}'
|
||||
except Exception as e:
|
||||
results[plan_code] = f'error: {e}'
|
||||
|
||||
# Usuń koszyk
|
||||
try:
|
||||
del_cart = urllib.request.Request(
|
||||
f'https://eu.api.ovh.com/1.0/order/cart/{cart_id}',
|
||||
method='DELETE'
|
||||
)
|
||||
urllib.request.urlopen(del_cart, timeout=15)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def check_availability_ovh_lib():
|
||||
"""Sprawdza dostępność VPS w WAW przez ovh Python library (z autoryzacją)."""
|
||||
try:
|
||||
import ovh
|
||||
except ImportError:
|
||||
print('[WARN] Brak biblioteki ovh — pip3 install ovh')
|
||||
return {}
|
||||
|
||||
conf_path = Path.home() / '.ovh.conf'
|
||||
if not conf_path.exists():
|
||||
print(f'[WARN] Brak {conf_path} — użyj trybu bez autoryzacji')
|
||||
return {}
|
||||
|
||||
try:
|
||||
client = ovh.Client()
|
||||
except Exception as e:
|
||||
print(f'[ERROR] Nie mogę połączyć z OVH API: {e}')
|
||||
return {}
|
||||
|
||||
results = {}
|
||||
|
||||
for plan_code, plan_name in MODELS.items():
|
||||
try:
|
||||
# Utwórz koszyk
|
||||
cart = client.post('/order/cart', ovhSubsidiary='PL')
|
||||
cart_id = cart['cartId']
|
||||
client.post(f'/order/cart/{cart_id}/assign')
|
||||
|
||||
# Dodaj VPS
|
||||
item = client.post(f'/order/cart/{cart_id}/vps',
|
||||
planCode=plan_code, duration='P1M',
|
||||
pricingMode='default', quantity=1)
|
||||
item_id = item['itemId']
|
||||
|
||||
# Skonfiguruj WAW + OS + region
|
||||
client.post(f'/order/cart/{cart_id}/item/{item_id}/configuration',
|
||||
label='vps_datacenter', value=TARGET_DC)
|
||||
client.post(f'/order/cart/{cart_id}/item/{item_id}/configuration',
|
||||
label='vps_os', value='Ubuntu 24.04')
|
||||
client.post(f'/order/cart/{cart_id}/item/{item_id}/configuration',
|
||||
label='region', value='europe')
|
||||
|
||||
# Validate checkout (GET = validate, POST = place order)
|
||||
checkout = client.get(f'/order/cart/{cart_id}/checkout')
|
||||
# Jeśli doszliśmy tutaj — VPS jest dostępny w WAW!
|
||||
results[plan_code] = 'available'
|
||||
|
||||
# Cleanup
|
||||
client.delete(f'/order/cart/{cart_id}')
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e).lower()
|
||||
if 'stock' in error_msg or 'not available' in error_msg or 'unavailable' in error_msg:
|
||||
results[plan_code] = 'out_of_stock'
|
||||
elif 'expired' in error_msg:
|
||||
results[plan_code] = 'error_expired'
|
||||
else:
|
||||
results[plan_code] = f'error: {e}'
|
||||
|
||||
# Cleanup
|
||||
try:
|
||||
client.delete(f'/order/cart/{cart_id}')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def notify_macos(title, message):
|
||||
"""Powiadomienie macOS."""
|
||||
subprocess.run([
|
||||
'osascript', '-e',
|
||||
f'display notification "{message}" with title "{title}" sound name "Glass"'
|
||||
], check=False)
|
||||
|
||||
|
||||
def notify_telegram(message):
|
||||
"""Powiadomienie Telegram."""
|
||||
if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
|
||||
return
|
||||
import urllib.request
|
||||
url = f'https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage'
|
||||
data = json.dumps({
|
||||
'chat_id': TELEGRAM_CHAT_ID,
|
||||
'text': message,
|
||||
'parse_mode': 'Markdown'
|
||||
}).encode()
|
||||
req = urllib.request.Request(url, data=data,
|
||||
headers={'Content-Type': 'application/json'})
|
||||
try:
|
||||
urllib.request.urlopen(req, timeout=10)
|
||||
except Exception as e:
|
||||
print(f'[WARN] Telegram notification failed: {e}')
|
||||
|
||||
|
||||
def load_state():
|
||||
"""Wczytaj poprzedni stan."""
|
||||
if STATE_FILE.exists():
|
||||
return json.loads(STATE_FILE.read_text())
|
||||
return {}
|
||||
|
||||
|
||||
def save_state(state):
|
||||
"""Zapisz stan."""
|
||||
STATE_FILE.write_text(json.dumps(state, indent=2))
|
||||
|
||||
|
||||
def run_check():
|
||||
"""Główna funkcja sprawdzająca."""
|
||||
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
print(f'\n[{now}] Sprawdzam dostępność VPS w {TARGET_DC}...')
|
||||
|
||||
# Preferuj auth flow (dokładniejszy), fallback na public API
|
||||
conf_path = Path.home() / '.ovh.conf'
|
||||
if conf_path.exists():
|
||||
print(' Tryb: OVH API (z autoryzacją)')
|
||||
results = check_availability_ovh_lib()
|
||||
else:
|
||||
print(' Tryb: Public API (bez autoryzacji — mniej dokładny)')
|
||||
results = check_availability_curl()
|
||||
|
||||
if not results:
|
||||
print(' [WARN] Brak wyników — problem z API?')
|
||||
return
|
||||
|
||||
prev_state = load_state()
|
||||
new_available = []
|
||||
|
||||
for plan_code, status in results.items():
|
||||
plan_name = MODELS.get(plan_code, plan_code)
|
||||
icon = '✅' if status == 'available' else '❌' if 'stock' in str(status) else '❓'
|
||||
print(f' {icon} {plan_name}: {status}')
|
||||
|
||||
# Czy to nowa dostępność?
|
||||
prev = prev_state.get(plan_code, '')
|
||||
if status == 'available' and prev != 'available':
|
||||
new_available.append(plan_name)
|
||||
|
||||
# Powiadom o nowych dostępnościach
|
||||
if new_available:
|
||||
msg_lines = ['🟢 VPS dostępny w Warszawie!'] + [f' • {n}' for n in new_available]
|
||||
msg_lines.append(f'\n🔗 https://www.ovhcloud.com/pl/vps/')
|
||||
message = '\n'.join(msg_lines)
|
||||
|
||||
print(f'\n 🔔 POWIADOMIENIE: {message}')
|
||||
notify_macos('OVH VPS Warszawa!', '\n'.join(new_available))
|
||||
notify_telegram(message)
|
||||
else:
|
||||
print(' Brak nowych dostępności.')
|
||||
|
||||
save_state(results)
|
||||
|
||||
|
||||
def main():
|
||||
daemon = '--daemon' in sys.argv
|
||||
|
||||
if daemon:
|
||||
print(f'Uruchamiam monitoring VPS w {TARGET_DC} (co {CHECK_INTERVAL}s)...')
|
||||
print(f'Stan zapisywany w: {STATE_FILE}')
|
||||
print('Ctrl+C aby zatrzymać\n')
|
||||
while True:
|
||||
try:
|
||||
run_check()
|
||||
time.sleep(CHECK_INTERVAL)
|
||||
except KeyboardInterrupt:
|
||||
print('\nZatrzymano.')
|
||||
break
|
||||
else:
|
||||
run_check()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
103
scripts/sync_staging_db.sh
Executable file
103
scripts/sync_staging_db.sh
Executable file
@ -0,0 +1,103 @@
|
||||
#!/bin/bash
|
||||
# Sync staging database to local Docker dev environment
|
||||
# Usage: ./scripts/sync_staging_db.sh
|
||||
#
|
||||
# Can run interactively or via launchd (auto mode).
|
||||
# In auto mode: silently skips if Docker/VPN unavailable, logs to file.
|
||||
#
|
||||
# Prerequisites:
|
||||
# - SSH access to staging (maciejpi@10.22.68.248)
|
||||
# - Docker container 'nordabiz-postgres' running locally
|
||||
# - Local DB: nordabiz_app/dev_password on port 5433
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
STAGING_HOST="maciejpi@10.22.68.248"
|
||||
STAGING_DB="nordabiz_staging"
|
||||
LOCAL_CONTAINER="nordabiz-postgres"
|
||||
LOCAL_DB="nordabiz"
|
||||
LOCAL_USER="nordabiz_app"
|
||||
DUMP_FILE="/tmp/nordabiz_staging_dump.sql"
|
||||
LOG_FILE="$HOME/.local/log/nordabiz-db-sync.log"
|
||||
STAMP_FILE="$HOME/.local/state/nordabiz-db-sync-last"
|
||||
|
||||
# Ensure log/state dirs exist
|
||||
mkdir -p "$(dirname "$LOG_FILE")" "$(dirname "$STAMP_FILE")"
|
||||
|
||||
# Detect interactive vs auto mode
|
||||
AUTO=false
|
||||
if [ "${1:-}" = "--auto" ]; then
|
||||
AUTO=true
|
||||
fi
|
||||
|
||||
log() {
|
||||
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
||||
if [ "$AUTO" = true ]; then
|
||||
echo "$msg" >> "$LOG_FILE"
|
||||
else
|
||||
echo "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
fail() {
|
||||
log "SKIP: $1"
|
||||
exit 0 # exit 0 in auto mode so launchd doesn't retry
|
||||
}
|
||||
|
||||
log "=== NordaBiz: Sync staging DB → local dev ==="
|
||||
|
||||
# Check Docker container
|
||||
if ! docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${LOCAL_CONTAINER}$"; then
|
||||
if [ "$AUTO" = true ]; then
|
||||
fail "Docker container '${LOCAL_CONTAINER}' not running"
|
||||
else
|
||||
echo "ERROR: Docker container '${LOCAL_CONTAINER}' not running."
|
||||
echo "Start it: docker compose up -d"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check SSH/VPN
|
||||
if ! ssh -o ConnectTimeout=5 -o BatchMode=yes "${STAGING_HOST}" "echo ok" &>/dev/null; then
|
||||
if [ "$AUTO" = true ]; then
|
||||
fail "Cannot reach staging (VPN probably off)"
|
||||
else
|
||||
echo "ERROR: Cannot connect to staging (${STAGING_HOST})."
|
||||
echo "Check VPN connection."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Step 1: Dump staging
|
||||
log "[1/3] Dumping staging database..."
|
||||
ssh "${STAGING_HOST}" "sudo -u postgres pg_dump ${STAGING_DB}" > "${DUMP_FILE}"
|
||||
DUMP_SIZE=$(du -h "${DUMP_FILE}" | cut -f1)
|
||||
log " Done (${DUMP_SIZE})"
|
||||
|
||||
# Step 2: Restore to container
|
||||
log "[2/3] Restoring to local Docker..."
|
||||
docker cp "${DUMP_FILE}" "${LOCAL_CONTAINER}:/tmp/staging_dump.sql"
|
||||
|
||||
docker exec "${LOCAL_CONTAINER}" bash -c "
|
||||
psql -U ${LOCAL_USER} -d template1 -c \"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='${LOCAL_DB}' AND pid <> pg_backend_pid();\" 2>/dev/null
|
||||
psql -U ${LOCAL_USER} -d template1 -c 'DROP DATABASE IF EXISTS ${LOCAL_DB};'
|
||||
psql -U ${LOCAL_USER} -d template1 -c 'CREATE DATABASE ${LOCAL_DB} OWNER ${LOCAL_USER};'
|
||||
psql -U ${LOCAL_USER} -d ${LOCAL_DB} < /tmp/staging_dump.sql 2>/dev/null
|
||||
rm /tmp/staging_dump.sql
|
||||
"
|
||||
|
||||
# Step 3: Verify
|
||||
log "[3/3] Verifying..."
|
||||
COUNTS=$(docker exec "${LOCAL_CONTAINER}" psql -U "${LOCAL_USER}" -d "${LOCAL_DB}" -t -c "
|
||||
SELECT json_build_object(
|
||||
'users', (SELECT count(*) FROM users),
|
||||
'companies', (SELECT count(*) FROM companies),
|
||||
'user_companies', (SELECT count(*) FROM user_companies)
|
||||
);")
|
||||
log " ${COUNTS}"
|
||||
|
||||
# Cleanup and stamp
|
||||
rm -f "${DUMP_FILE}"
|
||||
date '+%Y-%m-%d %H:%M:%S' > "$STAMP_FILE"
|
||||
|
||||
log "=== Sync complete ==="
|
||||
@ -543,7 +543,7 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="log-time">
|
||||
{{ log.created_at.strftime('%d.%m.%Y %H:%M') }}
|
||||
{{ log.created_at|local_time('%d.%m.%Y %H:%M') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@ -505,7 +505,7 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="log-time">
|
||||
{{ log.created_at.strftime('%d.%m.%Y %H:%M') }}
|
||||
{{ log.created_at|local_time('%d.%m.%Y %H:%M') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@ -268,7 +268,7 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ ann.author.name if ann.author else '-' }}</td>
|
||||
<td>{{ ann.created_at.strftime('%Y-%m-%d %H:%M') if ann.created_at else '-' }}</td>
|
||||
<td>{{ ann.created_at|local_time('%Y-%m-%d %H:%M') if ann.created_at else '-' }}</td>
|
||||
<td class="views-count">{{ ann.views_count or 0 }}</td>
|
||||
<td class="actions-cell">
|
||||
<a href="{{ url_for('admin.admin_announcements_edit', id=ann.id) }}" class="btn btn-secondary btn-small">
|
||||
|
||||
@ -160,7 +160,7 @@
|
||||
<div class="status-info">
|
||||
<strong>Status:</strong> {{ announcement.status_label }}
|
||||
{% if announcement.published_at %}
|
||||
| <strong>Opublikowano:</strong> {{ announcement.published_at.strftime('%Y-%m-%d %H:%M') }}
|
||||
| <strong>Opublikowano:</strong> {{ announcement.published_at|local_time('%Y-%m-%d %H:%M') }}
|
||||
{% endif %}
|
||||
{% if announcement.views_count %}
|
||||
| <strong>Wyswietlenia:</strong> {{ announcement.views_count }}
|
||||
@ -242,7 +242,7 @@
|
||||
<div class="form-group">
|
||||
<label for="expires_at">Data wygasniecia</label>
|
||||
<input type="datetime-local" id="expires_at" name="expires_at"
|
||||
value="{{ announcement.expires_at.strftime('%Y-%m-%dT%H:%M') if announcement and announcement.expires_at else '' }}">
|
||||
value="{{ announcement.expires_at|local_time('%Y-%m-%dT%H:%M') if announcement and announcement.expires_at else '' }}">
|
||||
<p class="form-hint">Pozostaw puste aby nie wygasalo</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -95,7 +95,7 @@
|
||||
<tbody>
|
||||
{% for click in clicks %}
|
||||
<tr>
|
||||
<td>{{ click.clicked_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||
<td>{{ click.clicked_at|local_time('%Y-%m-%d %H:%M') }}</td>
|
||||
<td>
|
||||
{% if click.user %}
|
||||
{{ click.user.email }}
|
||||
|
||||
@ -680,7 +680,7 @@
|
||||
<div class="stat-label">Użytkownicy</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="font-size: var(--font-size-xl);">{{ company.created_at.strftime('%d.%m.%Y') if company.created_at else '---' }}</div>
|
||||
<div class="stat-value" style="font-size: var(--font-size-xl);">{{ company.created_at|local_time('%d.%m.%Y') if company.created_at else '---' }}</div>
|
||||
<div class="stat-label">Utworzono</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -798,7 +798,7 @@
|
||||
<div class="action-status">
|
||||
{% if enrichment.registry.done %}
|
||||
<span class="status-dot green"></span>
|
||||
Wykonano{% if enrichment.registry.date %} {{ enrichment.registry.date.strftime('%d.%m.%Y') }}{% endif %}
|
||||
Wykonano{% if enrichment.registry.date %} {{ enrichment.registry.date|local_time('%d.%m.%Y') }}{% endif %}
|
||||
{% else %}
|
||||
<span class="status-dot gray"></span>
|
||||
Nie wykonano
|
||||
@ -828,7 +828,7 @@
|
||||
<div class="action-status">
|
||||
{% if enrichment.seo.done %}
|
||||
<span class="status-dot green"></span>
|
||||
Wykonano{% if enrichment.seo.date %} {{ enrichment.seo.date.strftime('%d.%m.%Y') }}{% endif %}
|
||||
Wykonano{% if enrichment.seo.date %} {{ enrichment.seo.date|local_time('%d.%m.%Y') }}{% endif %}
|
||||
{% else %}
|
||||
<span class="status-dot gray"></span>
|
||||
Nie wykonano
|
||||
@ -875,7 +875,7 @@
|
||||
<div class="action-status">
|
||||
{% if enrichment.gbp.done %}
|
||||
<span class="status-dot green"></span>
|
||||
Wykonano{% if enrichment.gbp.date %} {{ enrichment.gbp.date.strftime('%d.%m.%Y') }}{% endif %}
|
||||
Wykonano{% if enrichment.gbp.date %} {{ enrichment.gbp.date|local_time('%d.%m.%Y') }}{% endif %}
|
||||
{% else %}
|
||||
<span class="status-dot gray"></span>
|
||||
Nie wykonano
|
||||
|
||||
@ -270,7 +270,7 @@
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Data:</span>
|
||||
{{ req.created_at.strftime('%Y-%m-%d %H:%M') if req.created_at else '-' }}
|
||||
{{ req.created_at|local_time('%Y-%m-%d %H:%M') if req.created_at else '-' }}
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Źródło:</span>
|
||||
|
||||
@ -580,7 +580,7 @@
|
||||
<p>Przegląd kompletności i jakości danych {{ total }} firm w katalogu</p>
|
||||
</div>
|
||||
<div class="dq-timestamp">
|
||||
Stan na {{ now.strftime('%d.%m.%Y, %H:%M') }}
|
||||
Stan na {{ now|local_time('%d.%m.%Y, %H:%M') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -450,9 +450,9 @@
|
||||
<td style="text-align:center;">
|
||||
{% if cf.reminder %}
|
||||
{% if cf.reminder.is_read %}
|
||||
<span style="color:var(--success);font-size:16px;cursor:default;" title="Odczytano {{ cf.reminder.read_at.strftime('%d.%m %H:%M') if cf.reminder.read_at else '' }}. Wysłano {{ cf.reminder.sent_at.strftime('%d.%m %H:%M') }}">✓</span>
|
||||
<span style="color:var(--success);font-size:16px;cursor:default;" title="Odczytano {{ cf.reminder.read_at|local_time('%d.%m %H:%M') if cf.reminder.read_at else '' }}. Wysłano {{ cf.reminder.sent_at|local_time('%d.%m %H:%M') }}">✓</span>
|
||||
{% else %}
|
||||
<span style="color:var(--text-secondary);font-size:16px;cursor:default;" title="Wysłano {{ cf.reminder.sent_at.strftime('%d.%m.%Y %H:%M') }}, jeszcze nie odczytano">✉</span>
|
||||
<span style="color:var(--text-secondary);font-size:16px;cursor:default;" title="Wysłano {{ cf.reminder.sent_at|local_time('%d.%m.%Y %H:%M') }}, jeszcze nie odczytano">✉</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
@ -587,7 +587,7 @@
|
||||
{{ status_labels.get(topic.status, 'Nowy') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="topic-meta">{{ topic.created_at.strftime('%d.%m.%Y') }}</td>
|
||||
<td class="topic-meta">{{ topic.created_at|local_time('%d.%m.%Y') }}</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn-icon {% if topic.is_pinned %}active{% endif %}"
|
||||
@ -652,7 +652,7 @@
|
||||
<div class="reply-meta">
|
||||
{{ reply.author.name or reply.author.email.split('@')[0] }}
|
||||
w temacie <a href="{{ url_for('forum_topic', topic_id=reply.topic_id) }}">{{ reply.topic.title[:30] }}{% if reply.topic.title|length > 30 %}...{% endif %}</a>
|
||||
• {{ reply.created_at.strftime('%d.%m.%Y %H:%M') }}
|
||||
• {{ reply.created_at|local_time('%d.%m.%Y %H:%M') }}
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-icon danger"
|
||||
|
||||
@ -386,7 +386,7 @@
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ topic.author.name or topic.author.email.split('@')[0] }}</td>
|
||||
<td>{{ topic.created_at.strftime('%d.%m.%Y %H:%M') }}</td>
|
||||
<td>{{ topic.created_at|local_time('%d.%m.%Y %H:%M') }}</td>
|
||||
<td>{{ topic.days_waiting }} dni</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@ -123,7 +123,7 @@
|
||||
<strong>{{ topic.title }}</strong>
|
||||
<div class="deleted-meta">
|
||||
Autor: {{ topic.author.name or topic.author.email.split('@')[0] }}
|
||||
• Utworzono: {{ topic.created_at.strftime('%d.%m.%Y %H:%M') }}
|
||||
• Utworzono: {{ topic.created_at|local_time('%d.%m.%Y %H:%M') }}
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-restore" onclick="restoreTopic({{ topic.id }})">
|
||||
@ -134,7 +134,7 @@
|
||||
{{ topic.content[:300] }}{% if topic.content|length > 300 %}...{% endif %}
|
||||
</div>
|
||||
<div class="deleted-info">
|
||||
Usunięto: {{ topic.deleted_at.strftime('%d.%m.%Y %H:%M') if topic.deleted_at else 'brak daty' }}
|
||||
Usunięto: {{ topic.deleted_at|local_time('%d.%m.%Y %H:%M') if topic.deleted_at else 'brak daty' }}
|
||||
{% if topic.deleter %}przez {{ topic.deleter.name or topic.deleter.email.split('@')[0] }}{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@ -157,7 +157,7 @@
|
||||
<div class="deleted-meta">
|
||||
W temacie: <a href="{{ url_for('forum_topic', topic_id=reply.topic_id) }}" target="_blank">{{ reply.topic.title }}</a>
|
||||
<br>Autor: {{ reply.author.name or reply.author.email.split('@')[0] }}
|
||||
• Utworzono: {{ reply.created_at.strftime('%d.%m.%Y %H:%M') }}
|
||||
• Utworzono: {{ reply.created_at|local_time('%d.%m.%Y %H:%M') }}
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-restore" onclick="restoreReply({{ reply.id }})">
|
||||
@ -168,7 +168,7 @@
|
||||
{{ reply.content[:300] }}{% if reply.content|length > 300 %}...{% endif %}
|
||||
</div>
|
||||
<div class="deleted-info">
|
||||
Usunięto: {{ reply.deleted_at.strftime('%d.%m.%Y %H:%M') if reply.deleted_at else 'brak daty' }}
|
||||
Usunięto: {{ reply.deleted_at|local_time('%d.%m.%Y %H:%M') if reply.deleted_at else 'brak daty' }}
|
||||
{% if reply.deleter %}przez {{ reply.deleter.name or reply.deleter.email.split('@')[0] }}{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -200,7 +200,7 @@
|
||||
<span class="report-reason reason-{{ report.reason }}">{{ reason_labels.get(report.reason, report.reason) }}</span>
|
||||
<span class="report-meta">
|
||||
Zgłoszone przez {{ report.reporter.name or report.reporter.email.split('@')[0] }}
|
||||
• {{ report.created_at.strftime('%d.%m.%Y %H:%M') }}
|
||||
• {{ report.created_at|local_time('%d.%m.%Y %H:%M') }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="report-meta">
|
||||
@ -242,7 +242,7 @@
|
||||
<div class="report-meta">
|
||||
{% if report.reviewed_by %}
|
||||
Rozpatrzone przez {{ report.reviewer.name or report.reviewer.email.split('@')[0] }}
|
||||
• {{ report.reviewed_at.strftime('%d.%m.%Y %H:%M') }}
|
||||
• {{ report.reviewed_at|local_time('%d.%m.%Y %H:%M') }}
|
||||
{% endif %}
|
||||
{% if report.review_note %}
|
||||
<br>Notatka: {{ report.review_note }}
|
||||
|
||||
@ -696,8 +696,8 @@
|
||||
<td class="date-cell">
|
||||
{% if company.audit_date %}
|
||||
{% set days_ago = (now - company.audit_date).days %}
|
||||
<span class="{{ 'date-old' if days_ago > 30 else '' }}" title="{{ company.audit_date.strftime('%Y-%m-%d %H:%M') }}">
|
||||
{{ company.audit_date.strftime('%d.%m.%Y') }}
|
||||
<span class="{{ 'date-old' if days_ago > 30 else '' }}" title="{{ company.audit_date|local_time('%Y-%m-%d %H:%M') }}">
|
||||
{{ company.audit_date|local_time('%d.%m.%Y') }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="date-never">Nigdy</span>
|
||||
|
||||
@ -354,7 +354,7 @@
|
||||
Health Check Dashboard
|
||||
</h1>
|
||||
<div class="health-meta">
|
||||
<span>Ostatnia aktualizacja: <strong id="last-update">{{ generated_at.strftime('%H:%M:%S') }}</strong></span>
|
||||
<span>Ostatnia aktualizacja: <strong id="last-update">{{ generated_at|local_time('%H:%M:%S') }}</strong></span>
|
||||
<span>Auto-refresh: <strong id="countdown">2:00</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -662,7 +662,7 @@
|
||||
data-zarzad="{{ company.people_count|default(0, true) }}"
|
||||
data-pkd="{{ company.pkd_count|default(0, true) }}"
|
||||
data-status="{{ 'audited' if company.krs_last_audit_at else 'pending' }}"
|
||||
data-date="{{ company.krs_last_audit_at.strftime('%Y%m%d') if company.krs_last_audit_at else '00000000' }}">
|
||||
data-date="{{ company.krs_last_audit_at|local_time('%Y%m%d') if company.krs_last_audit_at else '00000000' }}">
|
||||
<td class="company-name-cell">
|
||||
<a href="{{ url_for('company_detail', company_id=company.id) }}">{{ company.name }}</a>
|
||||
</td>
|
||||
@ -720,8 +720,8 @@
|
||||
</td>
|
||||
<td class="date-cell">
|
||||
{% if company.krs_last_audit_at %}
|
||||
<span title="{{ company.krs_last_audit_at.strftime('%Y-%m-%d %H:%M') }}">
|
||||
{{ company.krs_last_audit_at.strftime('%d.%m.%Y') }}
|
||||
<span title="{{ company.krs_last_audit_at|local_time('%Y-%m-%d %H:%M') }}">
|
||||
{{ company.krs_last_audit_at|local_time('%d.%m.%Y') }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="date-never">Nigdy</span>
|
||||
|
||||
@ -258,8 +258,8 @@
|
||||
</td>
|
||||
<td>
|
||||
{% if app.submitted_at %}
|
||||
{{ app.submitted_at.strftime('%Y-%m-%d') }}
|
||||
<br><small class="text-muted">{{ app.submitted_at.strftime('%H:%M') }}</small>
|
||||
{{ app.submitted_at|local_time('%Y-%m-%d') }}
|
||||
<br><small class="text-muted">{{ app.submitted_at|local_time('%H:%M') }}</small>
|
||||
{% else %}
|
||||
<span class="text-muted">-</span>
|
||||
{% endif %}
|
||||
|
||||
@ -729,7 +729,7 @@
|
||||
<div class="print-header">
|
||||
<h2>Izba Gospodarcza Norda Biznes</h2>
|
||||
<p>Deklaracja członkowska nr {{ application.id }} — {{ application.company_name }}</p>
|
||||
<p>Data złożenia: {{ application.submitted_at.strftime('%d.%m.%Y, %H:%M') if application.submitted_at else 'brak' }} | Status: {{ application.status_label }}</p>
|
||||
<p>Data złożenia: {{ application.submitted_at|local_time('%d.%m.%Y, %H:%M') if application.submitted_at else 'brak' }} | Status: {{ application.status_label }}</p>
|
||||
</div>
|
||||
|
||||
<a href="{{ url_for('admin.admin_membership') }}" class="back-link">
|
||||
@ -944,7 +944,7 @@
|
||||
<div class="data-value">
|
||||
{% if application.declaration_accepted %}
|
||||
✅ Zaakceptowane
|
||||
<br><small>{{ application.declaration_accepted_at.strftime('%Y-%m-%d %H:%M') if application.declaration_accepted_at else '' }}</small>
|
||||
<br><small>{{ application.declaration_accepted_at|local_time('%Y-%m-%d %H:%M') if application.declaration_accepted_at else '' }}</small>
|
||||
<br><small>IP: {{ application.declaration_ip_address or '-' }}</small>
|
||||
{% else %}
|
||||
❌ Nie zaakceptowane
|
||||
@ -1071,7 +1071,7 @@
|
||||
<strong>Oczekuje na akceptację użytkownika</strong>
|
||||
<br>Zaproponowano zmiany danych z rejestru. Użytkownik musi je zaakceptować lub odrzucić.
|
||||
{% if application.proposed_changes_at %}
|
||||
<br><small>Zaproponowano: {{ application.proposed_changes_at.strftime('%Y-%m-%d %H:%M') }}</small>
|
||||
<br><small>Zaproponowano: {{ application.proposed_changes_at|local_time('%Y-%m-%d %H:%M') }}</small>
|
||||
{% endif %}
|
||||
{% if application.proposed_changes_comment %}
|
||||
<br><small>Komentarz: {{ application.proposed_changes_comment }}</small>
|
||||
@ -1119,7 +1119,7 @@
|
||||
</div>
|
||||
<div class="tracker-label">{{ step.label }}</div>
|
||||
{% if step.date %}
|
||||
<div class="tracker-date">{{ step.date.strftime('%d.%m.%Y') }}<br>{{ step.date.strftime('%H:%M') }}</div>
|
||||
<div class="tracker-date">{{ step.date|local_time('%d.%m.%Y') }}<br>{{ step.date|local_time('%H:%M') }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -1162,7 +1162,7 @@
|
||||
<div class="history-dot dot-green"></div>
|
||||
<div class="history-content">
|
||||
<div class="history-title">Deklaracja zatwierdzona</div>
|
||||
<div class="history-meta">{{ application.updated_at.strftime('%d.%m.%Y %H:%M') }}</div>
|
||||
<div class="history-meta">{{ application.updated_at|local_time('%d.%m.%Y %H:%M') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -1171,7 +1171,7 @@
|
||||
<div class="history-dot dot-yellow"></div>
|
||||
<div class="history-content">
|
||||
<div class="history-title">Rozpoczęto rozpatrywanie</div>
|
||||
<div class="history-meta">{{ application.reviewed_at.strftime('%d.%m.%Y %H:%M') }}</div>
|
||||
<div class="history-meta">{{ application.reviewed_at|local_time('%d.%m.%Y %H:%M') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -1180,7 +1180,7 @@
|
||||
<div class="history-dot dot-blue"></div>
|
||||
<div class="history-content">
|
||||
<div class="history-title">Deklaracja złożona</div>
|
||||
<div class="history-meta">{{ application.submitted_at.strftime('%d.%m.%Y %H:%M') }}{% if application.user %} · {{ application.user.name }}{% endif %}</div>
|
||||
<div class="history-meta">{{ application.submitted_at|local_time('%d.%m.%Y %H:%M') }}{% if application.user %} · {{ application.user.name }}{% endif %}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -1226,11 +1226,11 @@
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">Data zgłoszenia</div>
|
||||
<div class="data-value">{{ application.created_at.strftime('%Y-%m-%d %H:%M') if application.created_at else '-' }}</div>
|
||||
<div class="data-value">{{ application.created_at|local_time('%Y-%m-%d %H:%M') if application.created_at else '-' }}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">Data wysłania</div>
|
||||
<div class="data-value">{{ application.submitted_at.strftime('%Y-%m-%d %H:%M') if application.submitted_at else '-' }}</div>
|
||||
<div class="data-value">{{ application.submitted_at|local_time('%Y-%m-%d %H:%M') if application.submitted_at else '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -211,7 +211,7 @@
|
||||
<p class="subtitle">Deklaracja Przystąpienia do Izby nr {{ app.id }}</p>
|
||||
<p class="company-name">{{ app.company_name }}</p>
|
||||
<p class="meta">
|
||||
Data złożenia: {{ app.submitted_at.strftime('%d.%m.%Y, godz. %H:%M') if app.submitted_at else 'brak' }}
|
||||
Data złożenia: {{ app.submitted_at|local_time('%d.%m.%Y, godz. %H:%M') if app.submitted_at else 'brak' }}
|
||||
• Status: {{ app.status_label }}
|
||||
</p>
|
||||
</div>
|
||||
@ -320,7 +320,7 @@
|
||||
</div>
|
||||
<div class="date">
|
||||
{% if app.declaration_accepted_at %}
|
||||
{{ app.declaration_accepted_at.strftime('%d.%m.%Y, %H:%M') }}
|
||||
{{ app.declaration_accepted_at|local_time('%d.%m.%Y, %H:%M') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -284,7 +284,7 @@
|
||||
|
||||
<!-- Latest audit header -->
|
||||
<h2 style="font-size: var(--font-size-lg); margin-bottom: var(--spacing-md); color: var(--text-secondary);">
|
||||
Ostatni audyt: {{ latest.audited_at.strftime('%d.%m.%Y %H:%M') }}
|
||||
Ostatni audyt: {{ latest.audited_at|local_time('%d.%m.%Y %H:%M') }}
|
||||
{% if latest.notes %}<span style="font-size: var(--font-size-sm); font-weight: 400;"> — {{ latest.notes }}</span>{% endif %}
|
||||
</h2>
|
||||
|
||||
@ -452,7 +452,7 @@
|
||||
{% set f_cit = f.get('citations', []) %}
|
||||
{% set f_cit_found = f_cit|selectattr('status', 'equalto', 'found')|list|length if f_cit else 0 %}
|
||||
<tr>
|
||||
<td>{{ a.audited_at.strftime('%d.%m %H:%M') }}</td>
|
||||
<td>{{ a.audited_at|local_time('%d.%m %H:%M') }}</td>
|
||||
|
||||
{# PageSpeed scores #}
|
||||
{% macro std(val, good=90, ok=50) %}<td>{% if val is not none %}<span class="score-badge {{ 'good' if val >= good else ('ok' if val >= ok else 'bad') }}">{{ val }}</span>{% else %}—{% endif %}</td>{% endmacro %}
|
||||
|
||||
@ -63,7 +63,7 @@
|
||||
</a>
|
||||
<h1 style="margin-top: var(--spacing-sm);">Audyt SEO #{{ audit.id }}</h1>
|
||||
<p style="color: var(--text-secondary);">
|
||||
{{ audit.url }} — {{ audit.audited_at.strftime('%d.%m.%Y %H:%M') }}
|
||||
{{ audit.url }} — {{ audit.audited_at|local_time('%d.%m.%Y %H:%M') }}
|
||||
{% if audit.notes %} — {{ audit.notes }}{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -415,7 +415,7 @@
|
||||
{{ rec.recommendation_text }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="recommendation-meta">{{ rec.created_at.strftime('%d.%m.%Y') }}</td>
|
||||
<td class="recommendation-meta">{{ rec.created_at|local_time('%d.%m.%Y') }}</td>
|
||||
<td>
|
||||
{% if rec.status == 'pending' %}
|
||||
<span class="badge badge-pending">Oczekuje</span>
|
||||
|
||||
@ -494,7 +494,7 @@
|
||||
<div class="section-header">
|
||||
<h2>🌍 Statystyki GeoIP Blocking</h2>
|
||||
<div style="text-align: right; font-size: var(--font-size-xs); color: var(--text-secondary);">
|
||||
<div>Ostatnia aktualizacja: <span id="geoip-timestamp" style="font-family: monospace; font-weight: 500;">{{ generated_at.strftime('%H:%M:%S') }}</span></div>
|
||||
<div>Ostatnia aktualizacja: <span id="geoip-timestamp" style="font-family: monospace; font-weight: 500;">{{ generated_at|local_time('%H:%M:%S') }}</span></div>
|
||||
<div style="margin-top: 4px;">
|
||||
<span style="display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: #22c55e; margin-right: 4px; animation: pulse 2s infinite;"></span>
|
||||
Auto-refresh: <span id="geoip-countdown">5:00</span>
|
||||
@ -579,7 +579,7 @@
|
||||
<td><span class="ip-address">{{ alert.ip_address or '-' }}</span></td>
|
||||
<td>{{ alert.user_email or '-' }}</td>
|
||||
<td><span class="badge badge-{{ alert.status }}">{{ alert.status }}</span></td>
|
||||
<td><span class="timestamp">{{ alert.created_at.strftime('%Y-%m-%d %H:%M') }}</span></td>
|
||||
<td><span class="timestamp">{{ alert.created_at|local_time('%Y-%m-%d %H:%M') }}</span></td>
|
||||
<td>
|
||||
{% if alert.status == 'new' %}
|
||||
<div class="action-buttons">
|
||||
@ -642,7 +642,7 @@
|
||||
{% if log.entity_name %}<br><small>{{ log.entity_name }}</small>{% endif %}
|
||||
</td>
|
||||
<td><span class="ip-address">{{ log.ip_address or '-' }}</span></td>
|
||||
<td><span class="timestamp">{{ log.created_at.strftime('%Y-%m-%d %H:%M') }}</span></td>
|
||||
<td><span class="timestamp">{{ log.created_at|local_time('%Y-%m-%d %H:%M') }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@ -677,7 +677,7 @@
|
||||
<tr class="locked-row">
|
||||
<td>{{ user.email }}</td>
|
||||
<td>{{ user.failed_login_attempts }}</td>
|
||||
<td><span class="timestamp">{{ user.locked_until.strftime('%Y-%m-%d %H:%M') }}</span></td>
|
||||
<td><span class="timestamp">{{ user.locked_until|local_time('%Y-%m-%d %H:%M') }}</span></td>
|
||||
<td>
|
||||
<form method="POST" action="{{ url_for('admin.unlock_account', user_id=user.id) }}" style="display:inline;">
|
||||
{{ csrf_token() }}
|
||||
|
||||
@ -249,7 +249,7 @@
|
||||
|
||||
{% if analysis and analysis.seo_audited_at %}
|
||||
<div class="audit-info">
|
||||
<span>Ostatni audyt: <strong>{{ analysis.seo_audited_at.strftime('%d.%m.%Y %H:%M') }}</strong></span>
|
||||
<span>Ostatni audyt: <strong>{{ analysis.seo_audited_at|local_time('%d.%m.%Y %H:%M') }}</strong></span>
|
||||
{% if analysis.cms_detected %}<span>CMS: <strong>{{ analysis.cms_detected }}</strong></span>{% endif %}
|
||||
{% if analysis.hosting_provider %}<span>Hosting: <strong>{{ analysis.hosting_provider }}</strong></span>{% endif %}
|
||||
{% if analysis.load_time_ms %}<span>Czas ladowania: <strong>{{ analysis.load_time_ms }}ms</strong></span>{% endif %}
|
||||
@ -1124,7 +1124,7 @@
|
||||
{% if analysis.ssl_expires_at %}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Certyfikat SSL wygasa</span>
|
||||
<span class="detail-value">{{ analysis.ssl_expires_at.strftime('%d.%m.%Y') }}</span>
|
||||
<span class="detail-value">{{ analysis.ssl_expires_at|local_time('%d.%m.%Y') }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if analysis.ssl_issuer %}
|
||||
@ -1196,7 +1196,7 @@
|
||||
{% if analysis.last_modified_at %}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Ostatnia modyfikacja strony</span>
|
||||
<span class="detail-value">{{ analysis.last_modified_at.strftime('%d.%m.%Y %H:%M') }}</span>
|
||||
<span class="detail-value">{{ analysis.last_modified_at|local_time('%d.%m.%Y %H:%M') }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -1214,7 +1214,7 @@
|
||||
{% if analysis.analyzed_at %}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Data audytu</span>
|
||||
<span class="detail-value">{{ analysis.analyzed_at.strftime('%d.%m.%Y %H:%M') }}</span>
|
||||
<span class="detail-value">{{ analysis.analyzed_at|local_time('%d.%m.%Y %H:%M') }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if analysis.audit_source %}
|
||||
|
||||
@ -907,8 +907,8 @@
|
||||
<td class="date-cell hide-mobile">
|
||||
{% if company.last_verified %}
|
||||
{% set days_ago = (now - company.last_verified).days %}
|
||||
<span class="{{ 'date-old' if days_ago > 30 else '' }}" title="{{ company.last_verified.strftime('%Y-%m-%d %H:%M') }}">
|
||||
{{ company.last_verified.strftime('%d.%m.%Y') }}
|
||||
<span class="{{ 'date-old' if days_ago > 30 else '' }}" title="{{ company.last_verified|local_time('%Y-%m-%d %H:%M') }}">
|
||||
{{ company.last_verified|local_time('%d.%m.%Y') }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="date-never">-</span>
|
||||
|
||||
@ -955,13 +955,13 @@
|
||||
{% if p.verified_at %}
|
||||
<div class="provenance-detail">
|
||||
<span class="label">Zweryfikowano:</span>
|
||||
{{ p.verified_at.strftime('%d.%m.%Y %H:%M') }}
|
||||
{{ p.verified_at|local_time('%d.%m.%Y %H:%M') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if p.last_checked_at %}
|
||||
<div class="provenance-detail">
|
||||
<span class="label">Ostatni check:</span>
|
||||
{{ p.last_checked_at.strftime('%d.%m.%Y %H:%M') }}
|
||||
{{ p.last_checked_at|local_time('%d.%m.%Y %H:%M') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="provenance-detail">
|
||||
@ -1017,7 +1017,7 @@
|
||||
<a href="{{ url_for('admin.social_publisher_company_settings', company_id=company.id) }}">Wybierz stronę →</a>
|
||||
{% endif %}
|
||||
{% if p.oauth_last_sync %}
|
||||
<span style="margin-left: auto; color: var(--text-secondary);">Sync: {{ p.oauth_last_sync.strftime('%d.%m.%Y') }}</span>
|
||||
<span style="margin-left: auto; color: var(--text-secondary);">Sync: {{ p.oauth_last_sync|local_time('%d.%m.%Y') }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif p.oauth_connected and p.oauth_expired %}
|
||||
|
||||
@ -453,7 +453,7 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if fb.last_checked_at %}
|
||||
<div style="margin-top: var(--spacing-sm); font-size: 11px; opacity: 0.6;">Ostatnia synchronizacja: {{ fb.last_checked_at.strftime('%d.%m.%Y %H:%M') }}</div>
|
||||
<div style="margin-top: var(--spacing-sm); font-size: 11px; opacity: 0.6;">Ostatnia synchronizacja: {{ fb.last_checked_at|local_time('%d.%m.%Y %H:%M') }}</div>
|
||||
{% endif %}
|
||||
<div style="margin-top: var(--spacing-sm); font-size: 11px; opacity: 0.5;">
|
||||
Publikowanie nie działa? Zmiana hasła FB lub usunięcie aplikacji wymaga ponownego połączenia w
|
||||
@ -582,11 +582,11 @@
|
||||
</td>
|
||||
<td style="white-space: nowrap; font-size: var(--font-size-sm); color: var(--text-secondary);">
|
||||
{% if post.published_at %}
|
||||
{{ post.published_at.strftime('%Y-%m-%d %H:%M') }}
|
||||
{{ post.published_at|local_time('%Y-%m-%d %H:%M') }}
|
||||
{% elif post.scheduled_at %}
|
||||
{{ post.scheduled_at.strftime('%Y-%m-%d %H:%M') }}
|
||||
{{ post.scheduled_at|local_time('%Y-%m-%d %H:%M') }}
|
||||
{% else %}
|
||||
{{ post.created_at.strftime('%Y-%m-%d %H:%M') if post.created_at else '-' }}
|
||||
{{ post.created_at|local_time('%Y-%m-%d %H:%M') if post.created_at else '-' }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="engagement-cell">
|
||||
@ -638,7 +638,7 @@
|
||||
<h3>Posty z Facebooka
|
||||
{% if cached_fb_posts.get(company_id_key) %}
|
||||
<span style="font-size: var(--font-size-xs); color: var(--text-secondary); font-weight: 400;">
|
||||
({{ cached_fb_posts[company_id_key].total_count }} postów, cache z {{ cached_fb_posts[company_id_key].cached_at.strftime('%d.%m %H:%M') }})
|
||||
({{ cached_fb_posts[company_id_key].total_count }} postów, cache z {{ cached_fb_posts[company_id_key].cached_at|local_time('%d.%m %H:%M') }})
|
||||
</span>
|
||||
{% endif %}
|
||||
</h3>
|
||||
|
||||
@ -220,7 +220,7 @@
|
||||
| Status: {% if config.is_active %}<span class="status-active">Aktywna</span>{% else %}Nieaktywna{% endif %}
|
||||
| Debug: {% if config.debug_mode %}<span style="color: var(--warning); font-weight: 600;">Wlaczony</span>{% else %}Wylaczony{% endif %}
|
||||
{% if config.updated_at %}
|
||||
| Ostatnia aktualizacja: {{ config.updated_at.strftime('%Y-%m-%d %H:%M') }}
|
||||
| Ostatnia aktualizacja: {{ config.updated_at|local_time('%Y-%m-%d %H:%M') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@ -492,7 +492,7 @@
|
||||
| <strong>Model AI:</strong> {{ post.ai_model }}
|
||||
{% endif %}
|
||||
{% if post.published_at %}
|
||||
| <strong>Opublikowano:</strong> {{ post.published_at.strftime('%Y-%m-%d %H:%M') }}
|
||||
| <strong>Opublikowano:</strong> {{ post.published_at|local_time('%Y-%m-%d %H:%M') }}
|
||||
{% endif %}
|
||||
{% if post.status == 'published' and post.meta_post_id %}
|
||||
|
|
||||
|
||||
@ -370,8 +370,8 @@
|
||||
</div>
|
||||
<div class="refresh-info">
|
||||
<div class="label">Ostatnia aktualizacja</div>
|
||||
<div class="timestamp" id="last-update">{{ generated_at.strftime('%H:%M:%S') }}</div>
|
||||
<div class="label" style="margin-top: var(--spacing-xs);">{{ generated_at.strftime('%d.%m.%Y') }}</div>
|
||||
<div class="timestamp" id="last-update">{{ generated_at|local_time('%H:%M:%S') }}</div>
|
||||
<div class="label" style="margin-top: var(--spacing-xs);">{{ generated_at|local_time('%d.%m.%Y') }}</div>
|
||||
<div class="next-refresh">
|
||||
<span class="refresh-indicator"></span>
|
||||
Auto-refresh: <span id="countdown">5:00</span>
|
||||
|
||||
@ -380,7 +380,7 @@
|
||||
</div>
|
||||
<div class="refresh-info">
|
||||
<div class="label">Ostatnia aktualizacja</div>
|
||||
<div class="timestamp" id="refresh-time">{{ now.strftime('%H:%M:%S') if now is defined else '--:--:--' }}</div>
|
||||
<div class="timestamp" id="refresh-time">{{ now|local_time('%H:%M:%S') if now is defined else '--:--:--' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -334,7 +334,7 @@
|
||||
{% for s in recent_sessions %}
|
||||
<tr data-user="{{ s.user_name }}" data-device="{{ s.device_type }}" data-browser="{{ s.browser }}{{ ' (PWA)' if s.is_pwa }}">
|
||||
<td>{{ s.user_name }}</td>
|
||||
<td data-sort-value="{{ s.started_at.strftime('%Y%m%d%H%M') if s.started_at else '0' }}">{{ s.started_at.strftime('%d.%m.%Y %H:%M') if s.started_at else '-' }}</td>
|
||||
<td data-sort-value="{{ s.started_at|local_time('%Y%m%d%H%M') if s.started_at else '0' }}">{{ s.started_at|local_time('%d.%m.%Y %H:%M') if s.started_at else '-' }}</td>
|
||||
<td>
|
||||
{% set dt = s.device_type|lower %}
|
||||
<span class="device-badge device-{{ dt if dt in ['desktop','mobile','tablet'] else 'other' }}">
|
||||
@ -386,7 +386,7 @@
|
||||
<td class="num">{{ u.session_count }}</td>
|
||||
<td class="num">{{ u.total_time_min }}</td>
|
||||
<td class="num">{{ u.total_pages }}</td>
|
||||
<td data-sort-value="{{ u.last_login.strftime('%Y%m%d%H%M') if u.last_login else '0' }}">{{ u.last_login.strftime('%d.%m.%Y %H:%M') if u.last_login else '-' }}</td>
|
||||
<td data-sort-value="{{ u.last_login|local_time('%Y%m%d%H%M') if u.last_login else '0' }}">{{ u.last_login|local_time('%d.%m.%Y %H:%M') if u.last_login else '-' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@ -911,7 +911,7 @@
|
||||
{% if msg.feedback_rating == 2 %}Pomocne{% else %}Do poprawy{% endif %}
|
||||
</span>
|
||||
<span style="font-size: var(--font-size-xs); color: var(--text-muted);">
|
||||
{{ msg.feedback_at.strftime('%d.%m %H:%M') if msg.feedback_at else '' }}
|
||||
{{ msg.feedback_at|local_time('%d.%m %H:%M') if msg.feedback_at else '' }}
|
||||
</span>
|
||||
</div>
|
||||
<div style="font-size: var(--font-size-sm); color: var(--text-secondary); max-height: 50px; overflow: hidden;">
|
||||
|
||||
@ -156,11 +156,11 @@
|
||||
</div>
|
||||
<div class="profile-meta-item">
|
||||
<span class="profile-meta-label">Rejestracja</span>
|
||||
<span class="profile-meta-value">{{ user.created_at.strftime('%d.%m.%Y') if user.created_at else 'N/A' }}</span>
|
||||
<span class="profile-meta-value">{{ user.created_at|local_time('%d.%m.%Y') if user.created_at else 'N/A' }}</span>
|
||||
</div>
|
||||
<div class="profile-meta-item">
|
||||
<span class="profile-meta-label">Ostatni login</span>
|
||||
<span class="profile-meta-value">{{ user.last_login.strftime('%d.%m.%Y %H:%M') if user.last_login else 'Nigdy' }}</span>
|
||||
<span class="profile-meta-value">{{ user.last_login|local_time('%d.%m.%Y %H:%M') if user.last_login else 'Nigdy' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -221,7 +221,7 @@
|
||||
{% if resolution.first_symptom %}
|
||||
<div class="resolution-detail-item">
|
||||
<div class="resolution-detail-label">Pierwszy objaw</div>
|
||||
<div class="resolution-detail-value">{{ resolution.first_symptom.strftime('%d.%m.%Y %H:%M') }}</div>
|
||||
<div class="resolution-detail-value">{{ resolution.first_symptom|local_time('%d.%m.%Y %H:%M') }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="resolution-detail-item">
|
||||
@ -231,7 +231,7 @@
|
||||
{% if resolution.last_reset %}
|
||||
<div class="resolution-detail-item">
|
||||
<div class="resolution-detail-label">Ostatni reset</div>
|
||||
<div class="resolution-detail-value">{{ resolution.last_reset.strftime('%d.%m.%Y %H:%M') }}</div>
|
||||
<div class="resolution-detail-value">{{ resolution.last_reset|local_time('%d.%m.%Y %H:%M') }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="resolution-detail-item">
|
||||
@ -275,7 +275,7 @@
|
||||
<div class="timeline-body">
|
||||
<div class="timeline-desc {% if event.css == 'danger' %}css-danger{% elif event.css == 'warning' %}css-warning{% elif event.css == 'success' %}css-success{% endif %}">{{ event.desc }}</div>
|
||||
{% if event.detail %}<div class="timeline-detail">{{ event.detail }}</div>{% endif %}
|
||||
<div class="timeline-time">{{ event.time.strftime('%d.%m.%Y %H:%M') }}</div>
|
||||
<div class="timeline-time">{{ event.time|local_time('%d.%m.%Y %H:%M') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@ -361,7 +361,7 @@
|
||||
<tr>
|
||||
<td style="max-width: 300px; word-break: break-all; font-size: var(--font-size-xs);">{{ e.message[:100] }}</td>
|
||||
<td style="font-size: var(--font-size-xs);">{{ e.url[:50] if e.url else 'N/A' }}</td>
|
||||
<td style="white-space: nowrap;">{{ e.occurred_at.strftime('%d.%m %H:%M') }}</td>
|
||||
<td style="white-space: nowrap;">{{ e.occurred_at|local_time('%d.%m %H:%M') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@ -382,7 +382,7 @@
|
||||
<tr>
|
||||
<td style="font-family: monospace; font-size: var(--font-size-xs);">{{ p.path }}</td>
|
||||
<td style="color: var(--error); font-weight: 600;">{{ p.load_time_ms }}ms</td>
|
||||
<td style="white-space: nowrap;">{{ p.viewed_at.strftime('%d.%m %H:%M') }}</td>
|
||||
<td style="white-space: nowrap;">{{ p.viewed_at|local_time('%d.%m %H:%M') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@ -409,7 +409,7 @@
|
||||
<div style="display: flex; flex-wrap: wrap; gap: var(--spacing-sm);">
|
||||
{% for s in search_queries %}
|
||||
<span style="padding: 4px 12px; background: var(--background); border-radius: var(--radius); font-size: var(--font-size-sm);">
|
||||
"{{ s.query }}" <span style="color: var(--text-muted); font-size: var(--font-size-xs);">{{ s.searched_at.strftime('%d.%m') }}</span>
|
||||
"{{ s.query }}" <span style="color: var(--text-muted); font-size: var(--font-size-xs);">{{ s.searched_at|local_time('%d.%m') }}</span>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@ -1243,8 +1243,8 @@
|
||||
</select>
|
||||
</td>
|
||||
<td style="font-size: var(--font-size-sm); color: var(--text-secondary);"
|
||||
data-sort-value="{{ user.created_at.strftime('%Y%m%d%H%M') }}">
|
||||
{{ user.created_at.strftime('%d.%m.%Y %H:%M') }}
|
||||
data-sort-value="{{ user.created_at|local_time('%Y%m%d%H%M') }}">
|
||||
{{ user.created_at|local_time }}
|
||||
{% if user.created_by_id and creators_map.get(user.created_by_id) %}
|
||||
<br><span style="font-size: 0.75rem; color: var(--text-muted, #9CA3AF);" title="Dodany przez {{ creators_map[user.created_by_id] }}">dodał: {{ creators_map[user.created_by_id] }}</span>
|
||||
{% else %}
|
||||
@ -1252,9 +1252,9 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="font-size: var(--font-size-sm); color: var(--text-secondary);"
|
||||
data-sort-value="{{ user.last_login.strftime('%Y%m%d%H%M') if user.last_login else '0' }}">
|
||||
data-sort-value="{{ user.last_login|local_time('%Y%m%d%H%M') if user.last_login else '0' }}">
|
||||
{% if user.last_login %}
|
||||
{{ user.last_login.strftime('%d.%m.%Y %H:%M') }}
|
||||
{{ user.last_login|local_time }}
|
||||
{% else %}
|
||||
<span style="color: var(--text-muted, #9CA3AF);">Nigdy</span>
|
||||
{% endif %}
|
||||
|
||||
@ -1639,7 +1639,7 @@
|
||||
{% endif %}
|
||||
<span>{{ news.source_name or news.source_domain or '-' }}</span>
|
||||
{% set news_year = news.published_at.year if news.published_at else None %}
|
||||
<span>{{ news.published_at.strftime('%d.%m.%Y') if news.published_at else (news.created_at.strftime('%d.%m.%Y') if news.created_at else '-') }}</span>
|
||||
<span>{{ news.published_at|local_time('%d.%m.%Y') if news.published_at else (news.created_at|local_time('%d.%m.%Y') if news.created_at else '-') }}</span>
|
||||
{% if news_year and news_year < min_year %}
|
||||
<span class="old-news-badge">⚠️ Sprzed {{ min_year }}</span>
|
||||
{% endif %}
|
||||
@ -1917,7 +1917,7 @@
|
||||
<div class="fetch-job">
|
||||
<div>
|
||||
<strong>{{ job.search_query }}</strong>
|
||||
<br><small class="text-muted">{{ job.created_at.strftime('%d.%m.%Y %H:%M') if job.created_at else '-' }}</small>
|
||||
<br><small class="text-muted">{{ job.created_at|local_time('%d.%m.%Y %H:%M') if job.created_at else '-' }}</small>
|
||||
</div>
|
||||
<div>
|
||||
Znaleziono: {{ job.results_found or 0 }} | Nowych: {{ job.results_new or 0 }}
|
||||
|
||||
@ -450,7 +450,7 @@
|
||||
{% if news.status == 'pending' %}Oczekuje{% elif news.status == 'approved' %}Zatwierdzony{% else %}Odrzucony{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ news.published_at.strftime('%d.%m.%Y') if news.published_at else (news.created_at.strftime('%d.%m.%Y') if news.created_at else '-') }}</td>
|
||||
<td>{{ news.published_at|local_time('%d.%m.%Y') if news.published_at else (news.created_at|local_time('%d.%m.%Y') if news.created_at else '-') }}</td>
|
||||
<td style="white-space: nowrap;">
|
||||
{% if news.status == 'pending' %}
|
||||
<button class="action-btn approve" onclick="approveNews({{ news.id }})">Zatwierdź</button>
|
||||
|
||||
@ -857,8 +857,8 @@
|
||||
<td class="date-cell">
|
||||
{% if company.seo_audited_at %}
|
||||
{% set days_ago = (now - company.seo_audited_at).days %}
|
||||
<span class="{{ 'date-old' if days_ago > 30 else '' }}" title="{{ company.seo_audited_at.strftime('%Y-%m-%d %H:%M') }}">
|
||||
{{ company.seo_audited_at.strftime('%d.%m.%Y') }}
|
||||
<span class="{{ 'date-old' if days_ago > 30 else '' }}" title="{{ company.seo_audited_at|local_time('%Y-%m-%d %H:%M') }}">
|
||||
{{ company.seo_audited_at|local_time('%d.%m.%Y') }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="date-never">Nigdy</span>
|
||||
|
||||
@ -386,7 +386,7 @@
|
||||
</span>
|
||||
{% endfor %}
|
||||
<span class="meta-date">
|
||||
{{ announcement.published_at.strftime('%d %B %Y') if announcement.published_at else '' }}
|
||||
{{ announcement.published_at|local_time('%d %B %Y') if announcement.published_at else '' }}
|
||||
</span>
|
||||
{% if announcement.author %}
|
||||
<span class="meta-author">
|
||||
@ -491,7 +491,7 @@
|
||||
{{ other.title }}
|
||||
</a>
|
||||
<div class="date">
|
||||
{{ other.published_at.strftime('%d.%m.%Y') if other.published_at else '' }}
|
||||
{{ other.published_at|local_time('%d.%m.%Y') if other.published_at else '' }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
@ -330,7 +330,7 @@
|
||||
|
||||
<div class="card-footer">
|
||||
<span class="card-date">
|
||||
{{ ann.published_at.strftime('%d.%m.%Y') if ann.published_at else '' }}
|
||||
{{ ann.published_at|local_time('%d.%m.%Y') if ann.published_at else '' }}
|
||||
</span>
|
||||
<a href="{{ url_for('announcement_detail', slug=ann.slug) }}" class="card-link">
|
||||
Czytaj wiecej →
|
||||
|
||||
@ -751,7 +751,7 @@
|
||||
<td style="font-size: var(--font-size-sm); color: var(--text-muted)">{{ doc.original_filename }}</td>
|
||||
<td style="font-size: var(--font-size-sm)">{{ doc.size_display }}</td>
|
||||
<td style="font-size: var(--font-size-sm); color: var(--text-muted)">
|
||||
{{ doc.uploaded_at.strftime('%d.%m.%Y') if doc.uploaded_at else '—' }}
|
||||
{{ doc.uploaded_at|local_time('%d.%m.%Y') if doc.uploaded_at else '—' }}
|
||||
{% if doc.uploader %}<br>{{ doc.uploader.name }}{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@ -307,7 +307,7 @@
|
||||
</div>
|
||||
<div class="classified-stats">
|
||||
<span>{{ classified.views_count }} wyswietl.</span>
|
||||
<span>{{ classified.created_at.strftime('%d.%m.%Y') }}</span>
|
||||
<span>{{ classified.created_at|local_time('%d.%m.%Y') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -746,9 +746,9 @@
|
||||
|
||||
<div class="stats-bar">
|
||||
<span>{{ classified.views_count }} wyswietlen</span>
|
||||
<span>Dodano: {{ classified.created_at.strftime('%d.%m.%Y %H:%M') }}</span>
|
||||
<span>Dodano: {{ classified.created_at|local_time('%d.%m.%Y %H:%M') }}</span>
|
||||
{% if classified.expires_at %}
|
||||
<span>Wygasa: {{ classified.expires_at.strftime('%d.%m.%Y') }}</span>
|
||||
<span>Wygasa: {{ classified.expires_at|local_time('%d.%m.%Y') }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -807,7 +807,7 @@
|
||||
{% if q.author.company %}<span style="color: var(--text-secondary); font-weight: normal;"> - {{ q.author.company.name }}</span>{% endif %}
|
||||
{% if not q.answer %}<span class="pending-badge">Oczekuje na odpowiedz</span>{% endif %}
|
||||
</div>
|
||||
<div class="question-date">{{ q.created_at.strftime('%d.%m.%Y %H:%M') }}</div>
|
||||
<div class="question-date">{{ q.created_at|local_time('%d.%m.%Y %H:%M') }}</div>
|
||||
</div>
|
||||
{% if classified.author_id == current_user.id %}
|
||||
<div class="question-actions">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user