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

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:
Maciej Pienczyn 2026-04-06 13:41:53 +02:00
parent 3df362f44e
commit 110d971dca
133 changed files with 4913 additions and 1439 deletions

View File

@ -2,7 +2,7 @@
## Status: WDROŻONE ✅ ## 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 ## Ukończone kroki
@ -16,12 +16,12 @@ Aplikacja Flask została pomyślnie wdrożona na NORDABIZ-01.
## Dostęp ## Dostęp
- **LAN:** http://10.22.68.249 - **LAN:** http://57.128.200.27
- **WAN:** https://nordabiznes.pl (maintenance mode w NPM) - **WAN:** https://nordabiznes.pl (maintenance mode w NPM)
## Ważne informacje ## 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) - **Baza:** PostgreSQL `nordabiz` (80 firm)
- **Port aplikacji:** 5000 - **Port aplikacji:** 5000
- **Venv:** /var/www/nordabiznes/venv/ - **Venv:** /var/www/nordabiznes/venv/

View File

@ -110,7 +110,7 @@ docker exec nordabiz-postgres psql -U nordabiz_app -d nordabiz -c "SELECT slug,
## Uwagi: ## Uwagi:
- DEV: PostgreSQL via Docker na localhost:5433 - 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` - Dla produkcji: po przetestowaniu lokalnie, wdróż przez `/deploy`
- Nowe firmy powinny pochodzić z oficjalnej listy członków Norda Biznes - Nowe firmy powinny pochodzić z oficjalnej listy członków Norda Biznes
- Zawsze weryfikuj NIP przed dodaniem - Zawsze weryfikuj NIP przed dodaniem

View File

@ -12,7 +12,7 @@ Opcjonalny argument określa typ backupu:
## System automatycznych backupów ## System automatycznych backupów
### Harmonogram (cron na NORDABIZ-01) ### Harmonogram (cron na OVH VPS inpi-vps-waw01)
| Typ | Częstotliwość | Godzina | Retencja | Lokalizacja | | Typ | Częstotliwość | Godzina | Retencja | Lokalizacja |
|-----|---------------|---------|----------|-------------| |-----|---------------|---------|----------|-------------|
@ -25,16 +25,16 @@ Opcjonalny argument określa typ backupu:
```bash ```bash
# Ostatnie backupy hourly # 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 # 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) # Sprawdź offsite (PBS)
ssh maciejpi@10.22.68.127 "ls -lt /backup/nordabiz/daily/ | head -5" ssh maciejpi@10.22.68.127 "ls -lt /backup/nordabiz/daily/ | head -5"
# Rozmiar backupów # 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: ## Kroki do wykonania:
@ -48,26 +48,26 @@ docker exec nordabiz-postgres pg_dump -U nordabiz_app nordabiz > "backups/dev_$(
### 2. Backup produkcyjnej bazy PostgreSQL ### 2. Backup produkcyjnej bazy PostgreSQL
Eksport do lokalnego: Eksport do lokalnego:
```bash ```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: Lub backup na serwerze:
```bash ```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 ### 3. Backup plików konfiguracyjnych
```bash ```bash
mkdir -p backups/config_$(date +%Y%m%d) mkdir -p backups/config_$(date +%Y%m%d)
cp .env backups/config_$(date +%Y%m%d)/dev.env 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@57.128.200.27 "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 /etc/nginx/sites-available/nordabiznes" > backups/config_$(date +%Y%m%d)/nginx.conf
``` ```
### 4. Snapshot VM w Proxmox ### 4. Snapshot VM w Proxmox
Użyj skill `proxmox-manager`: 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: Lub ręcznie:
@ -83,14 +83,14 @@ ls -la backups/*.sql 2>/dev/null
Snapshoty VM (użyj skill proxmox-manager): Snapshoty VM (użyj skill proxmox-manager):
``` ```
Pokaż snapshoty VM 249 Pokaż snapshoty OVH VPS
``` ```
### 6. Przywracanie z backupu ### 6. Przywracanie z backupu
#### Szybkie przywracanie (skrypt DR) #### Szybkie przywracanie (skrypt DR)
```bash ```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): #### DEV (Docker PostgreSQL):
@ -100,13 +100,13 @@ docker exec -i nordabiz-postgres psql -U nordabiz_app -d nordabiz < backups/dev_
#### PROD (PostgreSQL): #### PROD (PostgreSQL):
```bash ```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: #### Rollback VM:
Użyj skill `proxmox-manager`: Użyj skill `proxmox-manager`:
``` ```
Przywróć VM 249 ze snapshotu backup_YYYYMMDD Przywróć OVH VPS ze snapshotu backup_YYYYMMDD
``` ```
## Konfiguracja cron (na serwerze PROD) ## 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 - Snapshoty VM są najszybsze do rollbacku
- PostgreSQL dump jest przenośny między środowiskami - PostgreSQL dump jest przenośny między środowiskami
- DEV używa Docker PostgreSQL na localhost:5433 - 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 Data aktualizacji: 2026-02-02

View File

@ -99,4 +99,4 @@ Podsumuj w czytelnej formie:
- Monitoruj tokeny dla kontroli limitów - Monitoruj tokeny dla kontroli limitów
- Wysokie latency może wskazywać na problemy z API - Wysokie latency może wskazywać na problemy z API
- DEV: `docker exec nordabiz-postgres psql -U nordabiz_app -d nordabiz` - 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"`

View File

@ -127,4 +127,4 @@ Wygeneruj raport w formacie markdown z tabelami:
- Priorytet: email > telefon > www > opis - Priorytet: email > telefon > www > opis
- Weryfikuj dane przez oficjalne źródła (CEIDG, KRS) - Weryfikuj dane przez oficjalne źródła (CEIDG, KRS)
- DEV: `docker exec nordabiz-postgres psql -U nordabiz_app -d nordabiz` - 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"`

View File

@ -1,6 +1,6 @@
# Deploy NordaBiz to Production # 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: ## 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` - Sprawdź czy lokalna aplikacja działa: `curl http://localhost:5000/health` lub `curl http://localhost:5001/health`
### 2. Połączenie z serwerem ### 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` - Przejdź do katalogu: `cd /var/www/nordabiznes`
### 3. Deployment ### 3. Deployment

View File

@ -15,54 +15,54 @@ Opcjonalny argument określa typ logów lub liczbę linii, np.:
Połącz się z serwerem i pobierz logi: Połącz się z serwerem i pobierz logi:
```bash ```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: Dla więcej linii:
```bash ```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: Tylko błędy:
```bash ```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) ### 2. Logi Nginx (access)
```bash ```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) ### 3. Logi Nginx (error)
```bash ```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) ### 4. Logi w czasie rzeczywistym (follow)
```bash ```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ć) (Ctrl+C aby przerwać)
### 5. Logi z określonego czasu ### 5. Logi z określonego czasu
Ostatnia godzina: Ostatnia godzina:
```bash ```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: Dzisiaj:
```bash ```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 ### 6. Szukanie wzorca
```bash ```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 ### 7. Status usługi
```bash ```bash
ssh root@10.22.68.249 "systemctl status nordabiznes" ssh maciejpi@57.128.200.27 "systemctl status nordabiznes"
``` ```
## Analiza logów: ## Analiza logów:
@ -74,7 +74,7 @@ Po pobraniu logów przeanalizuj je pod kątem:
- Nieautoryzowanych prób dostępu - Nieautoryzowanych prób dostępu
## Uwagi: ## 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` - Usługa systemd: `nordabiznes`
- Logi rotują automatycznie - Logi rotują automatycznie
- Dla alertów rozważ integrację z Zabbix (skill: monitoring-manager) - Dla alertów rozważ integrację z Zabbix (skill: monitoring-manager)

View File

@ -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)"` - Test SSL: `curl -vI https://nordabiznes.pl 2>&1 | grep -E "(SSL|expire|subject)"`
### 3. Status VM (użyj skill proxmox-manager) ### 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 - Sprawdź: CPU, RAM, uptime, snapshoty
### 4. Statystyki bazy danych (DEV via Docker) ### 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) ### 5. Statystyki bazy danych (PROD)
```bash ```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 'Firmy: ' || COUNT(*) FROM companies;
SELECT 'Użytkownicy: ' || COUNT(*) FROM users; SELECT 'Użytkownicy: ' || COUNT(*) FROM users;
SELECT 'Konwersacje chat: ' || COUNT(*) FROM ai_chat_conversations; SELECT 'Konwersacje chat: ' || COUNT(*) FROM ai_chat_conversations;
@ -52,11 +52,11 @@ Podsumuj status w formie tabeli:
| Lokalna aplikacja | OK/ERROR | port | | Lokalna aplikacja | OK/ERROR | port |
| Docker PostgreSQL | OK/ERROR | localhost:5433 | | Docker PostgreSQL | OK/ERROR | localhost:5433 |
| Produkcja | OK/ERROR | response time | | 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 DEV | OK/ERROR | liczba rekordów |
| Baza danych PROD | OK/ERROR | liczba rekordów | | Baza danych PROD | OK/ERROR | liczba rekordów |
| Git | CLEAN/DIRTY | branch | | Git | CLEAN/DIRTY | branch |
## Uwagi: ## Uwagi:
- DEV: PostgreSQL via Docker na localhost:5433 - 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
View 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

View File

@ -168,7 +168,7 @@ Checked deployment architecture documentation against documented server IPs, por
| Item | Documented Value | Verified in Docs | Status | | 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 | | NORDABIZ-01 VM ID | 249 | ✅ Found | VERIFIED |
| R11-REVPROXY-01 IP | 10.22.68.250 | ✅ Found | VERIFIED | | R11-REVPROXY-01 IP | 10.22.68.250 | ✅ Found | VERIFIED |
| R11-REVPROXY-01 VM ID | 119 | ✅ Found | VERIFIED | | R11-REVPROXY-01 VM ID | 119 | ✅ Found | VERIFIED |

View File

@ -67,24 +67,23 @@ nordabiz/
- **Domena:** staging.nordabiznes.pl (NPM Proxy Host ID: 44) - **Domena:** staging.nordabiznes.pl (NPM Proxy Host ID: 44)
- **Weryfikacja:** `curl -I https://staging.nordabiznes.pl/health` - **Weryfikacja:** `curl -I https://staging.nordabiznes.pl/health`
### Production ### Production (OVH VPS)
- **Serwer:** NORDABIZ-01 (VM 249, IP 10.22.68.249) - **Serwer:** inpi-vps-waw01 (OVH VPS, IP 57.128.200.27)
- **Baza:** PostgreSQL na 10.22.68.249:5432 - **Baza:** PostgreSQL na 57.128.200.27:5432
- **Reverse Proxy:** NPM na R11-REVPROXY-01 (VM 119, IP 10.22.68.250) - **SSL/Proxy:** nginx na VPS (bezpośrednio, bez NPM)
- **Domena:** nordabiznes.pl (DNS w OVH, SSL Let's Encrypt) - **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 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.
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`
## Git & Deployment ## 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 # 1. DEV: Push do obu repozytoriów
git push origin master && git push inpi master 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" 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! # ⚠️ OBOWIĄZKOWO: Test manualny nowej funkcjonalności na staging!
# 3. PROD: Pull zmiany (DOPIERO PO WERYFIKACJI STAGING!) # 3. PROD (OVH VPS): Rsync zmienionych plików (DOPIERO PO WERYFIKACJI STAGING!)
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && sudo -u www-data git pull" rsync -avz -e ssh --rsync-path="sudo rsync" <files> maciejpi@57.128.200.27:/var/www/nordabiznes/
# 4. PROD: Migracje SQL (jeśli są) # 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 # 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 curl -sI https://nordabiznes.pl/health | head -3
``` ```
**⚠️ UWAGI KRYTYCZNE:** **⚠️ UWAGI KRYTYCZNE:**
1. **Migracje SQL** - NIE używaj `psql` bezpośrednio. Użyj `scripts/run_migration.py` 1. **Brak .git na VPS** - Deploy TYLKO przez rsync, NIE git pull
2. **Uprawnienia logów** - `sudo chown -R maciejpi:maciejpi /var/log/nordabiznes/` 2. **Migracje SQL** - Używaj `sudo -u postgres psql` (app user nie ma ALTER TABLE)
3. **502 po restarcie** - Poczekaj 3-5 sekund i sprawdź ponownie 3. **`.env` jest root-owned** - Skrypty wymagają sudo do odczytu
4. **Git pull** - Używaj `sudo -u www-data git pull` (www-data ma klucze SSH) 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>` 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!):** **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 ```bash
# ✅ PRAWIDŁOWO: # ✅ PRAWIDŁOWO:
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && \ ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && \
DATABASE_URL=\$(grep DATABASE_URL .env | cut -d'=' -f2) \ sudo DATABASE_URL=\$(sudo grep DATABASE_URL .env | cut -d'=' -f2) \
/var/www/nordabiznes/venv/bin/python3 skrypt.py" /var/www/nordabiznes/venv/bin/python3 skrypt.py"
# ❌ BŁĘDNIE: # ❌ 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 ## Konwencje danych
@ -157,8 +157,10 @@ ssh maciejpi@10.22.68.249 "source .env && python3 skrypt.py"
### Deployment ### Deployment
- Przed wdrożeniem: `python -m py_compile app.py` - Przed wdrożeniem: `python -m py_compile app.py`
- SSH: `ssh maciejpi@10.22.68.249` (ZAWSZE jako maciejpi!) - SSH prod: `ssh maciejpi@57.128.200.27` (OVH VPS, ZAWSZE jako maciejpi!)
- Ścieżka: `/var/www/nordabiznes` | Restart: `sudo systemctl reload nordabiznes` - 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 - **ZAWSZE** aktualizuj `release_notes` w app.py
### Szablony Jinja2 - WAŻNE! ### Szablony Jinja2 - WAŻNE!

View File

@ -68,9 +68,9 @@ grep -r "NordaBiz2025Secure" --include="*.txt" .
``` ```
./view_maturity_results.sh:# export PGPASSWORD='your_database_password' ./view_maturity_results.sh:# export PGPASSWORD='your_database_password'
./view_maturity_results.sh: echo " 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@57.128.200.27 "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@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 \"
``` ```
**Analysis:** **Analysis:**

View File

@ -100,14 +100,14 @@ graph LR
**Issue:** Line break example showed only a single node without connections: **Issue:** Line break example showed only a single node without connections:
```mermaid ```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: **Fix:** Added proper graph structure with connections:
```mermaid ```mermaid
graph TD graph TD
A[Flask App<br/>10.22.68.249<br/>Port 5000] A[Flask App<br/>57.128.200.27<br/>Port 5000]
B[PostgreSQL<br/>10.22.68.249<br/>Port 5432] B[PostgreSQL<br/>57.128.200.27<br/>Port 5432]
A --> B A --> B
``` ```
@ -188,7 +188,7 @@ Spot-checked key diagrams for accuracy:
- ✅ Identifies one-to-many, many-to-many, one-to-one - ✅ Identifies one-to-many, many-to-many, one-to-one
4. **Network Topology** (`07-network-topology.md`) 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) - ✅ Correct ports (5000 for Flask, 5432 for PostgreSQL)
- ✅ Shows Fortigate NAT configuration - ✅ Shows Fortigate NAT configuration

View File

@ -271,8 +271,8 @@ Norda Biznes Partner is a **Flask-powered web platform** built with PostgreSQL,
#### 🌍 Multi-Environment Deployment #### 🌍 Multi-Environment Deployment
- **Development:** PostgreSQL via Docker (localhost:5433) - **Development:** PostgreSQL via Docker (localhost:5433)
- **Production:** NORDABIZ-01 server (VM 249, 10.22.68.249) - **Production:** OVH VPS (57.128.200.27, inpi-vps-waw01)
- **Reverse proxy:** NPM on R11-REVPROXY-01 - **Staging reverse proxy:** NPM on R11-REVPROXY-01
- **SSL/TLS:** Let's Encrypt with auto-renewal - **SSL/TLS:** Let's Encrypt with auto-renewal
- **Domain:** nordabiznes.pl (DNS in OVH) - **Domain:** nordabiznes.pl (DNS in OVH)
- **WSGI:** Gunicorn production server - **WSGI:** Gunicorn production server
@ -859,9 +859,9 @@ The application is live in production at **https://nordabiznes.pl** since Novemb
| Component | Details | | Component | Details |
|-----------|---------| |-----------|---------|
| **Server** | NORDABIZ-01 (VM 249, IP: 10.22.68.249) | | **Server** | OVH VPS inpi-vps-waw01 (IP: 57.128.200.27) |
| **Database** | PostgreSQL 15 on 10.22.68.249:5432 | | **Database** | PostgreSQL 15 on 57.128.200.27 (localhost:5432) |
| **Reverse Proxy** | Nginx Proxy Manager on R11-REVPROXY-01 (VM 119, IP: 10.22.68.250) | | **Reverse Proxy** | Not used for production (direct DNS). NPM (10.22.68.250) serves staging only |
| **Domain** | nordabiznes.pl (DNS managed via OVH) | | **Domain** | nordabiznes.pl (DNS managed via OVH) |
| **SSL/TLS** | Let's Encrypt with automatic renewal | | **SSL/TLS** | Let's Encrypt with automatic renewal |
| **WSGI Server** | Gunicorn | | **WSGI Server** | Gunicorn |
@ -876,19 +876,23 @@ The project maintains two Git remotes for redundancy and deployment:
| Remote | Repository | Purpose | | Remote | Repository | Purpose |
|--------|------------|---------| |--------|------------|---------|
| **origin** (GitHub) | `git@github.com:pienczyn/nordabiz.git` | Cloud backup, public access, CI/CD ready | | **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) **Gitea Access:** https://10.22.68.180:3000/ (requires HTTPS)
### Deployment Workflow ### Deployment Workflow
``` ```
┌─────────────┐ git push ┌─────────────┐ git pull ┌─────────────┐ ┌─────────────┐ git push ┌─────────────┐
│ Development │ ────────────► │ Gitea │ ◄──────────── │ Production │ │ Development │ ────────────► │ GitHub │ (cloud backup)
│ (Local) │ │ (INPI) │ │ Server │ │ (Local) │ └─────────────┘
└─────────────┘ └─────────────┘ └─────────────┘ └──────┬──────┘
│ rsync
└──── git push ────► GitHub (cloud backup)
┌─────────────┐
│ OVH VPS │ (57.128.200.27)
│ Production │
└─────────────┘
``` ```
#### Deployment Steps #### Deployment Steps
@ -896,24 +900,19 @@ The project maintains two Git remotes for redundancy and deployment:
**1. Push Changes from Development:** **1. Push Changes from Development:**
```bash ```bash
# Push to both remotes (from local development) # Push to GitHub (and optionally Gitea)
git push origin master && git push inpi master git push origin master && git push inpi master
``` ```
**2. Deploy to Production:** **2. Deploy to Production (via rsync — no .git on server):**
```bash ```bash
# SSH to production server # Rsync to production server (from local dev machine)
ssh maciejpi@10.22.68.249 rsync -avz --exclude='.git' --exclude='venv' --exclude='.env' \
./ maciejpi@57.128.200.27:/var/www/nordabiznes/
# Navigate to application directory # SSH to restart
cd /var/www/nordabiznes ssh maciejpi@57.128.200.27 "sudo systemctl reload nordabiznes"
# Pull latest changes (as www-data user)
sudo -u www-data git pull
# Restart the application service
sudo systemctl restart nordabiznes
# Verify deployment # Verify deployment
curl -I https://nordabiznes.pl/health curl -I https://nordabiznes.pl/health
@ -934,13 +933,9 @@ Before deploying to production, always:
### Production Configuration ### Production Configuration
**Critical NPM Proxy Configuration:** **Network Configuration:**
``` 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).
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.
**Environment Variables:** **Environment Variables:**
@ -973,7 +968,7 @@ sudo systemctl start nordabiznes
**Database Backup:** **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:** **Health Check:**
@ -987,7 +982,7 @@ The application provides a health endpoint for monitoring:
```bash ```bash
# Always SSH as maciejpi user (NOT root!) # Always SSH as maciejpi user (NOT root!)
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
``` ```
**Test Accounts:** **Test Accounts:**
@ -1401,11 +1396,9 @@ python verify_all_companies_data.py # Check data quality
# Development: Push to both remotes # Development: Push to both remotes
git push origin master && git push inpi master git push origin master && git push inpi master
# Production: Deploy to server # Production: Deploy to server (rsync, no .git on server)
ssh maciejpi@10.22.68.249 rsync -avz --exclude='.git' --exclude='venv' --exclude='.env' ./ maciejpi@57.128.200.27:/var/www/nordabiznes/
cd /var/www/nordabiznes ssh maciejpi@57.128.200.27 "sudo systemctl reload nordabiznes"
sudo -u www-data git pull
sudo systemctl restart nordabiznes
curl -I https://nordabiznes.pl/health # Verify deployment curl -I https://nordabiznes.pl/health # Verify deployment
``` ```
@ -1661,8 +1654,8 @@ python run_ai_quality_tests.py -v
**Problem:** nordabiznes.pl not loading **Problem:** nordabiznes.pl not loading
```bash ```bash
# SSH to production server # SSH to production server (OVH VPS)
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
# Check service status # Check service status
sudo systemctl status nordabiznes sudo systemctl status nordabiznes
@ -1720,12 +1713,8 @@ sudo systemctl restart postgresql
**Solution:** **Solution:**
```bash ```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 as maciejpi (NOT root!)
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
``` ```
### API & External Service Issues ### API & External Service Issues
@ -1847,8 +1836,7 @@ Additional documentation and resources for developers.
### Production Environment ### Production Environment
- **Server:** NORDABIZ-01 (VM 249, IP: 10.22.68.249) - **Server:** OVH VPS inpi-vps-waw01 (IP: 57.128.200.27)
- **Reverse Proxy:** R11-REVPROXY-01 (VM 119, IP: 10.22.68.250)
- **URL:** https://nordabiznes.pl - **URL:** https://nordabiznes.pl
- **Status:** LIVE since 2025-11-23 - **Status:** LIVE since 2025-11-23

17
app.py
View File

@ -218,6 +218,19 @@ app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
# Template filters # 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') @app.template_filter('ensure_url')
def ensure_url_filter(url): def ensure_url_filter(url):
"""Ensure URL has http:// or https:// scheme""" """Ensure URL has http:// or https:// scheme"""
@ -1436,7 +1449,7 @@ def send_error_notification(error, request_info):
{traceback_str} {traceback_str}
{'='*50} {'='*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> body_html = f"""<!DOCTYPE html>
@ -1466,7 +1479,7 @@ def send_error_notification(error, request_info):
</div> </div>
<div style="margin-top: 20px; padding: 15px; background: #1e3a5f; border-radius: 8px;"> <div style="margin-top: 20px; padding: 15px; background: #1e3a5f; border-radius: 8px;">
<div style="color: #60a5fa;">🔧 <strong>Sprawdź logi:</strong></div> <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> </div>
</div> </div>

View File

@ -443,7 +443,7 @@ def admin_status():
# ===== SERVERS PING ===== # ===== SERVERS PING =====
servers_status = [] servers_status = []
servers_to_ping = [ servers_to_ping = [
('NORDABIZ-01', '10.22.68.249'), ('NORDABIZ-01', '57.128.200.27'),
('R11-REVPROXY-01', '10.22.68.250'), ('R11-REVPROXY-01', '10.22.68.250'),
('R11-DNS-01', '10.22.68.171'), ('R11-DNS-01', '10.22.68.171'),
('R11-GIT-INPI', '10.22.68.180'), ('R11-GIT-INPI', '10.22.68.180'),
@ -507,7 +507,7 @@ def admin_status():
{'name': 'systemd', 'version': '255', 'icon': '⚙️', 'category': 'Service Manager'}, {'name': 'systemd', 'version': '255', 'icon': '⚙️', 'category': 'Service Manager'},
], ],
'servers': [ '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-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-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)'}, {'name': 'R11-GIT-INPI', 'ip': '10.22.68.180', 'icon': '📦', 'role': 'Git Server (VM 180)'},

View File

@ -12,19 +12,19 @@ database/
--- ---
## 🚀 Instalacja na NORDABIZ-01 ## 🚀 Instalacja na OVH VPS (inpi-vps-waw01)
### Krok 1: Instalacja PostgreSQL ### Krok 1: Instalacja PostgreSQL
```bash ```bash
# SSH do serwera # SSH do serwera
ssh root@10.22.68.249 ssh maciejpi@57.128.200.27
# Aktualizacja pakietów # Aktualizacja pakietów
apt update sudo apt update
# Instalacja PostgreSQL 15 # Instalacja PostgreSQL 15
apt install -y postgresql-15 postgresql-contrib-15 sudo apt install -y postgresql-15 postgresql-contrib-15
# Sprawdzenie statusu # Sprawdzenie statusu
systemctl status postgresql systemctl status postgresql

View File

@ -1,8 +1,8 @@
# Norda Biznes - Deployment Checklist # Norda Biznes - Deployment Checklist
**Version:** 1.0 **Version:** 1.0
**Last Updated:** 2026-01-02 **Last Updated:** 2026-04-04
**Environment:** Production (NORDABIZ-01, IP: 10.22.68.249) **Environment:** Production (OVH VPS inpi-vps-waw01, IP: 57.128.200.27)
**Audience:** DevOps, SysAdmins **Audience:** DevOps, SysAdmins
--- ---
@ -75,23 +75,23 @@ This checklist ensures safe, repeatable deployments to production with minimal r
- [ ] Secrets stored securely (LastPass, 1Password, vault) - [ ] Secrets stored securely (LastPass, 1Password, vault)
### Access & Permissions ### Access & Permissions
- [ ] SSH access to NORDABIZ-01 verified - [ ] SSH access to OVH VPS verified
```bash ```bash
ssh maciejpi@10.22.68.249 "echo OK" ssh maciejpi@57.128.200.27 "echo OK"
``` ```
- [ ] PostgreSQL credentials verified (not displayed) - [ ] PostgreSQL credentials verified (not displayed)
```bash ```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 ```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 Location
- [ ] Backup destination has adequate free space - [ ] Backup destination has adequate free space
```bash ```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 # Minimum 2GB free recommended
``` ```
- [ ] Backup location is accessible and writable - [ ] Backup location is accessible and writable
@ -103,12 +103,12 @@ This checklist ensures safe, repeatable deployments to production with minimal r
### Application Status ### Application Status
- [ ] Current application is running and healthy - [ ] Current application is running and healthy
```bash ```bash
ssh maciejpi@10.22.68.249 "sudo systemctl status nordabiznes" ssh maciejpi@57.128.200.27 "sudo systemctl status nordabiznes"
# Status: active (running) # Status: active (running)
``` ```
- [ ] Application logs show no recent errors - [ ] Application logs show no recent errors
```bash ```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 # Should be empty or only non-critical errors
``` ```
- [ ] Health check endpoint responding - [ ] Health check endpoint responding
@ -120,25 +120,25 @@ This checklist ensures safe, repeatable deployments to production with minimal r
### Database Status ### Database Status
- [ ] PostgreSQL is running - [ ] PostgreSQL is running
```bash ```bash
ssh maciejpi@10.22.68.249 "sudo systemctl status postgresql" ssh maciejpi@57.128.200.27 "sudo systemctl status postgresql"
# Status: active (running) # Status: active (running)
``` ```
- [ ] Database is accessible - [ ] Database is accessible
```bash ```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 - [ ] No long-running transactions
```bash ```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 SELECT pid, usename, state, query
FROM pg_stat_activity FROM pg_stat_activity
WHERE state != 'idle' AND duration > interval '5 minutes';" WHERE state != 'idle' AND duration > interval '5 minutes';\""
# Should be empty # Should be empty
``` ```
- [ ] Database size recorded - [ ] Database size recorded
```bash ```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 pg_size_pretty(pg_database_size('nordabiz'));" SELECT pg_size_pretty(pg_database_size('nordabiz'));\""
# Record this value # 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) - Best deployment time: off-peak (11:00-12:00, 14:00-17:00)
- [ ] No ongoing data imports or batch jobs - [ ] No ongoing data imports or batch jobs
```bash ```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 # Should be empty
``` ```
@ -165,7 +165,7 @@ This checklist ensures safe, repeatable deployments to production with minimal r
- [ ] Full database backup - [ ] Full database backup
```bash ```bash
BACKUP_FILE="$HOME/backup_before_deployment_$(date +%Y%m%d_%H%M%S).sql" 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 # Verify backup was created
ls -lh "$BACKUP_FILE" ls -lh "$BACKUP_FILE"
# Minimum size: >5MB (should contain all schema and data) # 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) - [ ] Backup can be restored (test on separate database)
```bash ```bash
# Optional: Create test database and restore # Optional: Create test database and restore
psql -h 10.22.68.249 -U nordabiz_app -c "CREATE DATABASE nordabiz_test;" ssh maciejpi@57.128.200.27 "sudo -u postgres psql -c 'CREATE DATABASE nordabiz_test;'"
psql -h 10.22.68.249 -U nordabiz_app -d nordabiz_test < "$BACKUP_FILE" ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz_test" < "$BACKUP_FILE"
psql -h 10.22.68.249 -U nordabiz_app -d nordabiz_test -c "SELECT COUNT(*) FROM companies;" ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz_test -c 'SELECT COUNT(*) FROM companies;'"
# Then drop test database: DROP DATABASE nordabiz_test; # Then drop test database: sudo -u postgres psql -c "DROP DATABASE nordabiz_test;"
``` ```
- [ ] Backup copied to redundant location - [ ] Backup copied to redundant location
```bash ```bash
@ -254,15 +254,15 @@ This checklist ensures safe, repeatable deployments to production with minimal r
### Pre-SQL Execution ### Pre-SQL Execution
- [ ] Maintenance mode enabled (optional but recommended) - [ ] Maintenance mode enabled (optional but recommended)
```bash ```bash
ssh maciejpi@10.22.68.249 " ssh maciejpi@57.128.200.27 "
# Temporarily disable non-critical endpoints # Temporarily disable non-critical endpoints
# Or show 'maintenance' page # Or show 'maintenance' page
" "
``` ```
- [ ] Current user count recorded - [ ] Current user count recorded
```bash ```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 COUNT(DISTINCT session_key) FROM django_session WHERE expire_date > NOW();" SELECT COUNT(DISTINCT session_key) FROM django_session WHERE expire_date > NOW();\""
# Current active users: _______ # 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: **IMPORTANT:** Execute SQL scripts in this exact order within a transaction:
```bash ```bash
ssh maciejpi@10.22.68.249 << 'DEPLOY_EOF' ssh maciejpi@57.128.200.27 << 'DEPLOY_EOF'
# Start deployment # Start deployment
echo "=== DEPLOYMENT STARTED at $(date) ===" 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 # Step 1: Full backup BEFORE any changes
echo "STEP 1: Creating backup..." 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" echo "✓ Backup: $BACKUP_FILE"
# Step 2: Begin transaction (all SQL changes in one transaction) # Step 2: Begin transaction (all SQL changes in one transaction)
echo "STEP 2: Executing SQL migrations..." echo "STEP 2: Executing SQL migrations..."
# Execute schema migrations (in order of dependency) # 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; BEGIN;
-- 2.1 News tables migration (if not already applied) -- 2.1 News tables migration (if not already applied)
@ -314,7 +314,7 @@ echo "✓ SQL migrations completed"
# Step 3: Verify data integrity # Step 3: Verify data integrity
echo "STEP 3: Verifying 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 -- Check for orphaned foreign keys
SELECT 'Checking foreign key integrity...' AS status; SELECT 'Checking foreign key integrity...' AS status;
@ -327,7 +327,7 @@ SQL
# Step 4: Update indexes and statistics # Step 4: Update indexes and statistics
echo "STEP 4: Optimizing database..." 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 -- Update statistics for query planner
ANALYZE; ANALYZE;
@ -341,14 +341,14 @@ echo "✓ Database optimized"
echo "STEP 5: Deploying application..." echo "STEP 5: Deploying application..."
cd /var/www/nordabiznes cd /var/www/nordabiznes
# Pull latest code (if using git) # Deploy via rsync (no .git on OVH VPS)
sudo -u www-data git pull origin master # Run from LOCAL machine: rsync -avz --exclude='.git' --exclude='.env' --exclude='__pycache__' ./ maciejpi@57.128.200.27:/var/www/nordabiznes/
# Update dependencies # 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 # 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" echo "✓ Application files updated"
@ -363,7 +363,7 @@ if sudo systemctl is-active --quiet nordabiznes; then
else else
echo "✗ ERROR: Application failed to start" echo "✗ ERROR: Application failed to start"
echo "ROLLING BACK DATABASE..." 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 exit 1
fi fi
@ -401,15 +401,15 @@ If using separate SSH sessions, execute in this order:
```bash ```bash
# Session 1: Create backup # 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" 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" echo "Backup saved to: $BACKUP_FILE"
exit exit
# Session 2: Execute SQL # Session 2: Execute SQL
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
sudo -u www-data psql -U nordabiz_app -d nordabiz << 'EOF' sudo -u postgres psql -d nordabiz << 'EOF'
BEGIN; BEGIN;
\i /var/www/nordabiznes/database/migrate_news_tables.sql \i /var/www/nordabiznes/database/migrate_news_tables.sql
-- ... additional SQL ... -- ... additional SQL ...
@ -417,8 +417,8 @@ COMMIT;
EOF EOF
# Session 3: Validate # Session 3: Validate
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
sudo -u www-data psql -U nordabiz_app -d nordabiz -c "SELECT COUNT(*) FROM company_news;" sudo -u postgres psql -d nordabiz -c "SELECT COUNT(*) FROM company_news;"
exit exit
``` ```
@ -430,15 +430,15 @@ exit
``` ```
- [ ] Database size within expected range - [ ] Database size within expected range
```bash ```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 pg_size_pretty(pg_database_size('nordabiz'));" SELECT pg_size_pretty(pg_database_size('nordabiz'));\""
# Compare to pre-deployment size (should be similar ±10%) # Compare to pre-deployment size (should be similar ±10%)
``` ```
- [ ] New tables/columns exist (if schema changes) - [ ] New tables/columns exist (if schema changes)
```bash ```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 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 ## Phase 5: Application Deployment
### Code Deployment ### Code Deployment
- [ ] Application code pulled from Git - [ ] Application code deployed via rsync (no .git on OVH VPS)
```bash ```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 - [ ] Python dependencies installed
```bash ```bash
ssh maciejpi@10.22.68.249 " ssh maciejpi@57.128.200.27 "
sudo -u www-data /var/www/nordabiznes/venv/bin/pip install -q -r /var/www/nordabiznes/requirements.txt sudo /var/www/nordabiznes/venv/bin/pip install -q -r /var/www/nordabiznes/requirements.txt
" "
``` ```
- [ ] Application syntax validated - [ ] Application syntax validated
```bash ```bash
ssh maciejpi@10.22.68.249 " ssh maciejpi@57.128.200.27 "
sudo -u www-data /var/www/nordabiznes/venv/bin/python -m py_compile /var/www/nordabiznes/app.py /var/www/nordabiznes/venv/bin/python -m py_compile /var/www/nordabiznes/app.py
echo $? # Should return 0 (success) echo $? # Should return 0 (success)
" "
``` ```
@ -467,16 +469,16 @@ exit
### Service Restart ### Service Restart
- [ ] Application service restarted - [ ] Application service restarted
```bash ```bash
ssh maciejpi@10.22.69.249 "sudo systemctl restart nordabiznes" ssh maciejpi@57.128.200.27 "sudo systemctl restart nordabiznes"
``` ```
- [ ] Service started successfully - [ ] Service started successfully
```bash ```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 # Expected: active
``` ```
- [ ] Service status verified - [ ] Service status verified
```bash ```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 ### Initial Health Checks
@ -491,7 +493,7 @@ exit
``` ```
- [ ] No critical errors in logs - [ ] No critical errors in logs
```bash ```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' sudo tail -30 /var/log/nordabiznes/app.log | grep -i 'ERROR\|CRITICAL'
" "
# Should be empty or only non-critical warnings # Should be empty or only non-critical warnings
@ -521,16 +523,16 @@ exit
### Database Queries ### Database Queries
- [ ] New tables accessible - [ ] New tables accessible
```bash ```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 company_news LIMIT 1;
SELECT * FROM user_notifications LIMIT 1;" SELECT * FROM user_notifications LIMIT 1;'"
``` ```
- [ ] Search indexes working - [ ] Search indexes working
```bash ```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 EXPLAIN ANALYZE
SELECT * FROM companies SELECT * FROM companies
WHERE name ILIKE '%pixlab%' LIMIT 10;" WHERE name ILIKE '%pixlab%' LIMIT 10;\""
# Should show "Index Scan" (not "Seq Scan") # Should show "Index Scan" (not "Seq Scan")
``` ```
@ -542,8 +544,8 @@ exit
``` ```
- [ ] Database query response time acceptable - [ ] Database query response time acceptable
```bash ```bash
time psql -h 10.22.68.249 -U nordabiz_app -d nordabiz -c " ssh maciejpi@57.128.200.27 "time sudo -u postgres psql -d nordabiz -c '
SELECT * FROM companies WHERE category_id = 1 LIMIT 50;" SELECT * FROM companies WHERE category_id = 1 LIMIT 50;'"
# real time should be < 100ms # real time should be < 100ms
``` ```
- [ ] API endpoints respond within SLA - [ ] API endpoints respond within SLA
@ -578,22 +580,22 @@ exit
### Post-Deployment Monitoring (2 hours) ### Post-Deployment Monitoring (2 hours)
- [ ] Monitor application logs for errors - [ ] Monitor application logs for errors
```bash ```bash
ssh maciejpi@10.22.68.249 " ssh maciejpi@57.128.200.27 "
tail -f /var/log/nordabiznes/app.log tail -f /var/log/nordabiznes/app.log
" "
# Watch for ERROR, CRITICAL, EXCEPTION # Watch for ERROR, CRITICAL, EXCEPTION
``` ```
- [ ] Monitor database load - [ ] Monitor database load
```bash ```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 SELECT pid, usename, state, query
FROM pg_stat_activity FROM pg_stat_activity
WHERE datname = 'nordabiz' AND state != 'idle';" WHERE datname = 'nordabiz' AND state != 'idle';\""
# Should be minimal # Should be minimal
``` ```
- [ ] Monitor system resources - [ ] Monitor system resources
```bash ```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 ### 24-Hour Follow-up
@ -635,13 +637,13 @@ echo ""
# Step 1: Stop application # Step 1: Stop application
echo "STEP 1: Stopping 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 sleep 3
# Step 2: Restore database # Step 2: Restore database
echo "STEP 2: Restoring database from backup..." echo "STEP 2: Restoring database from backup..."
ssh maciejpi@10.22.68.249 " ssh maciejpi@57.128.200.27 "
sudo -u www-data psql -U nordabiz_app -d nordabiz << 'SQL' sudo -u postgres psql -d nordabiz << 'SQL'
-- Drop all changes -- Drop all changes
DROP TABLE IF EXISTS company_news CASCADE; DROP TABLE IF EXISTS company_news CASCADE;
DROP TABLE IF EXISTS user_notifications CASCADE; DROP TABLE IF EXISTS user_notifications CASCADE;
@ -649,7 +651,7 @@ DROP TABLE IF EXISTS user_notifications CASCADE;
SQL SQL
# Restore from backup # 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 if [ $? -ne 0 ]; then
@ -662,11 +664,8 @@ echo "✓ Database restored"
# Step 3: Restart application (previous version) # Step 3: Restart application (previous version)
echo "STEP 3: Restarting application..." echo "STEP 3: Restarting application..."
ssh maciejpi@10.22.68.249 " # Re-deploy previous version via rsync from local backup, then:
cd /var/www/nordabiznes ssh maciejpi@57.128.200.27 "sudo systemctl start nordabiznes"
sudo -u www-data git checkout HEAD~1 # Revert to previous commit
sudo systemctl start nordabiznes
"
sleep 3 sleep 3
@ -711,44 +710,44 @@ echo "4. Schedule post-mortem review"
curl -s https://nordabiznes.pl/health | jq . curl -s https://nordabiznes.pl/health | jq .
# Database connection # 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 # Service status
ssh maciejpi@10.22.68.249 "sudo systemctl status nordabiznes" ssh maciejpi@57.128.200.27 "sudo systemctl status nordabiznes"
# Log tailing # 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 # 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 SELECT
schemaname, schemaname,
tablename, tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
FROM pg_tables 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 ### Monitoring Queries
```bash ```bash
# Active connections # 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(*) SELECT datname, usename, count(*)
FROM pg_stat_activity FROM pg_stat_activity
GROUP BY datname, usename;" GROUP BY datname, usename;'"
# Long-running queries # 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 SELECT pid, usename, query, query_start
FROM pg_stat_activity FROM pg_stat_activity
WHERE query != 'autovacuum' WHERE query != 'autovacuum'
AND query_start < NOW() - interval '5 minutes';" AND query_start < NOW() - interval '5 minutes';\""
# Index usage # 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 SELECT schemaname, tablename, indexname, idx_scan
FROM pg_stat_user_indexes 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:** **Solution:**
1. Check logs: `sudo journalctl -xe -u nordabiznes | tail -50` 1. Check logs: `sudo journalctl -xe -u nordabiznes | tail -50`
2. Check syntax: `python -m py_compile /var/www/nordabiznes/app.py` 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 4. If database is issue, execute rollback script
5. If code is issue, revert Git commit and restart 5. If code is issue, revert Git commit and restart
@ -834,11 +833,12 @@ Approval:
- **Migration Scripts:** `/var/www/nordabiznes/database/*.sql` - **Migration Scripts:** `/var/www/nordabiznes/database/*.sql`
- **Application Logs:** `/var/log/nordabiznes/app.log` - **Application Logs:** `/var/log/nordabiznes/app.log`
- **PostgreSQL Logs:** `sudo journalctl -u postgresql --no-pager` - **PostgreSQL Logs:** `sudo journalctl -u postgresql --no-pager`
- **Production Server:** `10.22.68.249` (NORDABIZ-01) - **Production Server:** `57.128.200.27` (OVH VPS inpi-vps-waw01)
- **VPN Required:** FortiGate SSL-VPN (85.237.177.83) - **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 **Maintained By:** Norda Biznes Development Team
**Next Review:** 2026-04-02 (quarterly) **Next Review:** 2026-07-04 (quarterly)

View File

@ -47,16 +47,15 @@ git branch -d auto-claude/<task-id> # Usuń branch
| Hourly (lokalnie) | `/var/backups/nordabiz/hourly/` | 24h | | Hourly (lokalnie) | `/var/backups/nordabiz/hourly/` | 24h |
| Daily (lokalnie) | `/var/backups/nordabiz/daily/` | 30 dni | | Daily (lokalnie) | `/var/backups/nordabiz/daily/` | 30 dni |
| Offsite (PBS) | `10.22.68.127:/backup/nordabiz/` | 30 dni | | Offsite (PBS) | `10.22.68.127:/backup/nordabiz/` | 30 dni |
| VM Snapshots | Proxmox | 3 snapshoty |
### Szybkie przywracanie ### Szybkie przywracanie
```bash ```bash
# Lista dostępnych backupów # 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 # 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 # Weryfikacja
curl -I https://nordabiznes.pl/health curl -I https://nordabiznes.pl/health

View File

@ -34,7 +34,7 @@ API_KEY = 'AIzaSyAbc123...'
DATABASE_URL = os.getenv('DATABASE_URL', 'postgresql://user:CHANGE_ME@localhost/nordabiz') DATABASE_URL = os.getenv('DATABASE_URL', 'postgresql://user:CHANGE_ME@localhost/nordabiz')
# ❌ BŁĘDNIE - wartość produkcyjna jako fallback: # ❌ 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 ### 3. Przechowuj credentials w plikach .env
@ -137,9 +137,9 @@ grep -r "postgresql://.*:.*@" --include="*.py" . | grep -v "CHANGE_ME" | grep -v
### Uprawnienia plików ### Uprawnienia plików
```bash ```bash
# Tylko właściciel może czytać plik .env # Tylko root może czytać plik .env (OVH VPS — .env jest root-owned)
chmod 600 /var/www/nordabiznes/.env sudo chmod 600 /var/www/nordabiznes/.env
chown www-data:www-data /var/www/nordabiznes/.env sudo chown root:root /var/www/nordabiznes/.env
``` ```
--- ---

View File

@ -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' DATABASE_URL = 'postgresql://nordabiz_app:<PASSWORD_FROM_ENV>@127.0.0.1:5432/nordabiz'
# BŁĘDNIE (PostgreSQL nie akceptuje zewnętrznych połączeń): # 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:** **Pliki z konfiguracją bazy:**

View File

@ -1,8 +1,10 @@
# NordaBiz Disaster Recovery Playbook # NordaBiz Disaster Recovery Playbook
**Wersja:** 1.0 **Wersja:** 1.1
**Data utworzenia:** 2026-02-02 **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 ## Lokalizacje backupów
### Backup lokalny (NORDABIZ-01) ### Backup lokalny (OVH VPS — inpi-vps-waw01)
``` ```
/var/backups/nordabiz/ /var/backups/nordabiz/
@ -68,12 +70,13 @@
└── config/ # Mirror konfiguracji └── config/ # Mirror konfiguracji
``` ```
### VM Snapshots (Proxmox) ### VM Snapshots (Proxmox — TYLKO STAGING)
- **Lokalizacja:** Proxmox VE (hypervisor) - **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 - **Storage:** local-lvm lub PBS
- **Dostęp:** Proxmox Web UI (https://10.22.68.10:8006) - **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:** **Objawy:**
- Brak odpowiedzi SSH - Brak odpowiedzi SSH na 57.128.200.27
- HTTP 502 na https://nordabiznes.pl - HTTP 502 na https://nordabiznes.pl
- VM nie odpowiada w Proxmox
**Procedura:** **Procedura:**
#### Opcja A: Restart VM #### Opcja A: Restart VPS via OVH Panel
1. Zaloguj się do Proxmox: https://10.22.68.10:8006 1. Zaloguj się do OVH Manager: https://www.ovh.com/manager/
2. VM 249 → Stop → Start 2. Znajdź VPS inpi-vps-waw01
3. Poczekaj 2-3 minuty 3. Wykonaj reboot
4. Zweryfikuj: `curl -I https://nordabiznes.pl/health` 4. Poczekaj 2-3 minuty
5. Zweryfikuj: `curl -I https://nordabiznes.pl/health`
#### Opcja B: Rollback do snapshotu #### Opcja B: Pełne odtworzenie (nowy VPS lub VM)
1. Proxmox → VM 249 → Snapshots 1. Utwórz nowy VPS w OVH lub VM w Proxmox
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)
2. Zainstaluj Ubuntu 22.04 LTS 2. Zainstaluj Ubuntu 22.04 LTS
3. Postępuj zgodnie z sekcją [Pełne odtworzenie systemu](#pełne-odtworzenie-systemu) 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. Wykonaj gdy VM jest całkowicie niedostępna lub skompromitowana.
#### Krok 1: Przygotowanie nowej VM #### Krok 1: Przygotowanie nowego serwera
```bash ```bash
# Na Proxmox - utwórz VM: # Na OVH Manager - utwórz VPS lub na Proxmox - utwórz VM:
# - ID: 249 (lub nowy)
# - CPU: 4 vCPU # - CPU: 4 vCPU
# - RAM: 8 GB # - RAM: 8 GB
# - Disk: 100 GB SSD # - Disk: 100 GB SSD
# - Network: vmbr0
# - IP: 10.22.68.249/24
# - Gateway: 10.22.68.1
# - OS: Ubuntu 22.04 LTS # - OS: Ubuntu 22.04 LTS
# - Publiczny IP (jeśli OVH VPS) lub IP wewnętrzny (jeśli Proxmox)
``` ```
#### Krok 2: Instalacja pakietów #### 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 ```bash
sudo mkdir -p /var/www/nordabiznes 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 # Sync z lokalnej maszyny deweloperskiej (brak .git na serwerze!)
sudo -u www-data git clone https://10.22.68.180:3000/maciejpi/nordabiz.git /var/www/nordabiznes rsync -avz --exclude='.git' --exclude='venv' --exclude='.env' \
/path/to/nordabiz/ maciejpi@57.128.200.27:/var/www/nordabiznes/
# Virtualenv # Virtualenv
cd /var/www/nordabiznes cd /var/www/nordabiznes
sudo -u www-data python3 -m venv venv python3 -m venv venv
sudo -u www-data venv/bin/pip install -r requirements.txt venv/bin/pip install -r requirements.txt
``` ```
#### Krok 6: Przywrócenie konfiguracji #### Krok 6: Przywrócenie konfiguracji
@ -277,7 +271,7 @@ sudo -u www-data venv/bin/pip install -r requirements.txt
```bash ```bash
# Skopiuj .env z backup # Skopiuj .env z backup
scp maciejpi@10.22.68.127:/backup/nordabiz/config/latest/.env /var/www/nordabiznes/.env 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 sudo chmod 600 /var/www/nordabiznes/.env
# WAŻNE: Zaktualizuj DATABASE_URL w .env jeśli zmieniłeś hasło! # 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 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 ```bash
ssh maciejpi@10.22.68.250 # OVH Manager → nordabiznes.pl → DNS Zone → rekord A → NOWE_IP
# NPM Admin: http://10.22.68.250:81 # Produkcja na OVH VPS ma publiczny IP — ruch nie przechodzi przez NPM (10.22.68.250)
# Proxy Host 27 → Forward Host: NOWE_IP, Port: 5000 # NPM obsługuje tylko staging (staging.nordabiznes.pl)
``` ```
--- ---
@ -366,16 +360,13 @@ Wykonuj co kwartał:
curl -I https://nordabiznes.pl/health curl -I https://nordabiznes.pl/health
# Sprawdź status usługi # 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 # 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" 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;\""
``` ```
--- ---

View File

@ -174,7 +174,7 @@ git merge feature/phase2-auth-public
git push origin master && git push inpi master git push origin master && git push inpi master
# 3. Deploy # 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!) # 4. Weryfikacja (natychmiast!)
curl -sI https://nordabiznes.pl/ | head -3 curl -sI https://nordabiznes.pl/ | head -3
@ -188,7 +188,7 @@ curl -sI https://nordabiznes.pl/dashboard | head -3
# Natychmiastowy rollback # Natychmiastowy rollback
git revert HEAD git revert HEAD
git push origin master && git push inpi master 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"
``` ```
--- ---

View File

@ -510,7 +510,7 @@ git add . && git commit -m "refactor(phase-X): ..."
git push origin master && git push inpi master git push origin master && git push inpi master
# 4. Deploy # 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 # 5. Weryfikacja produkcji
curl https://nordabiznes.pl/health curl https://nordabiznes.pl/health
@ -521,7 +521,7 @@ curl https://nordabiznes.pl/health
```bash ```bash
git revert HEAD git revert HEAD
git push origin master && git push inpi master 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"
``` ```
--- ---

View File

@ -5,10 +5,10 @@
## Klucz SSH do dodania na PBS ## 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 ## Instrukcja konfiguracji
@ -20,7 +20,7 @@ Zaloguj się na PBS przez konsolę Proxmox lub inną metodę i wykonaj:
```bash ```bash
# Na PBS (10.22.68.127) # Na PBS (10.22.68.127)
mkdir -p /home/maciejpi/.ssh 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 700 /home/maciejpi/.ssh
chmod 600 /home/maciejpi/.ssh/authorized_keys chmod 600 /home/maciejpi/.ssh/authorized_keys
chown -R maciejpi:maciejpi /home/maciejpi/.ssh 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 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 ```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" 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: Po weryfikacji połączenia, utwórz plik cron:
```bash ```bash
# Na NORDABIZ-01 # Na OVH VPS (inpi-vps-waw01)
sudo tee /etc/cron.d/nordabiz-offsite << 'EOF' sudo tee /etc/cron.d/nordabiz-offsite << 'EOF'
# NordaBiz Offsite Backup # NordaBiz Offsite Backup
SHELL=/bin/bash SHELL=/bin/bash
@ -65,7 +65,7 @@ sudo chmod 644 /etc/cron.d/nordabiz-offsite
### Krok 5: Test synchronizacji ### Krok 5: Test synchronizacji
```bash ```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/ 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/" ssh maciejpi@10.22.68.127 "ls -la /backup/nordabiz/daily/"
# Sprawdź logi # 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"
``` ```

View File

@ -138,7 +138,7 @@ Environment variables:
## 8) Deployment and Ops ## 8) Deployment and Ops
Production: 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) - DB: PostgreSQL on same host (5432)
- Reverse proxy: NPM on 10.22.68.250 - Reverse proxy: NPM on 10.22.68.250
- Domain: nordabiznes.pl - Domain: nordabiznes.pl
@ -155,7 +155,7 @@ CRITICAL CONFIG:
Deployment workflow: Deployment workflow:
- push to GitHub and Gitea - push to GitHub and Gitea
- pull on staging, test - 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 - restart systemd service
--- ---

View File

@ -104,5 +104,5 @@ git commit -m "docs: Release notes vX.XX.0"
# 3. Push i deploy # 3. Push i deploy
git push origin master && git push inpi master 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"
``` ```

View File

@ -150,7 +150,7 @@ python3 app.py
### Step 1: SSH to Production Server ### Step 1: SSH to Production Server
```bash ```bash
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
``` ```
**IMPORTANT:** Always SSH as `maciejpi`, NEVER as root! **IMPORTANT:** Always SSH as `maciejpi`, NEVER as root!
@ -168,7 +168,7 @@ sudo -u www-data nano .env
### Step 3: Set Production Credentials ### Step 3: Set Production Credentials
```bash ```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 DATABASE_URL=postgresql://nordabiz_app:YOUR_PRODUCTION_PASSWORD@127.0.0.1:5432/nordabiz
# Flask Configuration # Flask Configuration
@ -186,13 +186,13 @@ GOOGLE_PLACES_API_KEY=your_production_places_key
### Step 4: Set File Permissions ### Step 4: Set File Permissions
```bash ```bash
# Ensure .env is readable only by www-data # Ensure .env is readable only by root (OVH VPS — .env is root-owned)
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 sudo chmod 600 /var/www/nordabiznes/.env
# Verify permissions # Verify permissions
ls -la /var/www/nordabiznes/.env ls -la /var/www/nordabiznes/.env
# Expected: -rw------- 1 www-data www-data # Expected: -rw------- 1 root root
``` ```
### Step 5: Restart Application ### Step 5: Restart Application
@ -245,7 +245,7 @@ nano ~/.pgpass
``` ```
# Format: hostname:port:database:username:password # 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 localhost:5433:nordabiz:nordabiz_user:nordabiz_password
``` ```
@ -564,10 +564,10 @@ sudo systemctl restart nordabiznes
# Development # Development
nano .env # Update DATABASE_URL with new password nano .env # Update DATABASE_URL with new password
# Production # Production (OVH VPS)
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
cd /var/www/nordabiznes 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 sudo systemctl restart nordabiznes
``` ```

View File

@ -1,7 +1,7 @@
# System Context Diagram (C4 Level 1) # System Context Diagram (C4 Level 1)
**Document Version:** 1.0 **Document Version:** 1.0
**Last Updated:** 2026-01-10 **Last Updated:** 2026-04-04
**Status:** Production LIVE **Status:** Production LIVE
**Diagram Type:** C4 Model - Level 1 (System Context) **Diagram Type:** C4 Model - Level 1 (System Context)
@ -344,7 +344,7 @@ System Event → NordaBiz Hub → MS Graph API → Outlook
### Data Protection ### Data Protection
- **Passwords:** Hashed with bcrypt - **Passwords:** Hashed with bcrypt
- **Sessions:** Flask session cookies (encrypted) - **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 - **CSRF:** Flask-WTF protection enabled
--- ---
@ -353,9 +353,13 @@ System Event → NordaBiz Hub → MS Graph API → Outlook
**Production:** **Production:**
- **URL:** https://nordabiznes.pl - **URL:** https://nordabiznes.pl
- **Server:** NORDABIZ-01 (10.22.68.249) - **Server:** OVH VPS (57.128.200.27, hostname: inpi-vps-waw01)
- **Proxy:** R11-REVPROXY-01 (10.22.68.250) - **SSL:** nginx with Let's Encrypt (on the same VPS)
- **Status:** LIVE since 2025-11-23 - **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:** **Development:**
- **Local:** localhost:5000 or 5001 - **Local:** localhost:5000 or 5001

View File

@ -1,7 +1,7 @@
# Container Diagram (C4 Level 2) # Container Diagram (C4 Level 2)
**Document Version:** 1.0 **Document Version:** 1.0
**Last Updated:** 2026-01-10 **Last Updated:** 2026-04-04
**Status:** Production LIVE **Status:** Production LIVE
**Diagram Type:** C4 Model - Level 2 (Containers) **Diagram Type:** C4 Model - Level 2 (Containers)
@ -32,11 +32,11 @@ graph TB
%% System boundary %% System boundary
subgraph "Norda Biznes Partner System" subgraph "Norda Biznes Partner System"
subgraph "R11-REVPROXY-01 [VM 119 | 10.22.68.250]" subgraph "OVH VPS [inpi-vps-waw01 | 57.128.200.27]"
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"] 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 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"] 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.)"] 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 end
%% User interactions %% User interactions
Users -->|"HTTPS<br/>Port 443"| NPM Users -->|"HTTPS<br/>Port 443"| Nginx
Admin -->|"HTTPS<br/>Port 443"| NPM Admin -->|"HTTPS<br/>Port 443"| Nginx
Admin -->|"SSH<br/>Port 22"| WebApp Admin -->|"SSH<br/>Port 22"| WebApp
%% NPM routing %% Nginx routing
NPM -->|"HTTP<br/>Port 5000<br/>(CRITICAL!)"| WebApp Nginx -->|"HTTP<br/>127.0.0.1:5000"| WebApp
%% Web app to database %% Web app to database
WebApp -->|"SQL Queries<br/>SQLAlchemy ORM<br/>localhost:5432"| Database WebApp -->|"SQL Queries<br/>SQLAlchemy ORM<br/>localhost:5432"| Database
@ -113,7 +113,7 @@ graph TB
class WebApp,Scripts containerStyle class WebApp,Scripts containerStyle
class Database databaseStyle class Database databaseStyle
class SearchSvc,ChatSvc,EmailSvc,GeminiSvc,KRSSvc,GBPSvc,ITSvc serviceStyle 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 Gemini,BraveAPI,PageSpeed,Places,KRS,MSGraph,ALEO,Rejestr externalStyle
class Users,Admin personStyle class Users,Admin personStyle
``` ```
@ -122,54 +122,45 @@ graph TB
## Container Descriptions ## Container Descriptions
### 🔒 Nginx Proxy Manager (NPM) ### 🔒 Nginx (Reverse Proxy)
**Location:** R11-REVPROXY-01 (VM 119, IP 10.22.68.250) **Location:** OVH VPS (57.128.200.27, hostname: inpi-vps-waw01)
**Technology:** Nginx + Docker **Technology:** Nginx
**Protocol:** HTTPS (Port 443) **Protocol:** HTTPS (Port 443)
**Purpose:** SSL termination and reverse proxy **Purpose:** SSL termination and reverse proxy
**Responsibilities:** **Responsibilities:**
- Terminate SSL/TLS connections (Let's Encrypt certificates) - Terminate SSL/TLS connections (Let's Encrypt certificates via certbot)
- Route incoming HTTPS requests to backend Flask app - Route incoming HTTPS requests to backend Gunicorn on 127.0.0.1:5000
- Automatically renew SSL certificates - Automatically renew SSL certificates
- Force HTTP→HTTPS redirects - Force HTTP→HTTPS redirects
- Basic security headers (HSTS, CSP) - Security headers (HSTS, CSP)
**Critical Configuration:** **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:** **Verification:**
```bash ```bash
curl -I https://nordabiznes.pl/health curl -I https://nordabiznes.pl/health
# Expected: HTTP 200 OK # Expected: HTTP 200 OK
``` ```
**NPM Access:** **SSL:** Let's Encrypt (certbot auto-renewal)
- **Web UI:** http://10.22.68.250:81
- **Proxy Host ID:** 27
- **Domain:** nordabiznes.pl
- **SSL:** Let's Encrypt (auto-renewal)
--- ---
### 🌐 Flask Web Application ### 🌐 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 **Technology:** Flask 3.0 + Gunicorn WSGI server
**Language:** Python 3.9+ **Language:** Python 3.9+
**Protocol:** HTTP (Port 5000 - internal only) **Protocol:** HTTP (Port 5000 - localhost only, via nginx proxy_pass)
**Main File:** `/var/www/nordabiznes/app.py` (13,144 lines) **Main File:** `/var/www/nordabiznes/app.py`
**Responsibilities:** **Responsibilities:**
- Handle HTTP requests from NPM - Handle HTTP requests from nginx
- Render HTML templates (Jinja2) - Render HTML templates (Jinja2)
- Provide REST API endpoints (90+ routes) - Provide REST API endpoints (90+ routes)
- User authentication (Flask-Login) - User authentication (Flask-Login)
@ -208,13 +199,13 @@ sudo systemctl restart nordabiznes
sudo journalctl -u nordabiznes -f sudo journalctl -u nordabiznes -f
``` ```
**Application User:** `www-data` (NOT root or maciejpi) **Application User:** `maciejpi`
--- ---
### 💾 PostgreSQL Database ### 💾 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 **Technology:** PostgreSQL 14
**Protocol:** PostgreSQL wire protocol (Port 5432) **Protocol:** PostgreSQL wire protocol (Port 5432)
**Access:** **localhost ONLY** (127.0.0.1) **Access:** **localhost ONLY** (127.0.0.1)
@ -254,10 +245,10 @@ sudo journalctl -u nordabiznes -f
**Connection Strings:** **Connection Strings:**
```python ```python
# Flask App (from NORDABIZ-01) # Flask App (from OVH VPS)
DATABASE_URL = 'postgresql://nordabiz_app:***@127.0.0.1:5432/nordabiz' 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' 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 ### ⚙️ 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/` **Directory:** `/var/www/nordabiznes/scripts/`
**Technology:** Python 3.9+ (same virtualenv as Flask app) **Technology:** Python 3.9+ (same virtualenv as Flask app)
**Execution:** Manual via SSH or Cron jobs **Execution:** Manual via SSH or Cron jobs
@ -300,7 +291,7 @@ Only connections from `localhost` (127.0.0.1) are allowed.
**Execution Example:** **Execution Example:**
```bash ```bash
cd /var/www/nordabiznes 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):** **Cron Jobs (Planned):**
@ -619,14 +610,14 @@ All external APIs are called via HTTPS with appropriate authentication.
User Browser (HTTPS :443) User Browser (HTTPS :443)
NPM @ 10.22.68.250:443 (SSL termination) Nginx @ 57.128.200.27:443 (SSL termination)
├─ Decrypt HTTPS ├─ Decrypt HTTPS
├─ Verify SSL certificate ├─ Verify SSL certificate
└─ Forward as HTTP └─ Forward as HTTP
Flask App @ 10.22.68.249:5000 (HTTP) Flask App @ 127.0.0.1:5000 (HTTP)
├─ Authenticate user (session cookie) ├─ Authenticate user (session cookie)
├─ Authorize request (permissions) ├─ Authorize request (permissions)
@ -644,7 +635,7 @@ PostgreSQL @ 127.0.0.1:5432 (local only)
Flask App (render template / JSON) Flask App (render template / JSON)
NPM (encrypt response) Nginx (encrypt response)
User Browser (HTTPS response) User Browser (HTTPS response)
@ -731,8 +722,8 @@ Admin Dashboard (display results)
| Zone | Components | Access Level | Trust Level | | Zone | Components | Access Level | Trust Level |
|------|------------|--------------|-------------| |------|------------|--------------|-------------|
| **Public Internet** | User browsers | Untrusted | Low | | **Public Internet** | User browsers | Untrusted | Low |
| **DMZ (Proxy)** | NPM (10.22.68.250) | Semi-trusted | Medium | | **Proxy (nginx)** | Nginx on OVH VPS (57.128.200.27:443) | Semi-trusted | Medium |
| **App Zone** | Flask App (10.22.68.249:5000) | Trusted | High | | **App Zone** | Flask App (127.0.0.1:5000) | Trusted | High |
| **Data Zone** | PostgreSQL (127.0.0.1:5432) | Highly trusted | Critical | | **Data Zone** | PostgreSQL (127.0.0.1:5432) | Highly trusted | Critical |
| **External APIs** | Gemini, Brave, etc. | Untrusted | Low | | **External APIs** | Gemini, Brave, etc. | Untrusted | Low |
@ -753,7 +744,7 @@ Admin Dashboard (display results)
### HTTPS/TLS ### 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 - **Protocols:** TLS 1.2, TLS 1.3
- **Cipher Suites:** Modern (AES-GCM, ChaCha20-Poly1305) - **Cipher Suites:** Modern (AES-GCM, ChaCha20-Poly1305)
- **HSTS:** Enabled (max-age=31536000) - **HSTS:** Enabled (max-age=31536000)
@ -763,7 +754,7 @@ Admin Dashboard (display results)
- **Access:** Localhost only (no external connections) - **Access:** Localhost only (no external connections)
- **Authentication:** Password-based (strong passwords) - **Authentication:** Password-based (strong passwords)
- **Encryption:** At-rest encryption (OS-level) - **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 ### Production Environment
**NPM Container (R11-REVPROXY-01):** **Nginx (OVH VPS):**
```yaml ```nginx
Proxy Host Configuration: # /etc/nginx/sites-available/nordabiznes.pl
Domain: nordabiznes.pl server {
Scheme: http listen 443 ssl http2;
Forward Hostname: 10.22.68.249 server_name nordabiznes.pl www.nordabiznes.pl;
Forward Port: 5000 # CRITICAL: Must be 5000, not 80!
Cache Assets: Yes ssl_certificate /etc/letsencrypt/live/nordabiznes.pl/fullchain.pem;
Block Common Exploits: Yes ssl_certificate_key /etc/letsencrypt/live/nordabiznes.pl/privkey.pem;
Websockets Support: No
SSL: location / {
Force SSL: Yes proxy_pass http://127.0.0.1:5000;
HTTP/2 Support: Yes proxy_set_header Host $host;
HSTS Enabled: Yes proxy_set_header X-Real-IP $remote_addr;
Certificate: Let's Encrypt proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
Auto-renewal: Yes proxy_set_header X-Forwarded-Proto $scheme;
}
}
``` ```
**Flask/Gunicorn (NORDABIZ-01):** **Flask/Gunicorn (OVH VPS):**
```ini ```ini
# /etc/systemd/system/nordabiznes.service # /etc/systemd/system/nordabiznes.service
[Unit] [Unit]
@ -798,8 +791,8 @@ After=network.target postgresql.service
[Service] [Service]
Type=notify Type=notify
User=www-data User=maciejpi
Group=www-data Group=maciejpi
WorkingDirectory=/var/www/nordabiznes WorkingDirectory=/var/www/nordabiznes
Environment="PATH=/var/www/nordabiznes/venv/bin" Environment="PATH=/var/www/nordabiznes/venv/bin"
ExecStart=/var/www/nordabiznes/venv/bin/gunicorn \ ExecStart=/var/www/nordabiznes/venv/bin/gunicorn \
@ -814,7 +807,7 @@ ExecStart=/var/www/nordabiznes/venv/bin/gunicorn \
WantedBy=multi-user.target WantedBy=multi-user.target
``` ```
**PostgreSQL (NORDABIZ-01):** **PostgreSQL (OVH VPS):**
```conf ```conf
# /etc/postgresql/14/main/postgresql.conf # /etc/postgresql/14/main/postgresql.conf
listen_addresses = 'localhost' # ONLY localhost! listen_addresses = 'localhost' # ONLY localhost!
@ -887,13 +880,13 @@ tail -f /var/log/nordabiznes/access.log
tail -f /var/log/nordabiznes/error.log tail -f /var/log/nordabiznes/error.log
``` ```
**NPM (Nginx Proxy Manager):** **Nginx (OVH VPS):**
```bash ```bash
# Docker logs # Nginx access logs
docker logs -f <npm-container-id> tail -f /var/log/nginx/access.log
# NPM access logs # Nginx error logs
docker exec <npm-container-id> tail -f /data/logs/proxy-host-27_access.log tail -f /var/log/nginx/error.log
``` ```
**PostgreSQL:** **PostgreSQL:**
@ -920,33 +913,22 @@ FROM pg_tables WHERE schemaname = 'public' ORDER BY pg_total_relation_size(schem
## Critical Configuration Warnings ## Critical Configuration Warnings
### ⚠️ NPM Proxy Port Configuration ### ⚠️ Nginx Proxy Configuration
**CRITICAL:** NPM **MUST** forward to port **5000**, NOT 80! **Production** uses nginx on OVH VPS (57.128.200.27) as reverse proxy to Gunicorn on 127.0.0.1:5000.
**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`
**Correct Configuration:** **Correct Configuration:**
``` ```
NPM (10.22.68.250:443) → Flask (10.22.68.249:5000) ✓ Nginx (57.128.200.27:443) → Gunicorn (127.0.0.1:5000) ✓
```
**Incorrect Configuration (causes loop):**
```
NPM (10.22.68.250:443) → Nginx (10.22.68.249:80) → NPM ✗
``` ```
**Verification After Changes:** **Verification After Changes:**
```bash ```bash
curl -I https://nordabiznes.pl/health 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):** **Scripts MUST use localhost (127.0.0.1):**
```python ```python
# CORRECT (from NORDABIZ-01) # CORRECT (from OVH VPS)
DATABASE_URL = 'postgresql://nordabiz_app:***@127.0.0.1:5432/nordabiz' DATABASE_URL = 'postgresql://nordabiz_app:***@127.0.0.1:5432/nordabiz'
# INCORRECT (external connection attempt) # 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 - OAuth client secrets
**Storage:** **Storage:**
- Production: `.env` file on NORDABIZ-01 - Production: `.env` file on OVH VPS (57.128.200.27)
- Development: `.env` file on local machine - Development: `.env` file on local machine
- Backup: Secure password manager (not in git!) - 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 **Document Status:** ✅ Complete
**Diagram Validated:** 2026-01-10 **Diagram Validated:** 2026-04-04
**Mermaid Syntax:** v10.6+ **Mermaid Syntax:** v10.6+
**Renders in:** GitHub, GitLab, VS Code (with Mermaid extension) **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)

View File

@ -1,12 +1,14 @@
# Deployment Architecture Diagram # Deployment Architecture Diagram
**Document Version:** 1.0 **Document Version:** 1.0
**Last Updated:** 2026-01-10 **Last Updated:** 2026-04-04
**Status:** Production LIVE **Status:** Production LIVE
**Diagram Type:** Infrastructure / Deployment View **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 ## Overview
This diagram shows the **physical deployment architecture** of the Norda Biznes Partner system. It illustrates: This diagram shows the **physical deployment architecture** of the Norda Biznes Partner system. It illustrates:
@ -30,27 +32,18 @@ graph TB
%% External actors and entry points %% External actors and entry points
Internet["🌐 INTERNET<br/>External Users"] Internet["🌐 INTERNET<br/>External Users"]
%% Public gateway %% Production server (OVH VPS)
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"] subgraph "OVH VPS [inpi-vps-waw01 | 57.128.200.27]"
subgraph "Reverse Proxy"
%% Internal network boundary Nginx["🔒 NGINX<br/>Ports: 443 (HTTPS), 80 (redirect)<br/><br/>SSL: Let's Encrypt (certbot)<br/>Domains: nordabiznes.pl"]
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"]
end end
%% Backend application server
subgraph "NORDABIZ-01<br/>VM 249 | 10.22.68.249"
subgraph "Application Layer" 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"] 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"]
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 end
subgraph "Data Layer" 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!"] 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 end
subgraph "Background Jobs" subgraph "Background Jobs"
@ -58,12 +51,6 @@ graph TB
end end
end 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"]
end
end
%% External services %% External services
subgraph "External APIs (HTTPS)" subgraph "External APIs (HTTPS)"
Gemini["🤖 Google Gemini AI<br/>generativelanguage.googleapis.com<br/>Auth: API Key"] Gemini["🤖 Google Gemini AI<br/>generativelanguage.googleapis.com<br/>Auth: API Key"]
@ -75,12 +62,10 @@ graph TB
end end
%% User traffic flow %% User traffic flow
Internet -->|"HTTPS :443<br/>HTTP :80"| Fortigate Internet -->|"HTTPS :443<br/>HTTP :80"| Nginx
Fortigate -->|"NAT<br/>443→443<br/>80→80"| NPM
%% NPM to backend (CRITICAL PATH) %% Nginx to backend
NPM ==>|"⚠️ CRITICAL!<br/>HTTP :5000<br/>(NOT port 80!)"| Gunicorn Nginx -->|"HTTP<br/>127.0.0.1:5000"| Gunicorn
NPM -.->|"❌ WRONG!<br/>Creates redirect loop"| NginxSys
%% Application to database (localhost only) %% Application to database (localhost only)
Gunicorn <-->|"SQL<br/>localhost:5432<br/>SQLAlchemy ORM"| PostgreSQL Gunicorn <-->|"SQL<br/>localhost:5432<br/>SQLAlchemy ORM"| PostgreSQL
@ -95,24 +80,15 @@ graph TB
Scripts -->|"HTTPS<br/>Audit requests"| PageSpeed Scripts -->|"HTTPS<br/>Audit requests"| PageSpeed
Scripts -->|"HTTPS<br/>News search"| BraveAPI Scripts -->|"HTTPS<br/>News search"| BraveAPI
%% Git operations
Gunicorn -.->|"git pull<br/>(deployment)"| Gitea
%% Styling %% Styling
classDef serverStyle fill:#2c3e50,stroke:#34495e,color:#ecf0f1,stroke-width:3px
classDef appStyle fill:#1168bd,stroke:#0b4884,color:#ffffff,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 dbStyle fill:#438dd5,stroke:#2e6295,color:#ffffff,stroke-width:3px
classDef proxyStyle fill:#e74c3c,stroke:#c0392b,color:#ffffff,stroke-width:4px 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 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 Nginx proxyStyle
class Fortigate firewallStyle
class Gunicorn,Scripts appStyle class Gunicorn,Scripts appStyle
class PostgreSQL dbStyle class PostgreSQL dbStyle
class NginxSys warningStyle
class Gitea serverStyle
class Gemini,BraveAPI,PageSpeed,Places,KRS,MSGraph externalStyle class Gemini,BraveAPI,PageSpeed,Places,KRS,MSGraph externalStyle
``` ```
@ -120,35 +96,42 @@ graph TB
## Infrastructure Inventory ## Infrastructure Inventory
### Production Servers ### Production Server
| Server | VM ID | IP Address | Hostname | OS | vCPU | RAM | Disk | Hypervisor | | Server | IP Address | Hostname | OS | vCPU | RAM | Disk | Provider |
|--------|-------|------------|----------|-----|------|-----|------|------------| |--------|------------|----------|-----|------|-----|------|----------|
| **NORDABIZ-01** | 249 | 10.22.68.249 | nordabiz-01 | Ubuntu 22.04 | 4 | 8 GB | 100 GB SSD | Proxmox VE | | **OVH VPS** | 57.128.200.27 | inpi-vps-waw01 | Ubuntu 22.04 | 4 | 8 GB | 80 GB SSD | OVH Cloud (Warsaw) |
| **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 | ### 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 ## Server Details
### NORDABIZ-01 (Backend Application Server) ### OVH VPS (Production Server)
**Infrastructure:** **Infrastructure:**
- **VM ID:** 249 - **IP Address:** 57.128.200.27
- **IP Address:** 10.22.68.249 - **Hostname:** inpi-vps-waw01
- **Internal DNS:** nordabiznes.inpi.local
- **OS:** Ubuntu 22.04 LTS - **OS:** Ubuntu 22.04 LTS
- **Resources:** 4 vCPU, 8 GB RAM, 100 GB SSD - **Resources:** 4 vCPU, 8 GB RAM, 80 GB SSD
- **Hypervisor:** Proxmox VE - **Provider:** OVH Cloud (Warsaw datacenter)
- **DNS:** nordabiznes.pl -> 57.128.200.27 (A record in OVH DNS)
**Services Running:** **Services Running:**
| Service | Port | Binding | User | Status | Purpose | | 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 | | 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 | | SSH | 22 | 0.0.0.0 | - | Active | Remote administration |
**Application Paths:** **Application Paths:**
@ -187,7 +170,7 @@ tail -f /var/log/nordabiznes/error.log # Error log
**SSH Access:** **SSH Access:**
```bash ```bash
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
# CRITICAL: Always use 'maciejpi' user, NEVER 'root'! # CRITICAL: Always use 'maciejpi' user, NEVER 'root'!
``` ```
@ -195,8 +178,10 @@ ssh maciejpi@10.22.68.249
```ini ```ini
# /etc/systemd/system/nordabiznes.service # /etc/systemd/system/nordabiznes.service
[Service] [Service]
User=maciejpi
Group=maciejpi
ExecStart=/var/www/nordabiznes/venv/bin/gunicorn \ ExecStart=/var/www/nordabiznes/venv/bin/gunicorn \
--bind 0.0.0.0:5000 \ --bind 127.0.0.1:5000 \
--workers 4 \ --workers 4 \
--timeout 120 \ --timeout 120 \
--access-logfile /var/log/nordabiznes/access.log \ --access-logfile /var/log/nordabiznes/access.log \
@ -204,140 +189,37 @@ ExecStart=/var/www/nordabiznes/venv/bin/gunicorn \
app:app app:app
``` ```
**⚠️ WARNING - System Nginx:** **Nginx Configuration:**
- System nginx on ports 80/443 is for HTTP→HTTPS redirects ONLY ```nginx
- **NEVER** configure NPM to use port 80 or 443 on this server # /etc/nginx/sites-available/nordabiznes.pl
- Doing so causes infinite redirect loop (ERR_TOO_MANY_REDIRECTS) server {
- See: `docs/INCIDENT_REPORT_20260102.md` 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) server {
listen 80;
**Infrastructure:** server_name nordabiznes.pl www.nordabiznes.pl;
- **VM ID:** 119 return 301 https://$server_name$request_uri;
- **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
``` ```
**NPM Configuration Database:** **SSL Certificate Management:**
- **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:**
```bash ```bash
ssh maciejpi@10.22.68.250 # Let's Encrypt via certbot
``` sudo certbot certificates # Check certificate status
sudo certbot renew # Renew certificates
**Critical Proxy Host Configuration (ID: 27):** sudo certbot renew --dry-run # Test renewal
```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)
``` ```
--- ---
@ -349,8 +231,8 @@ git remote -v
| Segment | CIDR/Address | Purpose | Security Level | | Segment | CIDR/Address | Purpose | Security Level |
|---------|--------------|---------|----------------| |---------|--------------|---------|----------------|
| Public Internet | 0.0.0.0/0 | External user access | Untrusted | | Public Internet | 0.0.0.0/0 | External user access | Untrusted |
| WAN (Fortigate) | 85.237.177.83/32 | Public gateway | Perimeter | | OVH VPS | 57.128.200.27/32 | Production server (direct) | Production |
| INPI LAN | 10.22.68.0/24 | Internal services network | Trusted | | INPI LAN | 10.22.68.0/24 | Staging + internal services | Trusted |
| Localhost | 127.0.0.1/8 | Server-local services | Isolated | | Localhost | 127.0.0.1/8 | Server-local services | Isolated |
### DNS Configuration ### DNS Configuration
@ -359,42 +241,29 @@ git remote -v
| Type | Name | Value | TTL | Purpose | | Type | Name | Value | TTL | Purpose |
|------|------|-------|-----|---------| |------|------|-------|-----|---------|
| A | nordabiznes.pl | 85.237.177.83 | 3600 | Main domain | | A | nordabiznes.pl | 57.128.200.27 | 3600 | Main domain (OVH VPS) |
| A | www.nordabiznes.pl | 85.237.177.83 | 3600 | WWW subdomain | | 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) |
**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 |
--- ---
## Port Mappings ## 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 | | Port | Protocol | Service | Binding | Access | Purpose | Security Notes |
|------|----------|---------|---------|--------|---------|----------------| |------|----------|---------|---------|--------|---------|----------------|
| 22 | TCP | SSH | 0.0.0.0 | Internal only | Server administration | Key-based auth | | 22 | TCP | SSH | 0.0.0.0 | Public (key-only) | Server administration | Key-based auth |
| 80 | TCP | Nginx (system) | 0.0.0.0 | Internal only | HTTP→HTTPS redirect | ⚠️ Not for NPM! | | 80 | TCP | Nginx | 0.0.0.0 | Public | HTTP→HTTPS redirect | Auto-redirect |
| 443 | TCP | Nginx (system) | 0.0.0.0 | Internal only | HTTPS redirect | ⚠️ Not for NPM! | | 443 | TCP | Nginx | 0.0.0.0 | Public | HTTPS (SSL termination) | Let's Encrypt |
| **5000** | **TCP** | **Gunicorn/Flask** | **0.0.0.0** | **Internal only** | **Main Application** | **✓ NPM uses this** | | **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 | | 5432 | TCP | PostgreSQL | 127.0.0.1 | Localhost only | Database | No external access |
| 5433 | TCP | - | - | Unused | Reserved for dev Docker | - |
**⚠️ PORT 5000 - CRITICAL NOTES:** **Traffic flow:** Internet -> nginx (:443) -> proxy_pass -> Gunicorn (127.0.0.1:5000)
- 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
--- ---
### 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 | | 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 | | 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 | | 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 | | 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 | | 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 | | 80 | TCP | 10.22.68.250 | 80 | HTTP redirect | Incoming |
**Firewall Rules:** **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.
- 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)
--- ---
@ -438,30 +305,21 @@ git remote -v
│ 1. USER BROWSER │ │ 1. USER BROWSER │
│ https://nordabiznes.pl │ │ https://nordabiznes.pl │
└────────────────────────────┬────────────────────────────────────┘ └────────────────────────────┬────────────────────────────────────┘
│ DNS: nordabiznes.pl → 85.237.177.83 │ DNS: nordabiznes.pl → 57.128.200.27
│ HTTPS :443 │ HTTPS :443
┌─────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────┐
│ 2. FORTIGATE FIREWALL (85.237.177.83) │ │ 2. NGINX @ OVH VPS (57.128.200.27:443) │
│ NAT: 443 → 10.22.68.250:443 │
└────────────────────────────┬────────────────────────────────────┘
│ Forward to proxy
│ HTTPS :443
┌─────────────────────────────────────────────────────────────────┐
│ 3. NPM @ R11-REVPROXY-01 (10.22.68.250:443) │
│ • Accept HTTPS connection │ │ • Accept HTTPS connection │
│ • TLS handshake (Let's Encrypt certificate) │ • TLS handshake (Let's Encrypt certificate via certbot) │
│ • Terminate SSL/TLS │ │ • Terminate SSL/TLS │
│ • Add security headers (HSTS, etc.) │ │ • Add security headers (HSTS, etc.) │
│ • Proxy pass to backend │ • Proxy pass to Gunicorn │
└────────────────────────────┬────────────────────────────────────┘ └────────────────────────────┬────────────────────────────────────┘
│ ⚠️ CRITICAL PATH │ http://127.0.0.1:5000
│ http://10.22.68.249:5000
│ (NOT port 80!)
┌─────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────┐
4. GUNICORN @ NORDABIZ-01 (10.22.68.249:5000) 3. GUNICORN @ OVH VPS (127.0.0.1:5000)
│ • Receive HTTP request (decrypted) │ │ • Receive HTTP request (decrypted) │
│ • Load balance across 4 workers │ │ • Load balance across 4 workers │
│ • Pass to Flask application │ │ • Pass to Flask application │
@ -469,7 +327,7 @@ git remote -v
│ Process request │ Process request
┌─────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────┐
5. FLASK APP (app.py) │ 4. FLASK APP (app.py) │
│ • Rate limiting check (Flask-Limiter) │ │ • Rate limiting check (Flask-Limiter) │
│ • Session validation (Flask-Login) │ │ • Session validation (Flask-Login) │
│ • CSRF protection (Flask-WTF) │ │ • CSRF protection (Flask-WTF) │
@ -480,7 +338,7 @@ git remote -v
│ localhost:5432 │ localhost:5432
┌─────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────┐
6. POSTGRESQL @ localhost:5432 │ 5. POSTGRESQL @ localhost:5432 │
│ • Execute SQL query (SQLAlchemy ORM) │ │ • Execute SQL query (SQLAlchemy ORM) │
│ • Apply constraints and indexes │ │ • Apply constraints and indexes │
│ • Return result set │ │ • Return result set │
@ -488,62 +346,32 @@ git remote -v
│ Results │ Results
┌─────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────┐
7. FLASK APP (render response) │ 6. FLASK APP (render response) │
│ • Jinja2 template rendering │ │ • Jinja2 template rendering │
│ • JSON serialization (API routes) │ │ • JSON serialization (API routes) │
│ • Apply response headers │ │ • Apply response headers │
└────────────────────────────┬────────────────────────────────────┘ └────────────────────────────┬────────────────────────────────────┘
│ HTTP response │ HTTP response
GUNICORN → NPM (encrypt with TLS) → FORTIGATE → USER BROWSER GUNICORN → NGINX (encrypt with TLS) → USER BROWSER
``` ```
**Timing Breakdown:** **Timing Breakdown:**
- DNS resolution: ~50ms - DNS resolution: ~50ms
- SSL handshake: ~100ms - SSL handshake: ~100ms
- NPM proxy: ~10ms - Nginx proxy: ~5ms
- Flask processing: ~50-500ms (depends on query complexity) - Flask processing: ~50-500ms (depends on query complexity)
- Database query: ~10-100ms - Database query: ~10-100ms
- Template rendering: ~20-50ms - 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.**
``` 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.
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
--- ---
@ -553,14 +381,13 @@ BROWSER ERROR: ERR_TOO_MANY_REDIRECTS
**Certificate Details:** **Certificate Details:**
- **Provider:** Let's Encrypt - **Provider:** Let's Encrypt
- **Managed By:** NPM (Nginx Proxy Manager) - **Managed By:** certbot on OVH VPS
- **Certificate ID:** 27
- **Domains:** - **Domains:**
- nordabiznes.pl - nordabiznes.pl
- www.nordabiznes.pl - www.nordabiznes.pl
- **Key Type:** RSA 2048-bit - **Key Type:** RSA 2048-bit
- **Validity:** 90 days (auto-renewed) - **Validity:** 90 days (auto-renewed)
- **Renewal:** Automatic (30 days before expiry) - **Renewal:** Automatic via certbot cron/timer
**TLS Configuration:** **TLS Configuration:**
- **Protocols:** TLS 1.2, TLS 1.3 (TLS 1.0/1.1 disabled) - **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 - **HTTP/2:** Enabled
- **HSTS:** Enabled (max-age=31536000, includeSubDomains) - **HSTS:** Enabled (max-age=31536000, includeSubDomains)
**Security Headers (Added by NPM):** **Security Headers (Added by nginx):**
```http ```http
Strict-Transport-Security: max-age=31536000; includeSubDomains Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Frame-Options: SAMEORIGIN 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 s_client -connect nordabiznes.pl:443 -servername nordabiznes.pl < /dev/null 2>/dev/null | \
openssl x509 -noout -dates -subject -issuer openssl x509 -noout -dates -subject -issuer
# Check expiry date # Check certificate status on server
curl -vI https://nordabiznes.pl 2>&1 | grep -E "(expire|issuer)" ssh maciejpi@57.128.200.27 "sudo certbot certificates"
# Test SSL configuration # Test SSL configuration
curl -I https://nordabiznes.pl/health curl -I https://nordabiznes.pl/health
@ -620,7 +447,9 @@ curl -I https://nordabiznes.pl/health
## Deployment Workflow ## 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 │ │ GIT REPOSITORIES │
│ │ │ │
│ ┌─────────────────────────────────────────────────────┐ │ │ ┌─────────────────────────────────────────────────────┐ │
│ │ GITEA @ r11-git-inpi (10.22.68.180:3000) │ │
│ │ Repository: maciejpi/nordabiz │ │
│ │ Purpose: Production deployment source │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ GITHUB @ github.com │ │ │ │ GITHUB @ github.com │ │
│ │ Repository: pienczyn/nordabiz │ │ │ │ Repository: pienczyn/nordabiz │ │
│ │ Purpose: Cloud backup, collaboration │ │ │ │ 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: │ │ 1. Deploy via rsync: │
│ ssh maciejpi@10.22.68.249 │ │ rsync -avz --exclude='.env' --exclude='venv/' \ │
│ ./ maciejpi@57.128.200.27:/var/www/nordabiznes/ │
│ │ │ │
│ 2. Pull latest code: │ 2. Restart service:
cd /var/www/nordabiznes ssh maciejpi@57.128.200.27 \
sudo -u www-data git -c http.sslVerify=false pull "sudo systemctl reload nordabiznes"
│ │ │ │
│ 3. Restart service: │ │ 3. Verify deployment: │
│ sudo systemctl restart nordabiznes │
│ │
│ 4. Verify deployment: │
│ curl -I http://localhost:5000/health │
│ curl -I https://nordabiznes.pl/health │ │ curl -I https://nordabiznes.pl/health │
└────────────────────────────────────────────────────────────────┘ └────────────────────────────────────────────────────────────────┘
``` ```
@ -674,14 +500,12 @@ curl -I https://nordabiznes.pl/health
**Deployment Checklist:** **Deployment Checklist:**
1. ✅ Code tested locally 1. ✅ Code tested locally
2. ✅ Git commit with descriptive message 2. ✅ Git commit with descriptive message
3. ✅ Push to both remotes (Gitea + GitHub) 3. ✅ Push to both remotes (GitHub + Gitea)
4. ✅ SSH to production server as `maciejpi` 4. ✅ Rsync to OVH VPS
5. ✅ Pull latest code as `www-data` user 5. ✅ Reload `nordabiznes` service
6. ✅ Restart `nordabiznes` service 6. ✅ Verify health endpoint (nordabiznes.pl)
7. ✅ Verify health endpoint (localhost:5000) 7. ✅ Check logs for errors (`journalctl -u nordabiznes`)
8. ✅ Verify public endpoint (nordabiznes.pl) 8. ✅ Update release notes in `app.py`
9. ✅ Check logs for errors (`journalctl -u nordabiznes`)
10. ✅ Update release notes in `app.py`
--- ---
@ -708,16 +532,12 @@ Content-Type: application/json
**Health Check Commands:** **Health Check Commands:**
```bash ```bash
# External check (via NPM) # External check (via nginx)
curl -I https://nordabiznes.pl/health curl -I https://nordabiznes.pl/health
# Expected: HTTP/2 200 OK # Expected: HTTP/2 200 OK
# Internal check (direct to Flask) # Localhost check (from OVH VPS)
curl -I http://10.22.68.249:5000/health ssh maciejpi@57.128.200.27 "curl -I http://localhost:5000/health"
# Expected: HTTP/1.1 200 OK
# Localhost check (from NORDABIZ-01)
curl -I http://localhost:5000/health
# Expected: HTTP/1.1 200 OK # 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';" sudo -u postgres psql -c "SELECT count(*) FROM pg_stat_activity WHERE datname='nordabiz';"
``` ```
**NPM Service:** **Nginx Service (OVH VPS):**
```bash ```bash
# Check Docker container # Check nginx status
docker ps | grep nginx-proxy-manager ssh maciejpi@57.128.200.27 "sudo systemctl status nginx"
# Should show container running
# Check NPM logs # Check nginx logs
docker logs nginx-proxy-manager_app_1 --tail 50 ssh maciejpi@57.128.200.27 "sudo tail -20 /var/log/nginx/error.log"
# Verify proxy configuration # Test nginx config
docker exec nginx-proxy-manager_app_1 \ ssh maciejpi@57.128.200.27 "sudo nginx -t"
sqlite3 /data/database.sqlite \
"SELECT forward_port FROM proxy_host WHERE id = 27;"
# Expected: 5000
``` ```
### Planned Monitoring (Not Yet Implemented) ### Planned Monitoring (Not Yet Implemented)
@ -784,7 +600,7 @@ docker exec nginx-proxy-manager_app_1 \
### PostgreSQL Database Backups ### PostgreSQL Database Backups
**Current Status:** Manual backups only **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:** **Manual Backup Procedure:**
```bash ```bash
@ -824,83 +640,27 @@ curl -I http://localhost:5000/health
--- ---
### VM Snapshots (Proxmox) ### OVH VPS Backups
**Snapshot Schedule:** **OVH Automated Backups:**
- **Daily:** 7-day retention - OVH VPS snapshot backups (configured via OVH panel)
- **Weekly:** 4-week retention - Application code backed up via git (GitHub + Gitea)
- **Monthly:** 6-month retention - Database backed up via pg_dump + offsite copy
**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
```
--- ---
## Security Configuration ## Security Configuration
### Firewall Rules (Fortigate) ### OVH VPS Firewall
**The OVH VPS uses ufw (Uncomplicated Firewall) and/or OVH firewall:**
**Inbound Rules:** **Inbound Rules:**
``` ```
Priority 1: ALLOW Public (0.0.0.0/0) → 10.22.68.250:443 (HTTPS) ALLOW 22/tcp (SSH - key-based auth only)
Priority 2: ALLOW Public (0.0.0.0/0) → 10.22.68.250:80 (HTTP redirect) ALLOW 80/tcp (HTTP - redirects to HTTPS)
Priority 100: DENY All other inbound traffic ALLOW 443/tcp (HTTPS - production traffic)
``` DENY all other inbound
**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
``` ```
### SSH Access Control ### SSH Access Control
@ -912,10 +672,6 @@ Priority 100: DENY All other outbound traffic
- SSH key-based authentication (required) - SSH key-based authentication (required)
- Password authentication disabled - Password authentication disabled
**Firewall:**
- SSH accessible from internal network only (10.22.68.0/24)
- No public SSH access
**SSH Configuration:** **SSH Configuration:**
```bash ```bash
# /etc/ssh/sshd_config # /etc/ssh/sshd_config
@ -925,6 +681,11 @@ PubkeyAuthentication yes
AllowUsers maciejpi AllowUsers maciejpi
``` ```
**SSH Access:**
```bash
ssh maciejpi@57.128.200.27
```
### Database Security ### Database Security
**PostgreSQL Access Control:** **PostgreSQL Access Control:**
@ -1008,7 +769,7 @@ curl -I https://nordabiznes.pl/health
**Diagnosis:** **Diagnosis:**
```bash ```bash
# 1. Check Gunicorn service # 1. Check Gunicorn service
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
sudo systemctl status nordabiznes sudo systemctl status nordabiznes
# If not running: sudo systemctl start nordabiznes # If not running: sudo systemctl start nordabiznes
@ -1022,7 +783,7 @@ curl -I http://localhost:5000/health
# 4. Test from NPM server # 4. Test from NPM server
ssh maciejpi@10.22.68.250 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 # Expected: HTTP/1.1 200 OK
``` ```
@ -1161,7 +922,7 @@ docker restart nginx-proxy-manager_app_1
- [ ] All IP addresses are current - [ ] All IP addresses are current
- [ ] All port numbers are correct - [ ] 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 - [ ] DNS records match actual configuration
- [ ] SSL certificate status checked - [ ] SSL certificate status checked
- [ ] Firewall rules documented accurately - [ ] Firewall rules documented accurately
@ -1192,23 +953,20 @@ docker restart nginx-proxy-manager_app_1
--- ---
**Document Status:** ✅ Complete **Document Status:** ✅ Complete
**Diagram Validated:** 2026-01-10 **Diagram Validated:** 2026-04-04
**Production Verified:** 2026-01-10 **Production Verified:** 2026-04-04 (OVH VPS migration)
**Mermaid Syntax:** v10.6+ **Mermaid Syntax:** v10.6+
**Renders in:** GitHub, GitLab, VS Code (with Mermaid extension) **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:** **Verify production:**
- **Forward Host:** 10.22.68.249
- **Forward Port:** 5000 (NOT 80!)
**Always verify after changes:**
```bash ```bash
curl -I https://nordabiznes.pl/health curl -I https://nordabiznes.pl/health
# Expected: HTTP/2 200 OK # Expected: HTTP/2 200 OK
``` ```
**See:** `docs/INCIDENT_REPORT_20260102.md` for details on port 80 incident

View File

@ -1058,15 +1058,15 @@ alembic upgrade head
**Production:** **Production:**
```bash ```bash
# SSH to NORDABIZ-01 # SSH to OVH VPS
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
# Backup database # Backup database
pg_dump nordabiz > backup_$(date +%Y%m%d).sql pg_dump nordabiz > backup_$(date +%Y%m%d).sql
# Apply migration # Apply migration
cd /var/www/nordabiznes 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 # Verify
psql -U nordabiz_app -d nordabiz -c "\dt" psql -U nordabiz_app -d nordabiz -c "\dt"

View File

@ -1,12 +1,14 @@
# Network Topology Diagram # Network Topology Diagram
**Document Version:** 1.0 **Document Version:** 1.0
**Last Updated:** 2026-01-10 **Last Updated:** 2026-04-04
**Status:** Production LIVE **Status:** Production LIVE
**Diagram Type:** Network Topology / Infrastructure Network **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 ## Overview
This document provides a **network-centric view** of the Norda Biznes Partner infrastructure. It focuses on: This document provides a **network-centric view** of the Norda Biznes Partner infrastructure. It focuses on:
@ -37,90 +39,58 @@ graph TB
%% External network %% External network
subgraph "Public Internet (Untrusted)" subgraph "Public Internet (Untrusted)"
Users["👥 External Users<br/>Website Visitors"] 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 end
%% Perimeter security %% Production (OVH VPS - direct internet access)
subgraph "Network Perimeter" subgraph "OVH Cloud (Warsaw) — PRODUCTION"
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"] 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 end
%% Internal network zones %% Staging (on-prem via FortiGate + NPM)
subgraph "INPI Internal Network (10.22.68.0/24)" 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 NPM_Server["🖥️ R11-REVPROXY-01<br/>10.22.68.250<br/>NPM (staging proxy)"]
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
%% Application Zone Staging_Server["🖥️ NORDABIZ-STAGING-01<br/>10.22.68.248<br/>Staging app + DB"]
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
%% Internal Services Zone Git_Server["🖥️ r11-git-inpi<br/>10.22.68.180<br/>Gitea :3000"]
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"]
end end
%% External services %% External services
subgraph "External APIs (Internet)" 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_Google["☁️ Google Cloud APIs<br/>Gemini AI, PageSpeed, Places"]
API_MS["☁️ Microsoft Graph API<br/>Email sending"]
API_MS["☁️ Microsoft Graph API<br/><br/>• Email sending<br/><br/>Auth: OAuth 2.0<br/>HTTPS only"] API_Brave["☁️ Brave Search API"]
API_KRS["🏛️ KRS Open API"]
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"]
end end
%% Network flows - User traffic %% Production traffic (direct to OVH VPS)
Users -->|"DNS Query<br/>nordabiznes.pl"| DNS_OVH Users -->|"DNS Query<br/>nordabiznes.pl"| DNS_OVH
DNS_OVH -->|"DNS Response<br/>85.237.177.83"| Users DNS_OVH -->|"DNS Response<br/>57.128.200.27"| Users
Users -->|"HTTPS :443<br/>HTTP :80"| Fortigate Users -->|"HTTPS :443"| VPS
%% Firewall to DMZ %% Staging traffic (via FortiGate)
Fortigate -->|"NAT + Route<br/>:443 → 10.22.68.250:443<br/>:80 → 10.22.68.250:80"| NPM_Server Fortigate -.->|"staging NAT<br/>→ NPM"| NPM_Server
NPM_Server -.->|"HTTP :5000"| Staging_Server
%% DMZ to Application Zone %% API calls from production
NPM_Server ==>|"⚠️ CRITICAL<br/>HTTP :5000<br/>(NOT port 80!)"| App_Server VPS -->|"HTTPS"| API_Google
NPM_Server -.->|"Internal DNS Query<br/>nordabiznes.inpi.local"| Internal_DNS VPS -->|"HTTPS OAuth 2.0"| API_MS
VPS -->|"HTTPS"| API_Brave
%% Application to External APIs VPS -->|"HTTPS"| API_KRS
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
%% Styling %% Styling
classDef public fill:#f9f,stroke:#333,stroke-width:2px classDef public fill:#f9f,stroke:#333,stroke-width:2px
classDef perimeter fill:#f96,stroke:#333,stroke-width:3px classDef production fill:#9f9,stroke:#333,stroke-width:3px
classDef dmz fill:#ff9,stroke:#333,stroke-width:2px classDef staging 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 external fill:#ccc,stroke:#333,stroke-width:1px classDef external fill:#ccc,stroke:#333,stroke-width:1px
class Users,DNS_OVH public class Users,DNS_OVH public
class Fortigate perimeter class VPS production
class NPM_Server dmz class Fortigate,NPM_Server,Staging_Server,Git_Server staging
class App_Server app class API_Google,API_MS,API_Brave,API_KRS external
class Git_Server,Internal_DNS internal
class API_Google,API_MS,API_Brave,API_KRS,Web_Scrapers 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:** **Components:**
- **Fortigate Firewall** - **Fortigate Firewall**
- Model: (Infrastructure-specific)
- WAN IP: 85.237.177.83 - WAN IP: 85.237.177.83
- LAN IP: 10.22.68.1 (gateway for internal network) - 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 | | 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) | | 85.237.177.83 | 80 | 10.22.68.250 | 80 | TCP | HTTP to NPM (redirect) |
**Firewall Rules:** **Note:** Production (nordabiznes.pl) DNS now points directly to OVH VPS (57.128.200.27), bypassing FortiGate entirely.
| 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)
--- ---
### 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) **Network:** 10.22.68.250/32 (single host)
@ -195,15 +150,16 @@ graph TB
- **R11-REVPROXY-01** (VM 119) - **R11-REVPROXY-01** (VM 119)
- IP: 10.22.68.250 - IP: 10.22.68.250
- Services: Nginx Proxy Manager (Docker), SSH - 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:** **Inbound Traffic:**
- From Internet (via Fortigate NAT): HTTPS :443, HTTP :80 - From Internet (via Fortigate NAT): HTTPS :443, HTTP :80
- From ADMIN_NET: SSH :22 - From ADMIN_NET: SSH :22
**Outbound Traffic:** **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) - To Internet: HTTPS (for Let's Encrypt ACME challenge, Docker image updates)
**Security Controls:** **Security Controls:**
@ -218,7 +174,7 @@ graph TB
# NPM Proxy Host Configuration (Host ID: 27) # NPM Proxy Host Configuration (Host ID: 27)
domain_names: domain_names:
- nordabiznes.pl - nordabiznes.pl
forward_host: 10.22.68.249 forward_host: 57.128.200.27
forward_port: 5000 # ⚠️ MUST be 5000, NOT 80! forward_port: 5000 # ⚠️ MUST be 5000, NOT 80!
ssl: ssl:
certificate_id: 27 certificate_id: 27
@ -230,7 +186,7 @@ ssl:
**Common Misconfiguration:** **Common Misconfiguration:**
```yaml ```yaml
# ❌ WRONG - Causes infinite redirect loop # ❌ 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:** **Correct Configuration:**
@ -246,7 +202,7 @@ curl -I https://nordabiznes.pl/health
# Expected: HTTP/2 200 (success) # Expected: HTTP/2 200 (success)
# Test internal routing (from NPM server) # 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) # 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 **Purpose:** Application hosting, business logic processing
**Network:** 10.22.68.249/32 (single host) **Network:** 57.128.200.27 (OVH VPS, public IP)
**Components:** **Components:**
- **NORDABIZ-01** (VM 249) - **OVH VPS** (inpi-vps-waw01)
- IP: 10.22.68.249 - IP: 57.128.200.27
- Internal DNS: nordabiznes.inpi.local - Services: Nginx :443/:80, Gunicorn :5000 (localhost), PostgreSQL :5432 (localhost), SSH :22
- Services: Flask/Gunicorn :5000, PostgreSQL :5432 (localhost), Nginx :80/443 (unused), SSH :22
**Security Level:** **Trusted** - Internal zone, application processing **Security Level:** **Production** - Public-facing, secured by nginx + ufw
**Inbound Traffic:** **Inbound Traffic:**
- From DMZ (10.22.68.250): HTTP :5000 (NPM → Gunicorn) - From Internet: HTTPS :443 (nginx SSL termination)
- From ADMIN_NET: SSH :22 - From Internet: HTTP :80 (redirects to HTTPS)
- SSH :22 (key-based auth only)
**Outbound Traffic:** **Outbound Traffic:**
- To External APIs: HTTPS :443 (Google, Microsoft, Brave, KRS) - 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) - To Internet: HTTP/HTTPS (web scraping ALEO.com, rejestr.io)
**Localhost Services (127.0.0.1):** **Localhost Services (127.0.0.1):**
@ -293,7 +248,7 @@ curl -I http://10.22.68.249:5000/health
**Network Configuration:** **Network Configuration:**
``` ```
IP Address: 10.22.68.249/24 IP Address: 57.128.200.27/24
Gateway: 10.22.68.1 Gateway: 10.22.68.1
DNS: 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 **Security Level:** **Trusted** - Internal services, no public exposure
**Inbound Traffic:** **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) - From ADMIN_NET: SSH :22, HTTPS :3000 (git operations)
**Outbound Traffic:** **Outbound Traffic:**
@ -365,7 +320,7 @@ DNS: 10.22.68.1
**Network Flow:** **Network Flow:**
``` ```
App Server (10.22.68.249) App Server (57.128.200.27)
→ Fortigate (10.22.68.1) → Fortigate (10.22.68.1)
→ Internet Gateway → Internet Gateway
→ External API (HTTPS :443) → 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.1 | fortigate-lan | N/A | Default gateway | Perimeter |
| 10.22.68.180 | r11-git-inpi | N/A | Gitea server | Internal Services | | 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 | | 10.22.68.250 | r11-revproxy-01 | 119 | NPM reverse proxy | DMZ |
### Reserved Ranges ### 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 | | 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 | | HTTP | NPM | 10.22.68.250 | 80 | TCP | Public (via NAT) | Redirect to HTTPS |
| **Application Services** | | **Application Services** |
| Flask/Gunicorn | NORDABIZ-01 | 10.22.68.249 | 5000 | TCP | Internal only | Web application | | Flask/Gunicorn | OVH VPS inpi-vps-waw01 | 57.128.200.27 | 5000 | TCP | Internal only | Web application |
| PostgreSQL | NORDABIZ-01 | 127.0.0.1 | 5432 | TCP | Localhost only | Database | | PostgreSQL | OVH VPS inpi-vps-waw01 | 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) | OVH VPS inpi-vps-waw01 | 57.128.200.27 | 80 | TCP | Internal only | HTTP redirect (not used) |
| Nginx (unused) | NORDABIZ-01 | 10.22.68.249 | 443 | TCP | Internal only | SSL (not used) | | Nginx (unused) | OVH VPS inpi-vps-waw01 | 57.128.200.27 | 443 | TCP | Internal only | SSL (not used) |
| **Internal Services** | | **Internal Services** |
| Gitea | r11-git-inpi | 10.22.68.180 | 3000 | TCP | Internal only | Git repository | | 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 | | NPM Admin | NPM | 10.22.68.250 | 81 | TCP | Internal only | NPM web UI |
| **Administration** | | **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 | 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 | | 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 | | Source | Destination | Protocol | Purpose | Notes |
|--------|-------------|----------|---------|-------| |--------|-------------|----------|---------|-------|
| NPM :443 | 10.22.68.249:5000 | HTTP | HTTPS requests → Flask | ✅ CORRECT | | NPM :443 | 57.128.200.27:5000 | HTTP | HTTPS requests → Flask | ✅ CORRECT |
| NPM :80 | 10.22.68.249:5000 | HTTP | HTTP requests → Flask | ✅ CORRECT | | NPM :80 | 57.128.200.27: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:80 | HTTP | HTTPS requests → Nginx | ❌ WRONG - Redirect loop! |
**Why Port 5000 is Critical:** **Why Port 5000 is Critical:**
1. NPM terminates SSL at port 443 1. NPM terminates SSL at port 443
@ -496,7 +451,7 @@ whois nordabiznes.pl
| Record Type | Name | Value | Purpose | | 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 | git.inpi.local | 10.22.68.180 | Gitea server |
| A | npm.inpi.local | 10.22.68.250 | NPM server | | A | npm.inpi.local | 10.22.68.250 | NPM server |
@ -515,7 +470,7 @@ whois nordabiznes.pl
**Configuration:** **Configuration:**
```bash ```bash
# /etc/resolv.conf on NORDABIZ-01 # /etc/resolv.conf on OVH VPS inpi-vps-waw01
nameserver 10.22.68.1 nameserver 10.22.68.1
search inpi.local search inpi.local
``` ```
@ -528,12 +483,12 @@ search inpi.local
All internal servers use **10.22.68.1** (Fortigate LAN interface) as default gateway. 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 ```bash
# ip route show # ip route show
default via 10.22.68.1 dev ens18 # All non-local traffic → Fortigate 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 127.0.0.0/8 dev lo # Localhost
``` ```
@ -550,7 +505,7 @@ Fortigate WAN (85.237.177.83:443)
↓ NAT translation ↓ NAT translation
Fortigate LAN → NPM (10.22.68.250:443) Fortigate LAN → NPM (10.22.68.250:443)
↓ SSL termination, proxy ↓ SSL termination, proxy
NPM → Flask/Gunicorn (10.22.68.249:5000) NPM → Flask/Gunicorn (57.128.200.27:5000)
↓ HTTP request processing ↓ HTTP request processing
Flask → PostgreSQL (127.0.0.1:5432) Flask → PostgreSQL (127.0.0.1:5432)
↓ SQL query ↓ SQL query
@ -569,7 +524,7 @@ Fortigate WAN → User Browser (85.237.177.83:443)
#### Example 2: Application Calls External API (Gemini AI) #### Example 2: Application Calls External API (Gemini AI)
``` ```
Flask Application (10.22.68.249) Flask Application (57.128.200.27)
↓ HTTPS :443 ↓ HTTPS :443
Default Gateway (10.22.68.1) Default Gateway (10.22.68.1)
↓ NAT + Firewall ↓ NAT + Firewall
@ -579,7 +534,7 @@ Google Gemini API (generativelanguage.googleapis.com)
↓ API response ↓ API response
Internet → Fortigate WAN Internet → Fortigate WAN
↓ NAT reverse ↓ 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 **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 #### Example 3: Git Pull for Deployment
``` ```
Flask Application (10.22.68.249) Flask Application (57.128.200.27)
↓ git pull over HTTPS :3000 ↓ git pull over HTTPS :3000
Local routing (same subnet) Local routing (same subnet)
↓ Direct connection ↓ Direct connection
@ -712,7 +667,7 @@ openssl s_client -connect nordabiznes.pl:443 -servername nordabiznes.pl
**Internal Health Checks:** **Internal Health Checks:**
```bash ```bash
# From NORDABIZ-01, check Gunicorn # From OVH VPS inpi-vps-waw01, check Gunicorn
curl -I http://127.0.0.1:5000/health curl -I http://127.0.0.1:5000/health
# Expected: HTTP/1.1 200 OK # Expected: HTTP/1.1 200 OK
@ -789,13 +744,13 @@ curl http://10.22.68.250:443
# If fails: NPM service down # If fails: NPM service down
# Check if Gunicorn is responding # 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 # If fails: Gunicorn service down
``` ```
**Resolution:** **Resolution:**
1. Restart NPM: `docker restart nginx-proxy-manager` (on R11-REVPROXY-01) 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) 3. Check Fortigate firewall rules (requires admin access)
--- ---
@ -834,7 +789,7 @@ openssl s_client -connect nordabiznes.pl:443 -servername nordabiznes.pl | openss
**Diagnosis:** **Diagnosis:**
```bash ```bash
# Check network latency # Check network latency
ping -c 10 10.22.68.249 ping -c 10 57.128.200.27
# Expected: <2ms average # Expected: <2ms average
# Check database performance # Check database performance
@ -895,7 +850,7 @@ NPM forwarding to port 80 instead of port 5000
```bash ```bash
# Check NPM configuration (from R11-REVPROXY-01) # Check NPM configuration (from R11-REVPROXY-01)
docker exec -it nginx-proxy-manager cat /data/nginx/proxy_host/27.conf 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 # 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"] 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"] GIT_NIC["Git Server NIC<br/>10.22.68.180<br/>MAC: xx:xx:xx:xx:xx:03"]
end end
@ -953,7 +908,7 @@ sequenceDiagram
participant DNS as OVH DNS participant DNS as OVH DNS
participant FW as Fortigate participant FW as Fortigate
participant NPM as NPM (10.22.68.250) 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) participant DB as PostgreSQL (127.0.0.1)
User->>DNS: DNS query: nordabiznes.pl User->>DNS: DNS query: nordabiznes.pl
@ -964,7 +919,7 @@ sequenceDiagram
FW->>NPM: HTTPS :443 (10.22.68.250) FW->>NPM: HTTPS :443 (10.22.68.250)
Note over NPM: SSL termination<br/>Decrypt HTTPS → HTTP 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 Note over App: Flask processes request
App->>DB: SQL query (localhost:5432) App->>DB: SQL query (localhost:5432)
@ -984,7 +939,7 @@ sequenceDiagram
participant User as User Browser participant User as User Browser
participant FW as Fortigate participant FW as Fortigate
participant NPM as NPM (10.22.68.250) 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 User->>FW: HTTPS :443
FW->>NPM: 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:** **PostgreSQL Configuration:**
```bash ```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/ sudo tar czf postgresql-config-backup-$(date +%Y%m%d).tar.gz /etc/postgresql/14/main/
``` ```
**Network Configuration:** **Network Configuration:**
```bash ```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/ sudo tar czf network-config-backup-$(date +%Y%m%d).tar.gz /etc/netplan/ /etc/systemd/network/
``` ```

View File

@ -1,12 +1,14 @@
# Critical Configurations Reference # Critical Configurations Reference
**Document Version:** 1.0 **Document Version:** 1.0
**Last Updated:** 2026-01-10 **Last Updated:** 2026-04-04
**Status:** Production LIVE **Status:** Production LIVE (OVH VPS)
**Diagram Type:** Configuration Reference / Operations Guide **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 ## 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: 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 **Traffic flow:** Internet -> nginx (57.128.200.27:443) -> Gunicorn (127.0.0.1:5000)
**Domains:** nordabiznes.pl, www.nordabiznes.pl
> **CRITICAL WARNING:** NPM must forward to **port 5000**, NOT port 80! **SSL:** Let's Encrypt via certbot (auto-renewal)
>
> Forwarding to port 80 causes an infinite redirect loop (ERR_TOO_MANY_REDIRECTS). **Verification:**
> This was the root cause of the 2026-01-02 production incident. ```bash
> curl -I https://nordabiznes.pl/health
> See: [INCIDENT_REPORT_20260102.md](../INCIDENT_REPORT_20260102.md) # 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 ### Complete NPM Configuration
@ -78,7 +93,7 @@ This document provides a **comprehensive reference** of all critical configurati
"www.nordabiznes.pl" "www.nordabiznes.pl"
], ],
"forward_scheme": "http", "forward_scheme": "http",
"forward_host": "10.22.68.249", "forward_host": "57.128.200.27",
"forward_port": 5000, // ⚠️ CRITICAL: Must be 5000, NOT 80! "forward_port": 5000, // ⚠️ CRITICAL: Must be 5000, NOT 80!
"certificate_id": 27, "certificate_id": 27,
"ssl_forced": true, "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 \ ssh maciejpi@10.22.68.250 "docker exec nginx-proxy-manager_app_1 \
sqlite3 /data/database.sqlite \ sqlite3 /data/database.sqlite \
\"SELECT id, domain_names, forward_host, forward_port FROM proxy_host WHERE id = 27;\"" \"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 # 2. Verify website is accessible
curl -I https://nordabiznes.pl/health curl -I https://nordabiznes.pl/health
@ -182,21 +197,22 @@ else:
## Port Mappings Reference ## Port Mappings Reference
### Complete Port Matrix ### Production Port Matrix (OVH VPS 57.128.200.27)
| Server | Service | Port | Protocol | Access | Purpose | | Server | Service | Port | Protocol | Access | Purpose |
|--------|---------|------|----------|--------|---------| |--------|---------|------|----------|--------|---------|
| **Fortigate (WAN)** | Public Gateway | 443 | HTTPS | Public | SSL entry point | | **OVH VPS** | Nginx (HTTPS) | 443 | HTTPS | Public | SSL termination + proxy |
| Fortigate (WAN) | HTTP Redirect | 80 | HTTP | Public | Redirect to HTTPS | | OVH VPS | Nginx (HTTP) | 80 | HTTP | Public | Redirect to HTTPS |
| **R11-REVPROXY-01** | NPM Proxy | 443 | HTTPS | LAN | SSL termination | | OVH VPS | **Gunicorn/Flask** | **5000** | HTTP | **Localhost** | **Application** |
| R11-REVPROXY-01 | NPM Proxy | 80 | HTTP | LAN | HTTP → HTTPS redirect | | OVH VPS | PostgreSQL | 5432 | TCP | Localhost | Database |
| R11-REVPROXY-01 | NPM Admin | 81 | HTTP | LAN | Admin panel | | OVH VPS | SSH | 22 | SSH | Public (key-only) | Remote management |
| R11-REVPROXY-01 | SSH | 22 | SSH | Admin | Remote management |
| **NORDABIZ-01** | **Flask/Gunicorn** | **5000** | **HTTP** | **LAN** | **Application (CRITICAL!)** | ### Staging Port Matrix (on-prem, via FortiGate + NPM)
| NORDABIZ-01 | PostgreSQL | 5432 | TCP | Localhost | Database | | **OVH VPS inpi-vps-waw01** | **Flask/Gunicorn** | **5000** | **HTTP** | **LAN** | **Application (CRITICAL!)** |
| NORDABIZ-01 | Nginx (System) | 80 | HTTP | LAN | ⚠️ DO NOT USE (causes redirect loop) | | OVH VPS inpi-vps-waw01 | PostgreSQL | 5432 | TCP | Localhost | Database |
| NORDABIZ-01 | Nginx (System) | 443 | HTTPS | LAN | ⚠️ DO NOT USE | | OVH VPS inpi-vps-waw01 | Nginx (System) | 80 | HTTP | LAN | ⚠️ DO NOT USE (causes redirect loop) |
| NORDABIZ-01 | SSH | 22 | SSH | Admin | Remote management | | 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** | Gitea HTTPS | 3000 | HTTPS | LAN | Git repository |
| r11-git-inpi | SSH | 22 | SSH | Admin | Remote management | | r11-git-inpi | SSH | 22 | SSH | Admin | Remote management |
@ -210,9 +226,9 @@ Public → Internal NAT Mappings:
Internal → Internal Routing: Internal → Internal Routing:
10.22.68.250:* → 10.22.68.249:5000 (NPM → Flask) ⚠️ CRITICAL PORT 10.22.68.250:* → 57.128.200.27:5000 (NPM → Flask) ⚠️ CRITICAL PORT
10.22.68.249:* → 127.0.0.1:5432 (Flask → PostgreSQL) 57.128.200.27:* → 127.0.0.1:5432 (Flask → PostgreSQL)
10.22.68.249:* → 10.22.68.180:3000 (Flask → Gitea) 57.128.200.27:* → 10.22.68.180:3000 (Flask → Gitea)
``` ```
### Port Usage by Zone ### Port Usage by Zone
@ -226,7 +242,7 @@ Internal → Internal Routing:
- Port 80 (HTTP) - NPM HTTP redirect - Port 80 (HTTP) - NPM HTTP redirect
- Port 81 (HTTP) - NPM admin panel (internal only) - 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 5000 (HTTP) - Flask/Gunicorn application** ⚠️ CRITICAL
- Port 5432 (TCP) - PostgreSQL (localhost only) - Port 5432 (TCP) - PostgreSQL (localhost only)
- Port 80/443 (HTTP/HTTPS) - System nginx (DO NOT USE for app) - Port 80/443 (HTTP/HTTPS) - System nginx (DO NOT USE for app)
@ -426,7 +442,7 @@ FLASK_ENV=development
### Production Database ### Production Database
**Server:** NORDABIZ-01 (10.22.68.249) **Server:** OVH VPS (57.128.200.27, hostname: inpi-vps-waw01)
**DBMS:** PostgreSQL 14 **DBMS:** PostgreSQL 14
**Database Name:** `nordabiz` **Database Name:** `nordabiz`
**Port:** 5432 (localhost only) **Port:** 5432 (localhost only)
@ -545,7 +561,7 @@ sudo -u postgres psql -U nordabiz_app nordabiz < /tmp/nordabiz_backup.sql
```bash ```bash
gunicorn \ gunicorn \
--bind 0.0.0.0:5000 \ --bind 127.0.0.1:5000 \
--workers 4 \ --workers 4 \
--timeout 120 \ --timeout 120 \
--max-requests 1000 \ --max-requests 1000 \
@ -561,7 +577,7 @@ gunicorn \
# Recommended workers formula # Recommended workers formula
workers = (2 * num_cpu_cores) + 1 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 = (2 * 4) + 1 = 9 # Maximum
workers = 4 # Current (conservative) workers = 4 # Current (conservative)
``` ```
@ -596,13 +612,13 @@ Requires=postgresql.service
[Service] [Service]
Type=simple Type=simple
User=www-data User=maciejpi
Group=www-data Group=maciejpi
WorkingDirectory=/var/www/nordabiznes WorkingDirectory=/var/www/nordabiznes
Environment="PATH=/var/www/nordabiznes/venv/bin" Environment="PATH=/var/www/nordabiznes/venv/bin"
EnvironmentFile=/var/www/nordabiznes/.env EnvironmentFile=/var/www/nordabiznes/.env
ExecStart=/var/www/nordabiznes/venv/bin/gunicorn \ ExecStart=/var/www/nordabiznes/venv/bin/gunicorn \
--bind 0.0.0.0:5000 \ --bind 127.0.0.1:5000 \
--workers 4 \ --workers 4 \
--timeout 120 \ --timeout 120 \
--max-requests 1000 \ --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) | | **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) | | **origin** | `git@github.com:pienczyn/nordabiz.git` | GitHub (cloud backup) |
### Production Git Configuration ### Production Deployment (OVH VPS — no git)
**Repository Location:** `/var/www/nordabiznes/` **Deployment method:** rsync from development machine (no git on OVH VPS)
**User:** `www-data`
**Branch:** `master`
**Git Configuration (`/var/www/nordabiznes/.git/config`):** **Application Location:** `/var/www/nordabiznes/`
```ini **User:** `maciejpi`
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "inpi"] ### Deployment Workflow
url = https://10.22.68.180:3000/maciejpi/nordabiz.git
fetch = +refs/heads/*:refs/remotes/inpi/*
[remote "origin"] ```bash
url = git@github.com:pienczyn/nordabiz.git # On development machine (Mac)
fetch = +refs/heads/*:refs/remotes/origin/* git push origin master # Push to GitHub
git push inpi master # Push to internal Gitea
[branch "master"] # Deploy to production via rsync
remote = inpi rsync -avz --exclude='.env' --exclude='venv/' --exclude='.git/' \
merge = refs/heads/master ./ maciejpi@57.128.200.27:/var/www/nordabiznes/
[http] # Restart service
sslVerify = false # Required for self-signed Gitea cert 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 **Server:** r11-git-inpi
**IP:** 10.22.68.180 **IP:** 10.22.68.180
@ -730,42 +741,6 @@ sudo systemctl restart nordabiznes
**User:** `maciejpi` **User:** `maciejpi`
**Repository:** `maciejpi/nordabiz` **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 ### SSH Keys for Git Access
**Production Server:** **Production Server:**
@ -881,16 +856,16 @@ Policy 5: Default Deny
Action: 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:** **Default Policy:**
```bash ```bash
# Check iptables status # Check ufw status
sudo iptables -L -n -v sudo ufw status verbose
# Expected: Mostly ACCEPT policies (Fortigate handles filtering) # Expected rules: ALLOW 22/tcp, 80/tcp, 443/tcp
``` ```
### PostgreSQL Access Control ### PostgreSQL Access Control
@ -939,7 +914,7 @@ sudo -u postgres psql -U nordabiz_app nordabiz < \
- **Location:** Proxmox Backup Server - **Location:** Proxmox Backup Server
- **Schedule:** Weekly - **Schedule:** Weekly
- **Retention:** 4 weeks - **Retention:** 4 weeks
- **VM ID:** 249 (NORDABIZ-01) - **VM ID:** 249 (OVH VPS inpi-vps-waw01)
### Configuration Backups ### Configuration Backups
@ -1043,7 +1018,7 @@ sudo chmod 600 /home/maciejpi/backups/.env.backup
4. **Apply change to production** 4. **Apply change to production**
```bash ```bash
# SSH to production # SSH to production
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
# Pull changes # Pull changes
cd /var/www/nordabiznes 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;\"" \"SELECT id, domain_names, forward_host, forward_port FROM proxy_host WHERE id = 27;\""
# Expected output: # 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! # MUST BE 5000!
@ -1193,7 +1168,7 @@ sudo journalctl -u nordabiznes -p err -n 20
| Server | IP | SSH User | Purpose | | 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-REVPROXY-01 | 10.22.68.250 | `maciejpi` | NPM proxy |
| r11-git-inpi | 10.22.68.180 | `maciejpi` | Gitea repository | | 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 curl -I https://nordabiznes.pl/health
# 2. Check service status # 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 # 3. Check NPM proxy configuration
ssh maciejpi@10.22.68.250 "docker exec nginx-proxy-manager_app_1 \ 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;\"" \"SELECT forward_port FROM proxy_host WHERE id = 27;\""
# 4. View recent errors # 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 # 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 ### Documentation Resources
@ -1238,7 +1213,7 @@ ssh maciejpi@10.22.68.249 "sudo systemctl restart nordabiznes"
``` ```
Proxy Host ID: 27 Proxy Host ID: 27
Domains: nordabiznes.pl, www.nordabiznes.pl 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) SSL: Let's Encrypt (auto-renew)
``` ```
@ -1271,7 +1246,7 @@ curl -I https://nordabiznes.pl/health # Test health
### Quick Reference: Emergency Rollback ### Quick Reference: Emergency Rollback
```bash ```bash
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
cd /var/www/nordabiznes cd /var/www/nordabiznes
sudo systemctl stop nordabiznes sudo systemctl stop nordabiznes
sudo -u www-data git reset --hard HEAD~1 sudo -u www-data git reset --hard HEAD~1

View File

@ -1,8 +1,8 @@
# Security Architecture # Security Architecture
**Document Version:** 1.0 **Document Version:** 1.0
**Last Updated:** 2026-01-10 **Last Updated:** 2026-04-04
**Status:** Production LIVE **Status:** Production LIVE (OVH VPS)
**Diagram Type:** Security Architecture / Threat Model **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"] 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 end
subgraph "Zone 1: Network Perimeter (SECURITY BOUNDARY)" subgraph "Zone 1: Nginx Reverse Proxy (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"] 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 end
subgraph "Zone 2: DMZ - Reverse Proxy (SEMI-TRUSTED)" subgraph "Zone 2: Application Zone (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"] 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 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"]
end end
subgraph "Zone 4: Data Zone (HIGHLY TRUSTED)" 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 end
subgraph "Zone 5: External APIs (THIRD-PARTY)" 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"] 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 end
Internet -->|"HTTPS :443<br/>HTTP :80"| Fortigate Internet -->|"HTTPS :443<br/>HTTP :80"| Nginx
Fortigate -->|"NAT + Filter<br/>Allow: 443, 80"| DMZ Nginx -->|"HTTP :5000<br/>(localhost)"| AppZone
DMZ -->|"HTTP :5000<br/>(internal network)"| AppZone
AppZone -->|"PostgreSQL :5432<br/>(localhost)"| DataZone AppZone -->|"PostgreSQL :5432<br/>(localhost)"| DataZone
AppZone -->|"HTTPS<br/>(API requests)"| APIs AppZone -->|"HTTPS<br/>(API requests)"| APIs
style Internet fill:#ff6b6b,color:#fff style Internet fill:#ff6b6b,color:#fff
style Fortigate fill:#f59e0b,color:#fff style Nginx fill:#f59e0b,color:#fff
style DMZ fill:#fbbf24,color:#000
style AppZone fill:#10b981,color:#fff style AppZone fill:#10b981,color:#fff
style DataZone fill:#3b82f6,color:#fff style DataZone fill:#3b82f6,color:#fff
style APIs fill:#8b5cf6,color:#fff style APIs fill:#8b5cf6,color:#fff
@ -98,35 +92,31 @@ graph TB
| Boundary | Between Zones | Security Controls | Threat Mitigation | | Boundary | Between Zones | Security Controls | Threat Mitigation |
|----------|---------------|-------------------|-------------------| |----------|---------------|-------------------|-------------------|
| **External → Perimeter** | Internet → Fortigate | NAT, port filtering, stateful firewall | DDoS, port scanning, unauthorized access | | **External → Proxy** | Internet → Nginx | SSL termination, request filtering, HSTS | DDoS, port scanning, unauthorized access |
| **Perimeter → DMZ** | Fortigate → NPM | Port restrictions (443, 80), SSL enforcement | Man-in-the-middle, protocol attacks | | **Proxy → Application** | Nginx → Gunicorn | Localhost-only binding (127.0.0.1:5000) | Lateral movement, privilege escalation |
| **DMZ → Application** | NPM → Flask | Internal network isolation, port 5000 only | Lateral movement, privilege escalation |
| **Application → Data** | Flask → PostgreSQL | Localhost-only binding, role-based access | SQL injection, unauthorized data access | | **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 | | **Application → Internet** | Flask → External APIs | HTTPS/TLS, API key authentication, rate limiting | API key theft, cost overrun, data leakage |
### 1.3 Network Segmentation ### 1.3 Network Segmentation
**Physical Segmentation:** **Production (OVH VPS):**
- **10.22.68.0/24** - Internal INPI network (RFC 1918 private addressing) - **57.128.200.27** - Public IP (OVH VPS, direct internet access)
- **85.237.177.83** - Public IP (NAT at Fortigate) - **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:** **Staging (on-prem):**
- **DMZ:** 10.22.68.250 (reverse proxy only) - **10.22.68.0/24** - Internal INPI network
- **Application:** 10.22.68.249 (Flask app, no direct Internet access for incoming) - **85.237.177.83** - Public IP (NAT at FortiGate for staging)
- **Data:** 10.22.68.249:5432 (localhost binding, no network exposure) - FortiGate + NPM (10.22.68.250) for staging.nordabiznes.pl
**Firewall Rules (Fortigate):** **OVH VPS Firewall (ufw):**
``` ```
# Inbound (WAN → LAN) # Production firewall rules
allow tcp/443 from ANY to 10.22.68.250 # HTTPS to NPM allow tcp/443 from ANY # HTTPS
allow tcp/80 from ANY to 10.22.68.250 # HTTP to NPM (redirects to HTTPS) allow tcp/80 from ANY # HTTP (redirect to HTTPS)
allow tcp/22 from ADMIN_NET to 10.22.68.249 # SSH (admin only) allow tcp/22 from ANY # SSH (key-based auth only)
deny all from ANY to ANY # Default deny deny all other inbound
# 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)
``` ```
### 1.4 Attack Surface ### 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:** **Reduced Attack Surface:**
- PostgreSQL: Localhost-only binding (no network exposure) - PostgreSQL: Localhost-only binding (no network exposure)
- SSH: Restricted to admin network (firewall rule) - Gunicorn: Localhost-only binding (127.0.0.1:5000, not exposed to internet)
- NPM Admin UI: Port 81 (internal network only) - SSH: Key-based authentication only (password auth disabled)
--- ---
@ -670,7 +660,7 @@ def ratelimit_handler(e):
#### 4.4.1 Security Headers #### 4.4.1 Security Headers
**HTTP Security Headers (Configured in NPM):** **HTTP Security Headers (Configured in nginx):**
``` ```
# HSTS (HTTP Strict Transport Security) # HSTS (HTTP Strict Transport Security)
@ -693,7 +683,7 @@ Referrer-Policy: strict-origin-when-cross-origin
``` ```
**Current Status:** **Current Status:**
- ✅ HSTS enabled (NPM configuration) - ✅ HSTS enabled (nginx configuration)
- ✅ X-Frame-Options: SAMEORIGIN - ✅ X-Frame-Options: SAMEORIGIN
- ✅ X-Content-Type-Options: nosniff - ✅ X-Content-Type-Options: nosniff
- ❌ Content-Security-Policy (not yet implemented - requires frontend refactoring) - ❌ 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 Provider:** Let's Encrypt (free, auto-renewal)
- **Certificate Type:** RSA 2048-bit - **Certificate Type:** RSA 2048-bit
- **TLS Protocols:** TLS 1.2, TLS 1.3 only - **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) - **HSTS:** Enabled (max-age=31536000)
**Cipher Suites (Modern):** **Cipher Suites (Modern):**
@ -716,11 +706,11 @@ ECDHE-RSA-AES128-GCM-SHA256
ECDHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-AES256-GCM-SHA384
``` ```
**SSL Termination:** At NPM reverse proxy (10.22.68.250) **SSL Termination:** At nginx on OVH VPS (57.128.200.27)
**Internal Communication:** HTTP (10.22.68.250 → 10.22.68.249:5000) **Internal Communication:** HTTP (nginx → 127.0.0.1:5000)
**Certificate Renewal:** **Certificate Renewal:**
- Automatic via NPM + Let's Encrypt - Automatic via certbot + Let's Encrypt (on OVH VPS)
- Renewal frequency: Every 90 days - Renewal frequency: Every 90 days
- Grace period: 30 days before expiry - Grace period: 30 days before expiry
@ -851,16 +841,17 @@ SMTP_PASSWORD=<password>
#### 5.2.1 External Attack Surface (Internet-facing) #### 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) - **Exposed Services:** HTTPS (443), HTTP (80, redirects to HTTPS)
- **Attack Vectors:** - **Attack Vectors:**
- DDoS attacks (mitigated by Fortigate) - DDoS attacks (mitigated by OVH DDoS protection)
- SSL/TLS vulnerabilities (mitigated by modern cipher suites) - SSL/TLS vulnerabilities (mitigated by modern cipher suites)
- HTTP request smuggling (mitigated by NPM validation) - HTTP request smuggling (mitigated by nginx validation)
- **Mitigation:** - **Mitigation:**
- Fortigate stateful firewall + DDoS protection - OVH network-level DDoS protection
- Let's Encrypt TLS 1.2/1.3 only - Let's Encrypt TLS 1.2/1.3 only
- NPM request filtering ("block exploits" enabled) - ufw firewall on VPS
- nginx request filtering
**Public Endpoints:** **Public Endpoints:**
- `/` (Company directory) - `/` (Company directory)
@ -909,7 +900,7 @@ SMTP_PASSWORD=<password>
#### 5.2.4 Database Attack Surface #### 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) - **Binding:** Localhost only (127.0.0.1)
- **Authentication:** Password-based (pg_hba.conf) - **Authentication:** Password-based (pg_hba.conf)
- **Encryption:** SSL/TLS for connections (planned) - **Encryption:** SSL/TLS for connections (planned)
@ -1249,12 +1240,12 @@ def send_chat_message(id):
# Inbound rules # Inbound rules
allow tcp/443 from ANY to 10.22.68.250 # HTTPS to NPM 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/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 deny all from ANY to ANY # Default deny
# Outbound rules # Outbound rules
allow tcp/443 from 10.22.68.249 to ANY # API calls (HTTPS) allow tcp/443 from 57.128.200.27 to ANY # API calls (HTTPS)
allow tcp/80 from 10.22.68.249 to ANY # HTTP (rare) 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 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:** **Network Segmentation:**
- **DMZ Zone:** 10.22.68.250 (NPM only) - **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) - **Internal Services:** 10.22.68.180 (Git server)
**Network Security Best Practices:** **Network Security Best Practices:**
@ -1558,8 +1549,8 @@ ssh maciejpi@10.22.68.250
docker ps | grep npm docker ps | grep npm
# 4. Check network connectivity # 4. Check network connectivity
ping 10.22.68.249 ping 57.128.200.27
curl http://10.22.68.249:5000/health curl http://57.128.200.27:5000/health
``` ```
**Recovery:** **Recovery:**
@ -1590,7 +1581,7 @@ docker restart <npm-container-id>
**Lessons Learned:** **Lessons Learned:**
- Document critical configurations (port mappings) in architecture docs - 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 - Implement monitoring to detect redirect loops
--- ---

View File

@ -1,7 +1,7 @@
# 11. Troubleshooting Guide # 11. Troubleshooting Guide
**Document Type:** Operations Guide **Document Type:** Operations Guide
**Last Updated:** 2026-01-10 **Last Updated:** 2026-04-04
**Maintainer:** DevOps Team **Maintainer:** DevOps Team
--- ---
@ -80,7 +80,7 @@ graph TD
- Browser error: `ERR_TOO_MANY_REDIRECTS` - Browser error: `ERR_TOO_MANY_REDIRECTS`
- Portal completely inaccessible via https://nordabiznes.pl - 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 - Affects 100% of external users
#### Root Cause #### 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;" "SELECT id, domain_names, forward_host, forward_port FROM proxy_host WHERE id = 27;"
# Expected output: # 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! # If forward_port shows 80 → PROBLEM FOUND!
# 2. Test backend directly # 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 # 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 # 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) # 2. Navigate to: Proxy Hosts → nordabiznes.pl (ID 27)
# 3. Edit configuration: # 3. Edit configuration:
# - Forward Hostname/IP: 10.22.68.249 # - Forward Hostname/IP: 57.128.200.27
# - Forward Port: 5000 (CRITICAL!) # - Forward Port: 5000 (CRITICAL!)
# - Scheme: http # - Scheme: http
# 4. Save and test # 4. Save and test
@ -142,7 +142,7 @@ NPM_URL = "http://10.22.68.250:81/api"
data = { data = {
"domain_names": ["nordabiznes.pl", "www.nordabiznes.pl"], "domain_names": ["nordabiznes.pl", "www.nordabiznes.pl"],
"forward_scheme": "http", "forward_scheme": "http",
"forward_host": "10.22.68.249", "forward_host": "57.128.200.27",
"forward_port": 5000, # CRITICAL: Must be 5000! "forward_port": 5000, # CRITICAL: Must be 5000!
"certificate_id": 27, "certificate_id": 27,
"ssl_forced": True, "ssl_forced": True,
@ -191,14 +191,14 @@ docker logs nginx-proxy-manager_app_1 --tail 20
#### Root Causes #### Root Causes
1. Flask/Gunicorn service stopped 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 3. Firewall blocking port 5000
#### Diagnosis #### Diagnosis
```bash ```bash
# 1. Check Flask service status # 1. Check Flask service status
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
sudo systemctl status nordabiznes sudo systemctl status nordabiznes
# 2. Check if port 5000 is listening # 2. Check if port 5000 is listening
@ -250,10 +250,10 @@ sudo -u www-data /var/www/nordabiznes/venv/bin/python3 app.py
```bash ```bash
# Test connectivity from NPM to backend # Test connectivity from NPM to backend
ssh maciejpi@10.22.68.250 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 # Check firewall rules
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
sudo iptables -L -n | grep 5000 sudo iptables -L -n | grep 5000
``` ```
@ -287,7 +287,7 @@ curl -I https://nordabiznes.pl/health
```bash ```bash
# 1. Check Gunicorn worker status # 1. Check Gunicorn worker status
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
ps aux | grep gunicorn ps aux | grep gunicorn
# Look for zombie workers or high CPU usage # 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) # 2. Check internal DNS (inpi.local)
nslookup nordabiznes.inpi.local 10.22.68.1 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 # 3. Test from different locations
curl -I -H "Host: nordabiznes.pl" http://85.237.177.83/health 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 ```bash
# 1. Check service status # 1. Check service status
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
sudo systemctl status nordabiznes sudo systemctl status nordabiznes
# 2. Check recent logs # 2. Check recent logs
@ -609,7 +609,7 @@ curl http://localhost:5000/health
# Look for JavaScript errors # Look for JavaScript errors
# 2. Check Flask logs # 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 sudo journalctl -u nordabiznes -n 50 --no-pager | grep ERROR
# 3. Check template rendering # 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 curl "https://nordabiznes.pl/search?q=test" -v
# 2. Check search_service.py logs # 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 sudo journalctl -u nordabiznes -n 100 | grep -i search
# 3. Test database FTS # 3. Test database FTS
@ -786,7 +786,7 @@ Quick check:
```bash ```bash
# 1. Verify Gemini API key # 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 sudo -u www-data cat /var/www/nordabiznes/.env | grep GEMINI_API_KEY
# Should not be empty # Should not be empty
@ -818,7 +818,7 @@ curl -H "x-goog-api-key: YOUR_API_KEY" \
```bash ```bash
# 1. Check PostgreSQL service # 1. Check PostgreSQL service
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
sudo systemctl status postgresql sudo systemctl status postgresql
# 2. Check PostgreSQL is listening # 2. Check PostgreSQL is listening
@ -986,7 +986,7 @@ SELECT * FROM companies WHERE name ILIKE '%test%' LIMIT 10;
```bash ```bash
# 1. Check disk usage # 1. Check disk usage
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
df -h df -h
# Check /var/lib/postgresql usage # Check /var/lib/postgresql usage
@ -1140,7 +1140,7 @@ sudo -u www-data psql -h localhost -U nordabiz_app -d nordabiz
```bash ```bash
# 1. Check API usage in database # 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 sudo -u www-data psql -h localhost -U nordabiz_app -d nordabiz
-- Gemini API usage today -- 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"}]}]}' -d '{"contents":[{"parts":[{"text":"Hello, test"}]}]}'
# 2. Check Flask logs for Gemini errors # 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 sudo journalctl -u nordabiznes -n 100 | grep -i gemini
# 3. Check conversation ownership # 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" curl "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=https://nordabiznes.pl&key=$PAGESPEED_KEY"
# 2. Check audit logs # 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 sudo journalctl -u nordabiznes -n 100 | grep -i pagespeed
# 3. Check recent audits # 3. Check recent audits
@ -1495,7 +1495,7 @@ sudo systemctl restart nordabiznes
```bash ```bash
# 1. Check user exists and is active # 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 sudo -u www-data psql -h localhost -U nordabiz_app -d nordabiz
SELECT id, email, is_active, email_verified, failed_login_attempts 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/ time curl -I https://nordabiznes.pl/
# 2. Check Gunicorn worker status # 2. Check Gunicorn worker status
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
ps aux | grep gunicorn ps aux | grep gunicorn
# Look for: worker processes (should be 4-8) # Look for: worker processes (should be 4-8)
@ -1917,7 +1917,7 @@ done
```bash ```bash
# 1. Check memory usage # 1. Check memory usage
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
free -h free -h
# 2. Check which process using memory # 2. Check which process using memory
@ -2006,7 +2006,7 @@ echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
```bash ```bash
# 1. Check CPU usage # 1. Check CPU usage
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
top -n 1 top -n 1
# Look for processes using >80% CPU # Look for processes using >80% CPU
@ -2093,7 +2093,7 @@ docker ps | grep nginx-proxy-manager
# Should show: Up X hours # Should show: Up X hours
# Flask service health # Flask service health
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
sudo systemctl status nordabiznes sudo systemctl status nordabiznes
# Should show: active (running) # Should show: active (running)
``` ```
@ -2172,7 +2172,7 @@ ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
```bash ```bash
# Check last backup # Check last backup
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
ls -lah /backup/nordabiz/ | head -10 ls -lah /backup/nordabiz/ | head -10
# Expected: Daily backups (.sql files) # Expected: Daily backups (.sql files)
@ -2204,7 +2204,7 @@ curl -I https://nordabiznes.pl/health
# If fails, proceed # If fails, proceed
# 2. Check from internal network # 2. Check from internal network
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
curl -I http://localhost:5000/health curl -I http://localhost:5000/health
# If this works → Network/NPM issue # If this works → Network/NPM issue
# If this fails → Application issue # If this fails → Application issue
@ -2232,7 +2232,7 @@ docker exec nginx-proxy-manager_app_1 \
sqlite3 /data/database.sqlite \ sqlite3 /data/database.sqlite \
"SELECT id, forward_host, forward_port FROM proxy_host WHERE id = 27;" "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 # 3. Check Fortigate NAT
# Access Fortigate admin panel # Access Fortigate admin panel
@ -2243,7 +2243,7 @@ docker exec nginx-proxy-manager_app_1 \
```bash ```bash
# 1. Check Flask service # 1. Check Flask service
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
sudo systemctl status nordabiznes sudo systemctl status nordabiznes
# If failed, check logs # If failed, check logs
@ -2361,7 +2361,7 @@ pg_restore -t companies -d nordabiz /backup/nordabiz/latest.sql
```bash ```bash
# 1. ISOLATE the server # 1. ISOLATE the server
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
# Block all incoming traffic except your IP # Block all incoming traffic except your IP
sudo iptables -A INPUT -s YOUR_IP -j ACCEPT sudo iptables -A INPUT -s YOUR_IP -j ACCEPT
@ -2447,11 +2447,11 @@ sudo systemctl restart nordabiznes
echo "=== Application Health ===" && \ echo "=== Application Health ===" && \
curl -I https://nordabiznes.pl/health && \ curl -I https://nordabiznes.pl/health && \
echo -e "\n=== Service Status ===" && \ 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 ===" && \ 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 ===" && \ echo -e "\n=== Server Load ===" && \
ssh maciejpi@10.22.68.249 "uptime" ssh maciejpi@57.128.200.27 "uptime"
``` ```
### 10.2 NPM Proxy Diagnostics ### 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" ssh maciejpi@10.22.68.250 "docker ps | grep nginx-proxy-manager"
# Test backend from NPM server # 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 ### 10.3 Database Diagnostics
@ -2508,10 +2508,10 @@ for i in {1..10}; do
done done
# Server resource usage # 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 # 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 # Network connectivity
ping -c 5 nordabiznes.pl ping -c 5 nordabiznes.pl
@ -2525,7 +2525,7 @@ echo | openssl s_client -servername nordabiznes.pl -connect nordabiznes.pl:443 2
```bash ```bash
# Test all external APIs # Test all external APIs
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
# Gemini API # Gemini API
GEMINI_KEY=$(sudo -u www-data grep GEMINI_API_KEY .env | cut -d= -f2) 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 ```bash
# Check current deployment version # 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 # 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 # 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 # 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"
``` ```
--- ---

View File

@ -157,12 +157,12 @@ graph TB
NPM[NPM Reverse Proxy<br/>:443 SSL/TLS<br/>⚠️ Forwards to :5000] NPM[NPM Reverse Proxy<br/>:443 SSL/TLS<br/>⚠️ Forwards to :5000]
end 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] Flask[Flask/Gunicorn<br/>:5000 HTTP<br/>90+ routes, 7 services]
Scripts[Background Scripts<br/>SEO, Social, News] Scripts[Background Scripts<br/>SEO, Social, News]
end end
subgraph "Data Zone - 10.22.68.249" subgraph "Data Zone - 57.128.200.27"
PostgreSQL[(PostgreSQL<br/>:5432<br/>36 tables, 11 domains)] PostgreSQL[(PostgreSQL<br/>:5432<br/>36 tables, 11 domains)]
end end
@ -252,7 +252,7 @@ graph TB
> ⚠️ **Database Access** > ⚠️ **Database Access**
> >
> PostgreSQL only accepts connections from **localhost (127.0.0.1)** for security. > 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) > **See:** [Critical Configurations](08-critical-configurations.md#database-configuration)
@ -331,8 +331,8 @@ erDiagram
```mermaid ```mermaid
graph LR graph LR
Internet((Internet)) -->|HTTPS| NPM[NPM Proxy<br/>10.22.68.250:443] Internet((Internet)) -->|HTTPS| NPM[NPM Proxy<br/>10.22.68.250:443]
NPM -->|Port 5000| Flask[Flask App<br/>10.22.68.249:5000] NPM -->|Port 5000| Flask[Flask App<br/>57.128.200.27:5000]
Flask -->|Port 5432| DB[(PostgreSQL<br/>10.22.68.249:5432)] Flask -->|Port 5432| DB[(PostgreSQL<br/>57.128.200.27:5432)]
``` ```
#### How to View and Edit Diagrams #### How to View and Edit Diagrams
@ -390,7 +390,7 @@ Create an HTML file with Mermaid script:
**2. Naming Conventions** **2. Naming Conventions**
- Use descriptive node labels: `[Flask App]` not `[App]` - 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 - Use consistent colors/styling across related diagrams
**3. Comments and Documentation** **3. Comments and Documentation**
@ -443,8 +443,8 @@ graph LR
```mermaid ```mermaid
%% Use <br/> for line breaks in labels %% Use <br/> for line breaks in labels
graph TD graph TD
A[Flask App<br/>10.22.68.249<br/>Port 5000] A[Flask App<br/>57.128.200.27<br/>Port 5000]
B[PostgreSQL<br/>10.22.68.249<br/>Port 5432] B[PostgreSQL<br/>57.128.200.27<br/>Port 5432]
A --> B A --> B
``` ```
@ -586,14 +586,14 @@ sudo journalctl -u nordabiznes -f
sudo systemctl status postgresql sudo systemctl status postgresql
# Deploy updates (after git push) # 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 ### Key File Locations
- **Application:** `/var/www/nordabiznes/` - **Application:** `/var/www/nordabiznes/`
- **Environment:** `/var/www/nordabiznes/.env` - **Environment:** `/var/www/nordabiznes/.env`
- **Logs:** `/var/log/nordabiznes/` (check systemd journal) - **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) - **Backups:** Proxmox Backup Server (VM snapshots)
## Quick Links ## Quick Links

View File

@ -865,7 +865,7 @@ PageSpeed API quota remaining: 24,950
```bash ```bash
# Connect to server # Connect to server
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
# Navigate to application directory # Navigate to application directory
cd /var/www/nordabiznes 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' DATABASE_URL = 'postgresql://nordabiz_app:NordaBiz2025Secure@127.0.0.1:5432/nordabiz'
# WRONG (PostgreSQL doesn't accept external connections): # 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) ### 7.5 Cron Job (Automated Audits)

View File

@ -20,7 +20,7 @@ This document describes the **complete HTTP request flow** for the Norda Biznes
**Key Infrastructure:** **Key Infrastructure:**
- **Public Entry:** 85.237.177.83:443 (Fortigate NAT) - **Public Entry:** 85.237.177.83:443 (Fortigate NAT)
- **Reverse Proxy:** NPM on 10.22.68.250:443 (SSL termination) - **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 - **Protocol Flow:** HTTPS → NPM → HTTP → Flask → HTTP → NPM → HTTPS
**⚠️ CRITICAL CONFIGURATION:** **⚠️ CRITICAL CONFIGURATION:**
@ -47,7 +47,7 @@ sequenceDiagram
participant Browser participant Browser
participant Fortigate as 🛡️ Fortigate Firewall<br/>85.237.177.83 participant Fortigate as 🛡️ Fortigate Firewall<br/>85.237.177.83
participant NPM as 🔒 NPM Reverse Proxy<br/>10.22.68.250:443 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 participant DB as 💾 PostgreSQL<br/>localhost:5432
Note over User,DB: SUCCESSFUL REQUEST FLOW Note over User,DB: SUCCESSFUL REQUEST FLOW
@ -87,8 +87,8 @@ sequenceDiagram
actor User actor User
participant Browser participant Browser
participant NPM as 🔒 NPM Reverse Proxy<br/>10.22.68.250:443 participant NPM as 🔒 NPM Reverse Proxy<br/>10.22.68.250:443
participant NginxSys as ⚠️ Nginx System<br/>10.22.68.249:80 participant NginxSys as ⚠️ Nginx System<br/>57.128.200.27:80
participant Flask as 🌐 Flask/Gunicorn<br/>10.22.68.249:5000 participant Flask as 🌐 Flask/Gunicorn<br/>57.128.200.27:5000
Note over User,Flask: FAILED REQUEST FLOW (REDIRECT LOOP) 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: -- Result:
domain_names: ["nordabiznes.pl", "www.nordabiznes.pl"] domain_names: ["nordabiznes.pl", "www.nordabiznes.pl"]
forward_scheme: "http" forward_scheme: "http"
forward_host: "10.22.68.249" forward_host: "57.128.200.27"
forward_port: 5000 ← CRITICAL! forward_port: 5000 ← CRITICAL!
ssl_forced: true ssl_forced: true
certificate_id: 27 certificate_id: 27
@ -209,8 +209,8 @@ Firewall: ALLOW from any to 85.237.177.83:443 (state: NEW,ESTABLISHED)
5. **⚠️ CRITICAL ROUTING DECISION:** 5. **⚠️ CRITICAL ROUTING DECISION:**
``` ```
✓ CORRECT: Forward to http://10.22.68.249:5000 ✓ CORRECT: Forward to http://57.128.200.27:5000
❌ WRONG: Forward to http://10.22.68.249:80 (causes redirect loop!) ❌ WRONG: Forward to http://57.128.200.27:80 (causes redirect loop!)
``` ```
6. **Forward to Backend (HTTP, unencrypted):** 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 | | Domain Names | nordabiznes.pl, www.nordabiznes.pl | Primary + www alias |
| Forward Scheme | http | NPM→Backend uses HTTP (secure internal network) | | 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!)** | | **Forward Port** | **5000** | **Flask/Gunicorn port (CRITICAL!)** |
| SSL Certificate | 27 (Let's Encrypt) | Auto-renewal enabled | | SSL Certificate | 27 (Let's Encrypt) | Auto-renewal enabled |
| SSL Forced | Yes | Redirect HTTP→HTTPS | | 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;\"" \"SELECT id, domain_names, forward_host, forward_port FROM proxy_host WHERE id = 27;\""
# Expected output: # 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) ### 2.3 Layer 3: Flask/Gunicorn Application (Request Processing)
**Server:** NORDABIZ-01 (VM 249) **Server:** NORDABIZ-01 (VM 249)
**IP:** 10.22.68.249 **IP:** 57.128.200.27
**Port:** 5000 **Port:** 5000
**Technology:** Gunicorn 20.1.0 + Flask 3.0 **Technology:** Gunicorn 20.1.0 + Flask 3.0
@ -398,11 +398,11 @@ ExecStart=/var/www/nordabiznes/venv/bin/gunicorn \
**Verification Command:** **Verification Command:**
```bash ```bash
# Test Flask directly (from server) # 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 # Expected: HTTP/1.1 200 OK
# Check Gunicorn workers # 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 # Expected: 1 master + 4 worker processes
``` ```
@ -467,10 +467,10 @@ engine = create_engine(DATABASE_URL,
**Verification:** **Verification:**
```bash ```bash
# Check PostgreSQL status # 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) # 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 # 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 ```mermaid
sequenceDiagram 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 NPM as 🔒 NPM Reverse Proxy<br/>10.22.68.250:443
participant Fortigate as 🛡️ Fortigate Firewall<br/>85.237.177.83 participant Fortigate as 🛡️ Fortigate Firewall<br/>85.237.177.83
participant Browser participant Browser
@ -573,14 +573,14 @@ x-request-id: abc123def456
│ Certificate: Let's Encrypt (nordabiznes.pl) │ │ Certificate: Let's Encrypt (nordabiznes.pl) │
│ │ │ │
│ ⚠️ CRITICAL ROUTING DECISION: │ │ ⚠️ CRITICAL ROUTING DECISION: │
│ ✓ Forward to: http://10.22.68.249:5000 (CORRECT) │ │ ✓ Forward to: http://57.128.200.27:5000 (CORRECT) │
│ ❌ DO NOT use: http://10.22.68.249:80 (WRONG!) │ │ ❌ DO NOT use: http://57.128.200.27:80 (WRONG!) │
└────────────────────────────┬────────────────────────────────────┘ └────────────────────────────┬────────────────────────────────────┘
│ HTTP (Port 5000) ✓ │ HTTP (Port 5000) ✓
┌─────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────┐
│ FLASK/GUNICORN (NORDABIZ-01) │ │ FLASK/GUNICORN (NORDABIZ-01) │
│ IP: 10.22.68.249:5000 │ │ IP: 57.128.200.27:5000 │
│ Binding: 0.0.0.0:5000 │ │ Binding: 0.0.0.0:5000 │
│ Workers: 4 (Gunicorn) │ │ Workers: 4 (Gunicorn) │
│ Function: Application logic, template rendering │ │ 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! # If shows: 80 ← PROBLEM!
# 2. Test direct backend access # 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 # 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 # Should return: HTTP 200 OK
``` ```
@ -893,25 +893,25 @@ curl -I https://nordabiznes.pl/health
**Diagnosis:** **Diagnosis:**
```bash ```bash
# 1. Check if Gunicorn is running # 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) # Expected: Active (running)
# 2. Check if port 5000 is listening # 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 # Expected: 0.0.0.0:5000 ... gunicorn
# 3. Test direct connection # 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 # Expected: HTTP 200 OK
``` ```
**Solution:** **Solution:**
```bash ```bash
# Restart Gunicorn # Restart Gunicorn
ssh maciejpi@10.22.68.249 "sudo systemctl restart nordabiznes" ssh maciejpi@57.128.200.27 "sudo systemctl restart nordabiznes"
# Check logs # 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:** **Diagnosis:**
```bash ```bash
# 1. Check Gunicorn worker status # 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) # Look for workers in state 'R' (running) vs 'S' (sleeping)
# 2. Check application logs # 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 # 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';\"" \"SELECT count(*) FROM pg_stat_activity WHERE datname='nordabiz';\""
``` ```
@ -985,7 +985,7 @@ curl -I https://nordabiznes.pl/health
# Expected: HTTP/2 200 OK # Expected: HTTP/2 200 OK
# Internal access test (from INPI network) # 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 # 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:** **Application Status:**
```bash ```bash
# Service status # Service status
ssh maciejpi@10.22.68.249 "sudo systemctl status nordabiznes" ssh maciejpi@57.128.200.27 "sudo systemctl status nordabiznes"
# Worker processes # 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 # 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:** **Database Status:**
```bash ```bash
# PostgreSQL status # PostgreSQL status
ssh maciejpi@10.22.68.249 "sudo systemctl status postgresql" ssh maciejpi@57.128.200.27 "sudo systemctl status postgresql"
# Connection count # 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';\"" \"SELECT count(*) FROM pg_stat_activity WHERE datname='nordabiz';\""
# Database size # 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'));\"" \"SELECT pg_size_pretty(pg_database_size('nordabiz'));\""
``` ```
**Network Connectivity:** **Network Connectivity:**
```bash ```bash
# Test NPM → Flask connectivity # 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 # 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;'" -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, "id": 27,
"domain_names": ["nordabiznes.pl", "www.nordabiznes.pl"], "domain_names": ["nordabiznes.pl", "www.nordabiznes.pl"],
"forward_scheme": "http", "forward_scheme": "http",
"forward_host": "10.22.68.249", "forward_host": "57.128.200.27",
"forward_port": 5000, "forward_port": 5000,
"access_list_id": 0, "access_list_id": 0,
"certificate_id": 27, "certificate_id": 27,
@ -1075,7 +1075,7 @@ NPM_URL = "http://10.22.68.250:81/api"
data = { data = {
"domain_names": ["nordabiznes.pl", "www.nordabiznes.pl"], "domain_names": ["nordabiznes.pl", "www.nordabiznes.pl"],
"forward_scheme": "http", "forward_scheme": "http",
"forward_host": "10.22.68.249", "forward_host": "57.128.200.27",
"forward_port": 5000, # CRITICAL! "forward_port": 5000, # CRITICAL!
"certificate_id": 27, "certificate_id": 27,
"ssl_forced": True, "ssl_forced": True,
@ -1186,19 +1186,19 @@ ssh maciejpi@10.22.68.250 "docker logs -f nginx-proxy-manager_app_1"
**Gunicorn Logs:** **Gunicorn Logs:**
```bash ```bash
# Application logs (systemd journal) # 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) # 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 # 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:** **PostgreSQL Logs:**
```bash ```bash
# Query logs (if enabled) # 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 ### 10.2 Key Metrics to Monitor

View File

@ -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 - **Scenariusze:** HTTP brute-force, path traversal, scanner detection, bad user agents
- **Instalacja:** Docker sidecar obok NPM - **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 - **Parser:** Flask/Gunicorn access logs
- **Scenariusze:** Login brute-force, honeypot triggers, rate limit abuse - **Scenariusze:** Login brute-force, honeypot triggers, rate limit abuse
- **Instalacja:** Pakiet systemowy - **Instalacja:** Pakiet systemowy

View File

@ -14,7 +14,7 @@
## Task 1: Install CrowdSec on NordaBiz (VM 249) ## 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. 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** **Step 1: Install CrowdSec repository and package**
```bash ```bash
ssh maciejpi@10.22.68.249 "curl -s https://install.crowdsec.net | sudo sh" ssh maciejpi@57.128.200.27 "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 "sudo apt install -y crowdsec"
``` ```
**Step 2: Verify CrowdSec is running** **Step 2: Verify CrowdSec is running**
```bash ```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. 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. NordaBiz runs behind nginx on port 80, Flask/Gunicorn on port 5000.
```bash ```bash
ssh maciejpi@10.22.68.249 "sudo cscli collections install crowdsecurity/nginx" ssh maciejpi@57.128.200.27 "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/base-http-scenarios"
``` ```
**Step 4: Configure acquisition for nginx access logs** **Step 4: Configure acquisition for nginx access logs**
```bash ```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: filenames:
- /var/log/nginx/access.log - /var/log/nginx/access.log
labels: labels:
@ -60,7 +60,7 @@ EOF"
**Step 5: Configure acquisition for NordaBiz security log (honeypot)** **Step 5: Configure acquisition for NordaBiz security log (honeypot)**
```bash ```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: filenames:
- /var/log/nordabiznes/security.log - /var/log/nordabiznes/security.log
labels: labels:
@ -71,8 +71,8 @@ EOF"
**Step 6: Restart CrowdSec and verify** **Step 6: Restart CrowdSec and verify**
```bash ```bash
ssh maciejpi@10.22.68.249 "sudo systemctl restart crowdsec" ssh maciejpi@57.128.200.27 "sudo systemctl restart crowdsec"
ssh maciejpi@10.22.68.249 "sudo cscli metrics" ssh maciejpi@57.128.200.27 "sudo cscli metrics"
``` ```
Expected: Metrics show nginx parser processing lines from access.log. 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** **Step 7: Verify installed collections and scenarios**
```bash ```bash
ssh maciejpi@10.22.68.249 "sudo cscli collections list" ssh maciejpi@57.128.200.27 "sudo cscli collections list"
ssh maciejpi@10.22.68.249 "sudo cscli scenarios list" ssh maciejpi@57.128.200.27 "sudo cscli scenarios list"
``` ```
Expected: crowdsecurity/nginx, crowdsecurity/base-http-scenarios, crowdsecurity/linux listed. 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** **Step 2: Enroll NordaBiz instance**
```bash ```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** **Step 3: Enroll NPM instance**
@ -192,7 +192,7 @@ In Console → Blocklists tab → subscribe both engines to relevant blocklists.
**Step 6: Verify blocklist decisions** **Step 6: Verify blocklist decisions**
```bash ```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" 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 # From management network
curl -sI https://nordabiznes.pl/health | head -3 curl -sI https://nordabiznes.pl/health | head -3
ping -c 3 10.22.68.250 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. Expected: All services reachable through bridge.
@ -677,7 +677,7 @@ done
# OPNsense WebGUI → Services → Intrusion Detection → Alerts # OPNsense WebGUI → Services → Intrusion Detection → Alerts
# 2. Check CrowdSec decisions on all three servers # 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.250 "sudo cscli decisions list"
ssh maciejpi@10.22.68.155 "sudo cscli decisions list" ssh maciejpi@10.22.68.155 "sudo cscli decisions list"

View File

@ -1191,25 +1191,25 @@ Test messaging features manually on staging.
- [ ] **Step 1: Deploy** - [ ] **Step 1: Deploy**
```bash ```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** - [ ] **Step 2: Run migration**
```bash ```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** - [ ] **Step 3: Create upload directory**
```bash ```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** - [ ] **Step 4: Restart and verify**
```bash ```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 curl -sI https://nordabiznes.pl/health | head -3
``` ```

View File

@ -1139,6 +1139,6 @@ Then manually test `/pej`, `/pej/local-content`, `/pej/aktualnosci` on staging.
- [ ] **Step 4: Deploy to production (AFTER staging verification)** - [ ] **Step 4: Deploy to production (AFTER staging verification)**
```bash ```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 curl -sI https://nordabiznes.pl/health | head -3
``` ```

File diff suppressed because it is too large Load Diff

View File

@ -257,7 +257,7 @@ Type "Co wiesz o mnie?" — verify AI lists your profile data.
- [ ] **Step 4: Deploy to production** - [ ] **Step 4: Deploy to production**
```bash ```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 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** - [ ] **Step 3: Deploy to production**
```bash ```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 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: SSE requires Nginx to NOT buffer the response. The streaming endpoint sets `X-Accel-Buffering: no` header. Verify NPM custom config allows this:
```bash ```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: 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** - [ ] **Step 3: Deploy to production**
```bash ```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 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** - [ ] **Step 4: Deploy to production**
```bash ```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"
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@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@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@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@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 curl -sI https://nordabiznes.pl/health | head -3
``` ```

View File

@ -10,7 +10,7 @@
**Spec:** `docs/superpowers/specs/2026-03-31-event-guests-design.md` **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** - [ ] **Step 1: Deploy to production**
```bash ```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** - [ ] **Step 2: Run migration on production**
```bash ```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** - [ ] **Step 3: Reload service**
```bash ```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** - [ ] **Step 4: Verify production**

View File

@ -17,7 +17,7 @@ Podejście B: UptimeRobot (zewnętrzny monitoring) + wewnętrzny health logger z
## Architektura ## 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 │ sprawdza co 5 min │ wewnętrzny logger co 5 min
│ HTTPS → nordabiznes.pl │ app/db/cpu/ram/disk → PostgreSQL │ HTTPS → nordabiznes.pl │ app/db/cpu/ram/disk → PostgreSQL
│ │ │ │

View 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

View File

@ -1,12 +1,11 @@
# Zabbix Monitoring Setup - NORDABIZ-01 # Zabbix Monitoring Setup - OVH VPS (inpi-vps-waw01)
## Informacje o serwerze ## Informacje o serwerze
| Parametr | Wartosc | | Parametr | Wartosc |
|----------|---------| |----------|---------|
| **Nazwa hosta** | NORDABIZ-01 | | **Nazwa hosta** | inpi-vps-waw01 |
| **VM ID** | 249 | | **IP** | 57.128.200.27 |
| **IP** | 10.22.68.249 |
| **OS** | Ubuntu 22.04 LTS | | **OS** | Ubuntu 22.04 LTS |
| **Aplikacja** | Flask (NordaBiznes Partner) | | **Aplikacja** | Flask (NordaBiznes Partner) |
| **Baza danych** | PostgreSQL 15 (localhost:5432) | | **Baza danych** | PostgreSQL 15 (localhost:5432) |
@ -29,7 +28,7 @@
Polacz sie z serwerem i sprawdz czy agent jest zainstalowany: Polacz sie z serwerem i sprawdz czy agent jest zainstalowany:
```bash ```bash
ssh maciejpi@10.22.68.249 ssh maciejpi@57.128.200.27
# Sprawdz status agenta (Zabbix Agent 2 - nowsza wersja) # Sprawdz status agenta (Zabbix Agent 2 - nowsza wersja)
systemctl status zabbix-agent2 systemctl status zabbix-agent2
@ -100,7 +99,7 @@ Server=10.22.68.126
ServerActive=10.22.68.126 ServerActive=10.22.68.126
# Nazwa hosta - MUSI byc identyczna jak w Zabbix Server # Nazwa hosta - MUSI byc identyczna jak w Zabbix Server
Hostname=NORDABIZ-01 Hostname=inpi-vps-waw01
# Port nasluchiwania (domyslny) # Port nasluchiwania (domyslny)
ListenPort=10050 ListenPort=10050
@ -165,7 +164,7 @@ sudo nano /etc/zabbix/zabbix_agent2.d/nordabiznes.conf
```ini ```ini
# ============================================ # ============================================
# NordaBiznes Partner - Custom Zabbix Monitoring # NordaBiznes Partner - Custom Zabbix Monitoring
# Server: NORDABIZ-01 (10.22.68.249) # Server: OVH VPS inpi-vps-waw01 (57.128.200.27)
# ============================================ # ============================================
# --- FLASK APPLICATION --- # --- FLASK APPLICATION ---
@ -283,11 +282,11 @@ zabbix_agent2 -t nordabiznes.disk_usage_mb
| Pole | Wartosc | | Pole | Wartosc |
|------|---------| |------|---------|
| Host name | NORDABIZ-01 | | Host name | inpi-vps-waw01 |
| Visible name | NordaBiznes Partner (10.22.68.249) | | Visible name | NordaBiznes Partner (57.128.200.27) |
| Templates | Linux by Zabbix agent, PostgreSQL by Zabbix agent 2 | | Templates | Linux by Zabbix agent, PostgreSQL by Zabbix agent 2 |
| Host groups | Linux servers, Web servers, Databases | | 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 ### 5.2 Utworzenie dedykowanego template'u
@ -319,14 +318,14 @@ Utworz nowy template dla NordaBiznes:
| Name | Expression | Severity | | Name | Expression | Severity |
|------|------------|----------| |------|------------|----------|
| NordaBiznes app is down | last(/NORDABIZ-01/nordabiznes.health)=0 | High | | NordaBiznes app is down | last(/inpi-vps-waw01/nordabiznes.health)=0 | High |
| NordaBiznes service stopped | last(/NORDABIZ-01/nordabiznes.service_status)=0 | High | | NordaBiznes service stopped | last(/inpi-vps-waw01/nordabiznes.service_status)=0 | High |
| PostgreSQL is down | last(/NORDABIZ-01/postgresql.status)=0 | Disaster | | PostgreSQL is down | last(/inpi-vps-waw01/postgresql.status)=0 | Disaster |
| High response time (>2s) | last(/NORDABIZ-01/nordabiznes.response_time)>2000 | Warning | | High response time (>2s) | last(/inpi-vps-waw01/nordabiznes.response_time)>2000 | Warning |
| Low Gunicorn workers | last(/NORDABIZ-01/nordabiznes.workers)<2 | Warning | | Low Gunicorn workers | last(/inpi-vps-waw01/nordabiznes.workers)<2 | Warning |
| High DB connections (>80) | last(/NORDABIZ-01/postgresql.connections)>80 | Warning | | High DB connections (>80) | last(/inpi-vps-waw01/postgresql.connections)>80 | Warning |
| High memory usage (>500MB) | last(/NORDABIZ-01/nordabiznes.memory_mb)>500 | Warning | | High memory usage (>500MB) | last(/inpi-vps-waw01/nordabiznes.memory_mb)>500 | Warning |
| Disk usage >5GB | last(/NORDABIZ-01/nordabiznes.disk_usage_mb)>5120 | Warning | | Disk usage >5GB | last(/inpi-vps-waw01/nordabiznes.disk_usage_mb)>5120 | Warning |
--- ---
@ -336,20 +335,20 @@ Utworz nowy template dla NordaBiznes:
```bash ```bash
# Na serwerze Zabbix (10.22.68.126) # 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 # 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 # 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 # Oczekiwany wynik: 1
``` ```
### 6.2 Test lokalnie na NORDABIZ-01 ### 6.2 Test lokalnie na inpi-vps-waw01
```bash ```bash
# Na serwerze NORDABIZ-01 # Na serwerze inpi-vps-waw01
zabbix_agent2 -t agent.ping zabbix_agent2 -t agent.ping
zabbix_agent2 -t nordabiznes.health zabbix_agent2 -t nordabiznes.health
zabbix_agent2 -t nordabiznes.service_status 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) 4. **Graph: Memory Usage** - nordabiznes.memory_mb (trend)
5. **Graph: DB Connections** - postgresql.connections (trend) 5. **Graph: DB Connections** - postgresql.connections (trend)
6. **Counter: Chat Messages Today** - nordabiznes.chat_messages_today 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
--- ---

View 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 &middot; 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>

View File

@ -186,7 +186,7 @@ blog, contact_form, detailed_services, analytics, live_chat
--- ---
**Report generated by Norda Biznes Digital Maturity Platform** **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* *Phase: ETAP 1 - Foundation*
--- ---

View File

@ -0,0 +1,6 @@
{
"vps-2025-model1": "available",
"vps-2025-model2": "available",
"vps-2025-model3": "available",
"vps-2025-model4": "available"
}

View 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}')

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,7 @@ from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from database import Company, Category 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') DATABASE_URL = os.environ.get('DATABASE_URL', 'postgresql://nordabiz_app:dev_password@localhost:5433/nordabiz')

340
scripts/ovh_vps_monitor.py Normal file
View 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
View 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 ==="

View File

@ -543,7 +543,7 @@
{% endif %} {% endif %}
</td> </td>
<td class="log-time"> <td class="log-time">
{{ log.created_at.strftime('%d.%m.%Y %H:%M') }} {{ log.created_at|local_time('%d.%m.%Y %H:%M') }}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -505,7 +505,7 @@
{% endif %} {% endif %}
</td> </td>
<td class="log-time"> <td class="log-time">
{{ log.created_at.strftime('%d.%m.%Y %H:%M') }} {{ log.created_at|local_time('%d.%m.%Y %H:%M') }}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -268,7 +268,7 @@
{% endif %} {% endif %}
</td> </td>
<td>{{ ann.author.name if ann.author else '-' }}</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="views-count">{{ ann.views_count or 0 }}</td>
<td class="actions-cell"> <td class="actions-cell">
<a href="{{ url_for('admin.admin_announcements_edit', id=ann.id) }}" class="btn btn-secondary btn-small"> <a href="{{ url_for('admin.admin_announcements_edit', id=ann.id) }}" class="btn btn-secondary btn-small">

View File

@ -160,7 +160,7 @@
<div class="status-info"> <div class="status-info">
<strong>Status:</strong> {{ announcement.status_label }} <strong>Status:</strong> {{ announcement.status_label }}
{% if announcement.published_at %} {% 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 %} {% endif %}
{% if announcement.views_count %} {% if announcement.views_count %}
| <strong>Wyswietlenia:</strong> {{ announcement.views_count }} | <strong>Wyswietlenia:</strong> {{ announcement.views_count }}
@ -242,7 +242,7 @@
<div class="form-group"> <div class="form-group">
<label for="expires_at">Data wygasniecia</label> <label for="expires_at">Data wygasniecia</label>
<input type="datetime-local" id="expires_at" name="expires_at" <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> <p class="form-hint">Pozostaw puste aby nie wygasalo</p>
</div> </div>
</div> </div>

View File

@ -95,7 +95,7 @@
<tbody> <tbody>
{% for click in clicks %} {% for click in clicks %}
<tr> <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> <td>
{% if click.user %} {% if click.user %}
{{ click.user.email }} {{ click.user.email }}

View File

@ -680,7 +680,7 @@
<div class="stat-label">Użytkownicy</div> <div class="stat-label">Użytkownicy</div>
</div> </div>
<div class="stat-card"> <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 class="stat-label">Utworzono</div>
</div> </div>
</div> </div>
@ -798,7 +798,7 @@
<div class="action-status"> <div class="action-status">
{% if enrichment.registry.done %} {% if enrichment.registry.done %}
<span class="status-dot green"></span> <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 %} {% else %}
<span class="status-dot gray"></span> <span class="status-dot gray"></span>
Nie wykonano Nie wykonano
@ -828,7 +828,7 @@
<div class="action-status"> <div class="action-status">
{% if enrichment.seo.done %} {% if enrichment.seo.done %}
<span class="status-dot green"></span> <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 %} {% else %}
<span class="status-dot gray"></span> <span class="status-dot gray"></span>
Nie wykonano Nie wykonano
@ -875,7 +875,7 @@
<div class="action-status"> <div class="action-status">
{% if enrichment.gbp.done %} {% if enrichment.gbp.done %}
<span class="status-dot green"></span> <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 %} {% else %}
<span class="status-dot gray"></span> <span class="status-dot gray"></span>
Nie wykonano Nie wykonano

View File

@ -270,7 +270,7 @@
</div> </div>
<div class="info-item"> <div class="info-item">
<span class="info-label">Data:</span> <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>
<div class="info-item"> <div class="info-item">
<span class="info-label">Źródło:</span> <span class="info-label">Źródło:</span>

View File

@ -580,7 +580,7 @@
<p>Przegląd kompletności i jakości danych {{ total }} firm w katalogu</p> <p>Przegląd kompletności i jakości danych {{ total }} firm w katalogu</p>
</div> </div>
<div class="dq-timestamp"> <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>
</div> </div>

View File

@ -450,9 +450,9 @@
<td style="text-align:center;"> <td style="text-align:center;">
{% if cf.reminder %} {% if cf.reminder %}
{% if cf.reminder.is_read %} {% 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 %} {% 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 %}
{% endif %} {% endif %}
</td> </td>

View File

@ -587,7 +587,7 @@
{{ status_labels.get(topic.status, 'Nowy') }} {{ status_labels.get(topic.status, 'Nowy') }}
</span> </span>
</td> </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> <td>
<div class="action-buttons"> <div class="action-buttons">
<button class="btn-icon {% if topic.is_pinned %}active{% endif %}" <button class="btn-icon {% if topic.is_pinned %}active{% endif %}"
@ -652,7 +652,7 @@
<div class="reply-meta"> <div class="reply-meta">
{{ reply.author.name or reply.author.email.split('@')[0] }} {{ 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> 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>
&bull; {{ reply.created_at.strftime('%d.%m.%Y %H:%M') }} &bull; {{ reply.created_at|local_time('%d.%m.%Y %H:%M') }}
</div> </div>
</div> </div>
<button class="btn-icon danger" <button class="btn-icon danger"

View File

@ -386,7 +386,7 @@
</span> </span>
</td> </td>
<td>{{ topic.author.name or topic.author.email.split('@')[0] }}</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> <td>{{ topic.days_waiting }} dni</td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -123,7 +123,7 @@
<strong>{{ topic.title }}</strong> <strong>{{ topic.title }}</strong>
<div class="deleted-meta"> <div class="deleted-meta">
Autor: {{ topic.author.name or topic.author.email.split('@')[0] }} Autor: {{ topic.author.name or topic.author.email.split('@')[0] }}
&bull; Utworzono: {{ topic.created_at.strftime('%d.%m.%Y %H:%M') }} &bull; Utworzono: {{ topic.created_at|local_time('%d.%m.%Y %H:%M') }}
</div> </div>
</div> </div>
<button class="btn-restore" onclick="restoreTopic({{ topic.id }})"> <button class="btn-restore" onclick="restoreTopic({{ topic.id }})">
@ -134,7 +134,7 @@
{{ topic.content[:300] }}{% if topic.content|length > 300 %}...{% endif %} {{ topic.content[:300] }}{% if topic.content|length > 300 %}...{% endif %}
</div> </div>
<div class="deleted-info"> <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 %} {% if topic.deleter %}przez {{ topic.deleter.name or topic.deleter.email.split('@')[0] }}{% endif %}
</div> </div>
</div> </div>
@ -157,7 +157,7 @@
<div class="deleted-meta"> <div class="deleted-meta">
W temacie: <a href="{{ url_for('forum_topic', topic_id=reply.topic_id) }}" target="_blank">{{ reply.topic.title }}</a> 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] }} <br>Autor: {{ reply.author.name or reply.author.email.split('@')[0] }}
&bull; Utworzono: {{ reply.created_at.strftime('%d.%m.%Y %H:%M') }} &bull; Utworzono: {{ reply.created_at|local_time('%d.%m.%Y %H:%M') }}
</div> </div>
</div> </div>
<button class="btn-restore" onclick="restoreReply({{ reply.id }})"> <button class="btn-restore" onclick="restoreReply({{ reply.id }})">
@ -168,7 +168,7 @@
{{ reply.content[:300] }}{% if reply.content|length > 300 %}...{% endif %} {{ reply.content[:300] }}{% if reply.content|length > 300 %}...{% endif %}
</div> </div>
<div class="deleted-info"> <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 %} {% if reply.deleter %}przez {{ reply.deleter.name or reply.deleter.email.split('@')[0] }}{% endif %}
</div> </div>
</div> </div>

View File

@ -200,7 +200,7 @@
<span class="report-reason reason-{{ report.reason }}">{{ reason_labels.get(report.reason, report.reason) }}</span> <span class="report-reason reason-{{ report.reason }}">{{ reason_labels.get(report.reason, report.reason) }}</span>
<span class="report-meta"> <span class="report-meta">
Zgłoszone przez {{ report.reporter.name or report.reporter.email.split('@')[0] }} Zgłoszone przez {{ report.reporter.name or report.reporter.email.split('@')[0] }}
&bull; {{ report.created_at.strftime('%d.%m.%Y %H:%M') }} &bull; {{ report.created_at|local_time('%d.%m.%Y %H:%M') }}
</span> </span>
</div> </div>
<span class="report-meta"> <span class="report-meta">
@ -242,7 +242,7 @@
<div class="report-meta"> <div class="report-meta">
{% if report.reviewed_by %} {% if report.reviewed_by %}
Rozpatrzone przez {{ report.reviewer.name or report.reviewer.email.split('@')[0] }} Rozpatrzone przez {{ report.reviewer.name or report.reviewer.email.split('@')[0] }}
&bull; {{ report.reviewed_at.strftime('%d.%m.%Y %H:%M') }} &bull; {{ report.reviewed_at|local_time('%d.%m.%Y %H:%M') }}
{% endif %} {% endif %}
{% if report.review_note %} {% if report.review_note %}
<br>Notatka: {{ report.review_note }} <br>Notatka: {{ report.review_note }}

View File

@ -696,8 +696,8 @@
<td class="date-cell"> <td class="date-cell">
{% if company.audit_date %} {% if company.audit_date %}
{% set days_ago = (now - company.audit_date).days %} {% 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') }}"> <span class="{{ 'date-old' if days_ago > 30 else '' }}" title="{{ company.audit_date|local_time('%Y-%m-%d %H:%M') }}">
{{ company.audit_date.strftime('%d.%m.%Y') }} {{ company.audit_date|local_time('%d.%m.%Y') }}
</span> </span>
{% else %} {% else %}
<span class="date-never">Nigdy</span> <span class="date-never">Nigdy</span>

View File

@ -354,7 +354,7 @@
Health Check Dashboard Health Check Dashboard
</h1> </h1>
<div class="health-meta"> <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> <span>Auto-refresh: <strong id="countdown">2:00</strong></span>
</div> </div>
</div> </div>

View File

@ -662,7 +662,7 @@
data-zarzad="{{ company.people_count|default(0, true) }}" data-zarzad="{{ company.people_count|default(0, true) }}"
data-pkd="{{ company.pkd_count|default(0, true) }}" data-pkd="{{ company.pkd_count|default(0, true) }}"
data-status="{{ 'audited' if company.krs_last_audit_at else 'pending' }}" 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"> <td class="company-name-cell">
<a href="{{ url_for('company_detail', company_id=company.id) }}">{{ company.name }}</a> <a href="{{ url_for('company_detail', company_id=company.id) }}">{{ company.name }}</a>
</td> </td>
@ -720,8 +720,8 @@
</td> </td>
<td class="date-cell"> <td class="date-cell">
{% if company.krs_last_audit_at %} {% if company.krs_last_audit_at %}
<span title="{{ company.krs_last_audit_at.strftime('%Y-%m-%d %H:%M') }}"> <span title="{{ company.krs_last_audit_at|local_time('%Y-%m-%d %H:%M') }}">
{{ company.krs_last_audit_at.strftime('%d.%m.%Y') }} {{ company.krs_last_audit_at|local_time('%d.%m.%Y') }}
</span> </span>
{% else %} {% else %}
<span class="date-never">Nigdy</span> <span class="date-never">Nigdy</span>

View File

@ -258,8 +258,8 @@
</td> </td>
<td> <td>
{% if app.submitted_at %} {% if app.submitted_at %}
{{ app.submitted_at.strftime('%Y-%m-%d') }} {{ app.submitted_at|local_time('%Y-%m-%d') }}
<br><small class="text-muted">{{ app.submitted_at.strftime('%H:%M') }}</small> <br><small class="text-muted">{{ app.submitted_at|local_time('%H:%M') }}</small>
{% else %} {% else %}
<span class="text-muted">-</span> <span class="text-muted">-</span>
{% endif %} {% endif %}

View File

@ -729,7 +729,7 @@
<div class="print-header"> <div class="print-header">
<h2>Izba Gospodarcza Norda Biznes</h2> <h2>Izba Gospodarcza Norda Biznes</h2>
<p>Deklaracja członkowska nr {{ application.id }} — {{ application.company_name }}</p> <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> </div>
<a href="{{ url_for('admin.admin_membership') }}" class="back-link"> <a href="{{ url_for('admin.admin_membership') }}" class="back-link">
@ -944,7 +944,7 @@
<div class="data-value"> <div class="data-value">
{% if application.declaration_accepted %} {% if application.declaration_accepted %}
✅ Zaakceptowane ✅ 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> <br><small>IP: {{ application.declaration_ip_address or '-' }}</small>
{% else %} {% else %}
❌ Nie zaakceptowane ❌ Nie zaakceptowane
@ -1071,7 +1071,7 @@
<strong>Oczekuje na akceptację użytkownika</strong> <strong>Oczekuje na akceptację użytkownika</strong>
<br>Zaproponowano zmiany danych z rejestru. Użytkownik musi je zaakceptować lub odrzucić. <br>Zaproponowano zmiany danych z rejestru. Użytkownik musi je zaakceptować lub odrzucić.
{% if application.proposed_changes_at %} {% 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 %} {% endif %}
{% if application.proposed_changes_comment %} {% if application.proposed_changes_comment %}
<br><small>Komentarz: {{ application.proposed_changes_comment }}</small> <br><small>Komentarz: {{ application.proposed_changes_comment }}</small>
@ -1119,7 +1119,7 @@
</div> </div>
<div class="tracker-label">{{ step.label }}</div> <div class="tracker-label">{{ step.label }}</div>
{% if step.date %} {% 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 %} {% endif %}
</div> </div>
@ -1162,7 +1162,7 @@
<div class="history-dot dot-green"></div> <div class="history-dot dot-green"></div>
<div class="history-content"> <div class="history-content">
<div class="history-title">Deklaracja zatwierdzona</div> <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>
</div> </div>
{% endif %} {% endif %}
@ -1171,7 +1171,7 @@
<div class="history-dot dot-yellow"></div> <div class="history-dot dot-yellow"></div>
<div class="history-content"> <div class="history-content">
<div class="history-title">Rozpoczęto rozpatrywanie</div> <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>
</div> </div>
{% endif %} {% endif %}
@ -1180,7 +1180,7 @@
<div class="history-dot dot-blue"></div> <div class="history-dot dot-blue"></div>
<div class="history-content"> <div class="history-content">
<div class="history-title">Deklaracja złożona</div> <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>
</div> </div>
{% endif %} {% endif %}
@ -1226,11 +1226,11 @@
</div> </div>
<div class="data-item"> <div class="data-item">
<div class="data-label">Data zgłoszenia</div> <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>
<div class="data-item"> <div class="data-item">
<div class="data-label">Data wysłania</div> <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> </div>
</div> </div>

View File

@ -211,7 +211,7 @@
<p class="subtitle">Deklaracja Przystąpienia do Izby nr {{ app.id }}</p> <p class="subtitle">Deklaracja Przystąpienia do Izby nr {{ app.id }}</p>
<p class="company-name">{{ app.company_name }}</p> <p class="company-name">{{ app.company_name }}</p>
<p class="meta"> <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' }}
&nbsp;&bull;&nbsp; Status: {{ app.status_label }} &nbsp;&bull;&nbsp; Status: {{ app.status_label }}
</p> </p>
</div> </div>
@ -320,7 +320,7 @@
</div> </div>
<div class="date"> <div class="date">
{% if app.declaration_accepted_at %} {% 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 %} {% endif %}
</div> </div>
</div> </div>

View File

@ -284,7 +284,7 @@
<!-- Latest audit header --> <!-- Latest audit header -->
<h2 style="font-size: var(--font-size-lg); margin-bottom: var(--spacing-md); color: var(--text-secondary);"> <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;"> &mdash; {{ latest.notes }}</span>{% endif %} {% if latest.notes %}<span style="font-size: var(--font-size-sm); font-weight: 400;"> &mdash; {{ latest.notes }}</span>{% endif %}
</h2> </h2>
@ -452,7 +452,7 @@
{% set f_cit = f.get('citations', []) %} {% set f_cit = f.get('citations', []) %}
{% set f_cit_found = f_cit|selectattr('status', 'equalto', 'found')|list|length if f_cit else 0 %} {% set f_cit_found = f_cit|selectattr('status', 'equalto', 'found')|list|length if f_cit else 0 %}
<tr> <tr>
<td>{{ a.audited_at.strftime('%d.%m %H:%M') }}</td> <td>{{ a.audited_at|local_time('%d.%m %H:%M') }}</td>
{# PageSpeed scores #} {# 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 %}&mdash;{% endif %}</td>{% endmacro %} {% 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 %}&mdash;{% endif %}</td>{% endmacro %}

View File

@ -63,7 +63,7 @@
</a> </a>
<h1 style="margin-top: var(--spacing-sm);">Audyt SEO #{{ audit.id }}</h1> <h1 style="margin-top: var(--spacing-sm);">Audyt SEO #{{ audit.id }}</h1>
<p style="color: var(--text-secondary);"> <p style="color: var(--text-secondary);">
{{ audit.url }} &mdash; {{ audit.audited_at.strftime('%d.%m.%Y %H:%M') }} {{ audit.url }} &mdash; {{ audit.audited_at|local_time('%d.%m.%Y %H:%M') }}
{% if audit.notes %} &mdash; {{ audit.notes }}{% endif %} {% if audit.notes %} &mdash; {{ audit.notes }}{% endif %}
</p> </p>
</div> </div>

View File

@ -415,7 +415,7 @@
{{ rec.recommendation_text }} {{ rec.recommendation_text }}
</div> </div>
</td> </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> <td>
{% if rec.status == 'pending' %} {% if rec.status == 'pending' %}
<span class="badge badge-pending">Oczekuje</span> <span class="badge badge-pending">Oczekuje</span>

View File

@ -494,7 +494,7 @@
<div class="section-header"> <div class="section-header">
<h2>🌍 Statystyki GeoIP Blocking</h2> <h2>🌍 Statystyki GeoIP Blocking</h2>
<div style="text-align: right; font-size: var(--font-size-xs); color: var(--text-secondary);"> <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;"> <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> <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> 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><span class="ip-address">{{ alert.ip_address or '-' }}</span></td>
<td>{{ alert.user_email or '-' }}</td> <td>{{ alert.user_email or '-' }}</td>
<td><span class="badge badge-{{ alert.status }}">{{ alert.status }}</span></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> <td>
{% if alert.status == 'new' %} {% if alert.status == 'new' %}
<div class="action-buttons"> <div class="action-buttons">
@ -642,7 +642,7 @@
{% if log.entity_name %}<br><small>{{ log.entity_name }}</small>{% endif %} {% if log.entity_name %}<br><small>{{ log.entity_name }}</small>{% endif %}
</td> </td>
<td><span class="ip-address">{{ log.ip_address or '-' }}</span></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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -677,7 +677,7 @@
<tr class="locked-row"> <tr class="locked-row">
<td>{{ user.email }}</td> <td>{{ user.email }}</td>
<td>{{ user.failed_login_attempts }}</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> <td>
<form method="POST" action="{{ url_for('admin.unlock_account', user_id=user.id) }}" style="display:inline;"> <form method="POST" action="{{ url_for('admin.unlock_account', user_id=user.id) }}" style="display:inline;">
{{ csrf_token() }} {{ csrf_token() }}

View File

@ -249,7 +249,7 @@
{% if analysis and analysis.seo_audited_at %} {% if analysis and analysis.seo_audited_at %}
<div class="audit-info"> <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.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.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 %} {% 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 %} {% if analysis.ssl_expires_at %}
<div class="detail-item"> <div class="detail-item">
<span class="detail-label">Certyfikat SSL wygasa</span> <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> </div>
{% endif %} {% endif %}
{% if analysis.ssl_issuer %} {% if analysis.ssl_issuer %}
@ -1196,7 +1196,7 @@
{% if analysis.last_modified_at %} {% if analysis.last_modified_at %}
<div class="detail-item"> <div class="detail-item">
<span class="detail-label">Ostatnia modyfikacja strony</span> <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> </div>
{% endif %} {% endif %}
</div> </div>
@ -1214,7 +1214,7 @@
{% if analysis.analyzed_at %} {% if analysis.analyzed_at %}
<div class="detail-item"> <div class="detail-item">
<span class="detail-label">Data audytu</span> <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> </div>
{% endif %} {% endif %}
{% if analysis.audit_source %} {% if analysis.audit_source %}

View File

@ -907,8 +907,8 @@
<td class="date-cell hide-mobile"> <td class="date-cell hide-mobile">
{% if company.last_verified %} {% if company.last_verified %}
{% set days_ago = (now - company.last_verified).days %} {% 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') }}"> <span class="{{ 'date-old' if days_ago > 30 else '' }}" title="{{ company.last_verified|local_time('%Y-%m-%d %H:%M') }}">
{{ company.last_verified.strftime('%d.%m.%Y') }} {{ company.last_verified|local_time('%d.%m.%Y') }}
</span> </span>
{% else %} {% else %}
<span class="date-never">-</span> <span class="date-never">-</span>

View File

@ -955,13 +955,13 @@
{% if p.verified_at %} {% if p.verified_at %}
<div class="provenance-detail"> <div class="provenance-detail">
<span class="label">Zweryfikowano:</span> <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> </div>
{% endif %} {% endif %}
{% if p.last_checked_at %} {% if p.last_checked_at %}
<div class="provenance-detail"> <div class="provenance-detail">
<span class="label">Ostatni check:</span> <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> </div>
{% endif %} {% endif %}
<div class="provenance-detail"> <div class="provenance-detail">
@ -1017,7 +1017,7 @@
<a href="{{ url_for('admin.social_publisher_company_settings', company_id=company.id) }}">Wybierz stronę &rarr;</a> <a href="{{ url_for('admin.social_publisher_company_settings', company_id=company.id) }}">Wybierz stronę &rarr;</a>
{% endif %} {% endif %}
{% if p.oauth_last_sync %} {% 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 %} {% endif %}
</div> </div>
{% elif p.oauth_connected and p.oauth_expired %} {% elif p.oauth_connected and p.oauth_expired %}

View File

@ -453,7 +453,7 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if fb.last_checked_at %} {% 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 %} {% endif %}
<div style="margin-top: var(--spacing-sm); font-size: 11px; opacity: 0.5;"> <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 Publikowanie nie działa? Zmiana hasła FB lub usunięcie aplikacji wymaga ponownego połączenia w
@ -582,11 +582,11 @@
</td> </td>
<td style="white-space: nowrap; font-size: var(--font-size-sm); color: var(--text-secondary);"> <td style="white-space: nowrap; font-size: var(--font-size-sm); color: var(--text-secondary);">
{% if post.published_at %} {% 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 %} {% elif post.scheduled_at %}
{{ post.scheduled_at.strftime('%Y-%m-%d %H:%M') }} {{ post.scheduled_at|local_time('%Y-%m-%d %H:%M') }}
{% else %} {% 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 %} {% endif %}
</td> </td>
<td class="engagement-cell"> <td class="engagement-cell">
@ -638,7 +638,7 @@
<h3>Posty z Facebooka <h3>Posty z Facebooka
{% if cached_fb_posts.get(company_id_key) %} {% if cached_fb_posts.get(company_id_key) %}
<span style="font-size: var(--font-size-xs); color: var(--text-secondary); font-weight: 400;"> <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> </span>
{% endif %} {% endif %}
</h3> </h3>

View File

@ -220,7 +220,7 @@
| Status: {% if config.is_active %}<span class="status-active">Aktywna</span>{% else %}Nieaktywna{% endif %} | 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 %} | Debug: {% if config.debug_mode %}<span style="color: var(--warning); font-weight: 600;">Wlaczony</span>{% else %}Wylaczony{% endif %}
{% if config.updated_at %} {% 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 %} {% endif %}
</div> </div>
{% endif %} {% endif %}

View File

@ -492,7 +492,7 @@
| <strong>Model AI:</strong> {{ post.ai_model }} | <strong>Model AI:</strong> {{ post.ai_model }}
{% endif %} {% endif %}
{% if post.published_at %} {% 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 %} {% endif %}
{% if post.status == 'published' and post.meta_post_id %} {% if post.status == 'published' and post.meta_post_id %}
| |

View File

@ -370,8 +370,8 @@
</div> </div>
<div class="refresh-info"> <div class="refresh-info">
<div class="label">Ostatnia aktualizacja</div> <div class="label">Ostatnia aktualizacja</div>
<div class="timestamp" id="last-update">{{ generated_at.strftime('%H:%M:%S') }}</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.strftime('%d.%m.%Y') }}</div> <div class="label" style="margin-top: var(--spacing-xs);">{{ generated_at|local_time('%d.%m.%Y') }}</div>
<div class="next-refresh"> <div class="next-refresh">
<span class="refresh-indicator"></span> <span class="refresh-indicator"></span>
Auto-refresh: <span id="countdown">5:00</span> Auto-refresh: <span id="countdown">5:00</span>

View File

@ -380,7 +380,7 @@
</div> </div>
<div class="refresh-info"> <div class="refresh-info">
<div class="label">Ostatnia aktualizacja</div> <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>
</div> </div>

View File

@ -334,7 +334,7 @@
{% for s in recent_sessions %} {% 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 }}"> <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>{{ 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> <td>
{% set dt = s.device_type|lower %} {% set dt = s.device_type|lower %}
<span class="device-badge device-{{ dt if dt in ['desktop','mobile','tablet'] else 'other' }}"> <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.session_count }}</td>
<td class="num">{{ u.total_time_min }}</td> <td class="num">{{ u.total_time_min }}</td>
<td class="num">{{ u.total_pages }}</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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@ -911,7 +911,7 @@
{% if msg.feedback_rating == 2 %}Pomocne{% else %}Do poprawy{% endif %} {% if msg.feedback_rating == 2 %}Pomocne{% else %}Do poprawy{% endif %}
</span> </span>
<span style="font-size: var(--font-size-xs); color: var(--text-muted);"> <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> </span>
</div> </div>
<div style="font-size: var(--font-size-sm); color: var(--text-secondary); max-height: 50px; overflow: hidden;"> <div style="font-size: var(--font-size-sm); color: var(--text-secondary); max-height: 50px; overflow: hidden;">

View File

@ -156,11 +156,11 @@
</div> </div>
<div class="profile-meta-item"> <div class="profile-meta-item">
<span class="profile-meta-label">Rejestracja</span> <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>
<div class="profile-meta-item"> <div class="profile-meta-item">
<span class="profile-meta-label">Ostatni login</span> <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> </div>
</div> </div>
@ -221,7 +221,7 @@
{% if resolution.first_symptom %} {% if resolution.first_symptom %}
<div class="resolution-detail-item"> <div class="resolution-detail-item">
<div class="resolution-detail-label">Pierwszy objaw</div> <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> </div>
{% endif %} {% endif %}
<div class="resolution-detail-item"> <div class="resolution-detail-item">
@ -231,7 +231,7 @@
{% if resolution.last_reset %} {% if resolution.last_reset %}
<div class="resolution-detail-item"> <div class="resolution-detail-item">
<div class="resolution-detail-label">Ostatni reset</div> <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> </div>
{% endif %} {% endif %}
<div class="resolution-detail-item"> <div class="resolution-detail-item">
@ -275,7 +275,7 @@
<div class="timeline-body"> <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> <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 %} {% 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>
</div> </div>
{% endfor %} {% endfor %}
@ -361,7 +361,7 @@
<tr> <tr>
<td style="max-width: 300px; word-break: break-all; font-size: var(--font-size-xs);">{{ e.message[:100] }}</td> <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="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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -382,7 +382,7 @@
<tr> <tr>
<td style="font-family: monospace; font-size: var(--font-size-xs);">{{ p.path }}</td> <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="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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -409,7 +409,7 @@
<div style="display: flex; flex-wrap: wrap; gap: var(--spacing-sm);"> <div style="display: flex; flex-wrap: wrap; gap: var(--spacing-sm);">
{% for s in search_queries %} {% for s in search_queries %}
<span style="padding: 4px 12px; background: var(--background); border-radius: var(--radius); font-size: var(--font-size-sm);"> <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> </span>
{% endfor %} {% endfor %}
</div> </div>

View File

@ -1243,8 +1243,8 @@
</select> </select>
</td> </td>
<td style="font-size: var(--font-size-sm); color: var(--text-secondary);" <td style="font-size: var(--font-size-sm); color: var(--text-secondary);"
data-sort-value="{{ user.created_at.strftime('%Y%m%d%H%M') }}"> data-sort-value="{{ user.created_at|local_time('%Y%m%d%H%M') }}">
{{ user.created_at.strftime('%d.%m.%Y %H:%M') }} {{ user.created_at|local_time }}
{% if user.created_by_id and creators_map.get(user.created_by_id) %} {% 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> <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 %} {% else %}
@ -1252,9 +1252,9 @@
{% endif %} {% endif %}
</td> </td>
<td style="font-size: var(--font-size-sm); color: var(--text-secondary);" <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 %} {% if user.last_login %}
{{ user.last_login.strftime('%d.%m.%Y %H:%M') }} {{ user.last_login|local_time }}
{% else %} {% else %}
<span style="color: var(--text-muted, #9CA3AF);">Nigdy</span> <span style="color: var(--text-muted, #9CA3AF);">Nigdy</span>
{% endif %} {% endif %}

View File

@ -1639,7 +1639,7 @@
{% endif %} {% endif %}
<span>{{ news.source_name or news.source_domain or '-' }}</span> <span>{{ news.source_name or news.source_domain or '-' }}</span>
{% set news_year = news.published_at.year if news.published_at else None %} {% 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 %} {% if news_year and news_year < min_year %}
<span class="old-news-badge">⚠️ Sprzed {{ min_year }}</span> <span class="old-news-badge">⚠️ Sprzed {{ min_year }}</span>
{% endif %} {% endif %}
@ -1917,7 +1917,7 @@
<div class="fetch-job"> <div class="fetch-job">
<div> <div>
<strong>{{ job.search_query }}</strong> <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>
<div> <div>
Znaleziono: {{ job.results_found or 0 }} | Nowych: {{ job.results_new or 0 }} Znaleziono: {{ job.results_found or 0 }} | Nowych: {{ job.results_new or 0 }}

View File

@ -450,7 +450,7 @@
{% if news.status == 'pending' %}Oczekuje{% elif news.status == 'approved' %}Zatwierdzony{% else %}Odrzucony{% endif %} {% if news.status == 'pending' %}Oczekuje{% elif news.status == 'approved' %}Zatwierdzony{% else %}Odrzucony{% endif %}
</span> </span>
</td> </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;"> <td style="white-space: nowrap;">
{% if news.status == 'pending' %} {% if news.status == 'pending' %}
<button class="action-btn approve" onclick="approveNews({{ news.id }})">Zatwierdź</button> <button class="action-btn approve" onclick="approveNews({{ news.id }})">Zatwierdź</button>

View File

@ -857,8 +857,8 @@
<td class="date-cell"> <td class="date-cell">
{% if company.seo_audited_at %} {% if company.seo_audited_at %}
{% set days_ago = (now - company.seo_audited_at).days %} {% 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') }}"> <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.strftime('%d.%m.%Y') }} {{ company.seo_audited_at|local_time('%d.%m.%Y') }}
</span> </span>
{% else %} {% else %}
<span class="date-never">Nigdy</span> <span class="date-never">Nigdy</span>

View File

@ -386,7 +386,7 @@
</span> </span>
{% endfor %} {% endfor %}
<span class="meta-date"> <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> </span>
{% if announcement.author %} {% if announcement.author %}
<span class="meta-author"> <span class="meta-author">
@ -491,7 +491,7 @@
{{ other.title }} {{ other.title }}
</a> </a>
<div class="date"> <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>
</div> </div>
{% endfor %} {% endfor %}

View File

@ -330,7 +330,7 @@
<div class="card-footer"> <div class="card-footer">
<span class="card-date"> <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> </span>
<a href="{{ url_for('announcement_detail', slug=ann.slug) }}" class="card-link"> <a href="{{ url_for('announcement_detail', slug=ann.slug) }}" class="card-link">
Czytaj wiecej &rarr; Czytaj wiecej &rarr;

View File

@ -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); 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)">{{ doc.size_display }}</td>
<td style="font-size: var(--font-size-sm); color: var(--text-muted)"> <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 %} {% if doc.uploader %}<br>{{ doc.uploader.name }}{% endif %}
</td> </td>
<td> <td>

View File

@ -307,7 +307,7 @@
</div> </div>
<div class="classified-stats"> <div class="classified-stats">
<span>{{ classified.views_count }} wyswietl.</span> <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> </div>
</div> </div>

View File

@ -746,9 +746,9 @@
<div class="stats-bar"> <div class="stats-bar">
<span>{{ classified.views_count }} wyswietlen</span> <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 %} {% 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 %} {% endif %}
</div> </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 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 %} {% if not q.answer %}<span class="pending-badge">Oczekuje na odpowiedz</span>{% endif %}
</div> </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> </div>
{% if classified.author_id == current_user.id %} {% if classified.author_id == current_user.id %}
<div class="question-actions"> <div class="question-actions">

Some files were not shown because too many files have changed in this diff Show More