feat(admin): Add permanent delete for archived companies + update release notes
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

Workflow: Active → Archive → Permanent Delete (hard delete from DB).
Only ADMIN role can permanently delete, and only archived companies.
FK cleanup across 35+ tables before deletion.

Also adds 4 missing items to v1.25.0 release notes:
- Strefa RADA simplified (removed documents section)
- Korzyści commission column visibility
- Company hard-delete feature
- User delete FK cascade fix

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-05 08:00:30 +01:00
parent 932ee9c2b0
commit 85e14bb4bf
4 changed files with 194 additions and 474 deletions

527
CLAUDE.md
View File

@ -27,6 +27,7 @@ nordabiz/
├── DEVELOPMENT.md # Szczegóły deweloperskie
├── ROADMAP.md # Plan rozwoju, monetyzacja
├── CREDENTIALS.md # Zasady zarządzania credentials
├── CLAUDE-REFERENCE.md # Szczegóły operacyjne (DR, ZOPK, Remotion, cron)
└── INCIDENT_REPORT_*.md
```
@ -38,6 +39,7 @@ nordabiz/
| `docs/DEVELOPMENT.md` | SearchService, Chatbot, Testy AI, SEO, News |
| `docs/ROADMAP.md` | Plan rozwoju, priorytety, monetyzacja |
| `docs/CREDENTIALS.md` | Zarządzanie hasłami i kluczami API |
| `docs/CLAUDE-REFERENCE.md` | Szczegóły operacyjne przeniesione z CLAUDE.md |
**⚠️ WAŻNE:** Przed zmianami w NPM/proxy przeczytaj `docs/architecture/08-critical-configurations.md`
@ -59,51 +61,30 @@ nordabiz/
- **Uruchomienie:** `python3 app.py`
- **Docker DB:** `docker compose up -d` (jeśli nie działa)
### Staging (NOWE - 2026-02-02)
### Staging
- **Serwer:** NORDABIZ-STAGING-01 (VM 248, IP 10.22.68.248)
- **Baza:** PostgreSQL `nordabiz_staging` na 10.22.68.248:5432
- **Domena:** staging.nordabiznes.pl
- **NPM Proxy Host ID:** 44
- **SSL:** Let's Encrypt (ważny do 2026-05-03)
**Cel:** Testowanie zmian przed wdrożeniem na produkcję (E2E, integration tests)
**Weryfikacja:**
```bash
curl -I https://staging.nordabiznes.pl/health
ssh maciejpi@10.22.68.248
```
- **Domena:** staging.nordabiznes.pl (NPM Proxy Host ID: 44)
- **Weryfikacja:** `curl -I https://staging.nordabiznes.pl/health`
### Production
- **Serwer:** NORDABIZ-01 (VM 249, IP 10.22.68.249)
- **Baza:** PostgreSQL na 10.22.68.249:5432
- **Reverse Proxy:** NPM na R11-REVPROXY-01 (VM 119, IP 10.22.68.250)
- **Domena:** nordabiznes.pl (DNS w OVH)
- **SSL:** Let's Encrypt (auto-renewal)
- **Domena:** nordabiznes.pl (DNS w OVH, SSL Let's Encrypt)
### NPM Proxy Configuration (KRYTYCZNE!)
**Proxy Host ID:** 27
**Forward Port:** 5000 (NIE 80!)
**Proxy Host ID:** 27 | **Forward Port:** 5000 (NIE 80!)
```
PRAWIDŁOWA KONFIGURACJA:
NPM (10.22.68.250) → Backend (10.22.68.249:5000) ✓
BŁĘDNA KONFIGURACJA (powoduje pętlę przekierowań):
NPM (10.22.68.250) → Backend (10.22.68.249:80) ✗
NPM (10.22.68.250) → Backend (10.22.68.249:80) ✗ (pętla przekierowań!)
```
**UWAGA:** Na serwerze 10.22.68.249 działa nginx na porcie 80 który przekierowuje na HTTPS.
Flask/Gunicorn działa na porcie 5000. Przy edycji proxy hosta ZAWSZE sprawdź czy port = 5000!
Na serwerze .249 nginx na porcie 80 przekierowuje na HTTPS. Flask/Gunicorn na porcie 5000. **ZAWSZE** sprawdź port po edycji NPM!
**Weryfikacja po zmianach NPM:**
```bash
curl -I https://nordabiznes.pl/health
# Oczekiwany: HTTP 200
```
**Raport incydentu:** `docs/INCIDENT_REPORT_20260102.md`
**Weryfikacja:** `curl -I https://nordabiznes.pl/health` | **Incydent:** `docs/INCIDENT_REPORT_20260102.md`
## Git & Deployment
@ -111,21 +92,8 @@ curl -I https://nordabiznes.pl/health
| Remote | URL | Cel |
|--------|-----|-----|
| **origin** (GitHub) | `git@github.com:pienczyn/nordabiz.git` | Cloud backup, CI/CD ready |
| **inpi** (Gitea) | `git@10.22.68.180:maciejpi/nordabiz.git` | Wewnętrzny backup, deploy source |
**Konta:** GitHub: `pienczyn`, Gitea: `maciejpi`
### Workflow Deployment
```
┌─────────┐ git push ┌─────────┐ git pull ┌─────────┐ git pull ┌─────────┐
│ DEV │ ────────────► │ Gitea │ ◄──────────── │ STAGING │ ◄──────────── │ PROD │
│ (Mac) │ │ (INPI) │ │ (test) │ po testach │ │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
│ │
└──── git push ────► GitHub (CI/CD) ───────────────┘
```
| **origin** (GitHub) | `git@github.com:pienczyn/nordabiz.git` | Cloud backup, CI/CD |
| **inpi** (Gitea) | `git@10.22.68.180:maciejpi/nordabiz.git` | Wewnętrzny, deploy source |
### Procedura wdrażania (WAŻNE!)
@ -137,206 +105,81 @@ git push origin master && git push inpi master
# 2. STAGING: Wdrożenie i test
ssh maciejpi@10.22.68.248 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes"
curl -s https://staging.nordabiznes.pl/health
# ⚠️ OBOWIĄZKOWO: Test manualny nowej funkcjonalności na staging!
# 3. PROD: Pull zmiany (DOPIERO PO WERYFIKACJI STAGING!)
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && sudo -u www-data git pull"
# 4. PROD: Uruchom 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"
# 5. PROD: Restart serwisu
# 5. PROD: Restart + weryfikacja
ssh maciejpi@10.22.68.249 "sudo systemctl restart nordabiznes"
# 6. Weryfikacja produkcji
curl -sI https://nordabiznes.pl/health | head -3
```
**⚠️ UWAGI KRYTYCZNE:**
1. **Migracje SQL** - NIE używaj `psql` bezpośrednio. Użyj `scripts/run_migration.py`
2. **Uprawnienia logów** - Jeśli błąd `Permission denied: /var/log/nordabiznes/*`:
```bash
ssh maciejpi@10.22.68.249 "sudo chown -R maciejpi:maciejpi /var/log/nordabiznes/"
```
2. **Uprawnienia logów** - `sudo chown -R maciejpi:maciejpi /var/log/nordabiznes/`
3. **502 po restarcie** - Poczekaj 3-5 sekund i sprawdź ponownie
4. **Git pull** - Używaj `sudo -u www-data git pull` (www-data ma klucze SSH)
5. **SSH timeout** - NIE oznacza że komenda nie została wykonana! Sprawdź `ps aux | grep <skrypt>`
**Uruchamianie skryptów na produkcji (KRYTYCZNE!):**
- SSH timeout NIE oznacza że komenda nie została wykonana!
- ZAWSZE sprawdź czy poprzedni proces nie działa: `ps aux | grep <skrypt>`
- Dla długich operacji używaj `nohup` lub `screen`
- Incydent: `docs/INCIDENT_REPORT_20260115.md`
**Skrypty Python z dostępem do bazy (WAŻNE!):**
**Uruchamianie skryptów Python z dostępem do bazy (WAŻNE!):**
Skrypty Python wymagające dostępu do bazy danych MUSZĄ mieć ustawiony `DATABASE_URL`.
Plik `.env` NIE jest automatycznie wczytywany przez `source .env` - to nie działa w kontekście SSH!
`.env` NIE jest wczytywany przez `source .env` w kontekście SSH!
```bash
# ✅ PRAWIDŁOWO - ustaw DATABASE_URL bezpośrednio:
ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && \
DATABASE_URL='postgresql://nordabiz_app:HASŁO@127.0.0.1:5432/nordabiz' \
/var/www/nordabiznes/venv/bin/python3 skrypt.py"
# ✅ PRAWIDŁOWO - pobierz DATABASE_URL z .env:
# ✅ PRAWIDŁOWO:
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 skrypt.py"
# ❌ BŁĘDNIE - source .env nie działa przez SSH:
# ❌ BŁĘDNIE:
ssh maciejpi@10.22.68.249 "source .env && python3 skrypt.py"
```
**Hasło do bazy (produkcja):** W pliku `/var/www/nordabiznes/.env` (DATABASE_URL)
## Auto Claude - Rozwiązywanie problemów
### Pliki stanu (NIE COMMITOWAĆ!)
- `.auto-claude-security.json`, `.auto-claude-status`, `.auto-claude/`
- Są w `.gitignore` + pre-commit hook automatycznie je usuwa ze staging
### Konflikty merge z Auto Claude
```bash
# 1. Sprawdź konflikt
git status
# 2. Usuń pliki Auto Claude z merge
git rm .auto-claude-security.json .auto-claude-status
# 3. Dokończ merge
git commit -m "Merge branch 'feature' - resolve Auto Claude file conflicts"
```
### Worktrees
```bash
git worktree list # Lista aktywnych
git worktree remove .auto-claude/worktrees/tasks/<task-id> # Usuń nieaktualny
git branch -d auto-claude/<task-id> # Usuń branch
```
## Konwencje danych
### Identyfikatory firm
- **Slug:** kebab-case, np. `pixlab-sp-z-o-o`
- **NIP:** 10 cyfr bez myślników
- **REGON:** 9 lub 14 cyfr
- **KRS:** 10 cyfr (tylko spółki)
### Kategorie firm
`IT`, `Construction`, `Services`, `Production`, `Trade`, `Other`
### Poziomy jakości danych
`basic`, `enhanced`, `complete`
- **NIP:** 10 cyfr bez myślników | **REGON:** 9 lub 14 cyfr | **KRS:** 10 cyfr (tylko spółki)
- **Kategorie:** `IT`, `Construction`, `Services`, `Production`, `Trade`, `Other`
- **Jakość danych:** `basic`, `enhanced`, `complete`
## Ważne zasady
### Bezpieczeństwo
### Bezpieczeństwo & Credentials
- NIE edytuj bezpośrednio bazy produkcyjnej PostgreSQL
- Testuj zmiany na DEV (Docker: localhost:5433) przed wdrożeniem
- Klucze API i hasła tylko w `.env` (nigdy w kodzie)
- Klucze API i hasła **tylko w `.env`** (nigdy w kodzie!) — `docs/CREDENTIALS.md`
- Rate limiting: 200 req/dzień, 50 req/godzinę
**Panel bezpieczeństwa:** `/admin/security`
- 2FA, CSRF, HTTPS, GeoIP Blocking, Rate Limiting, Account Lockout, Audit Log
### Zarządzanie credentials (KRYTYCZNE!)
**NIGDY nie umieszczaj haseł w kodzie źródłowym!**
```python
# ✅ PRAWIDŁOWO:
DATABASE_URL = os.getenv('DATABASE_URL')
# ❌ BŁĘDNIE:
DATABASE_URL = 'postgresql://user:password123@localhost/db'
```
- Wszystkie sekrety w `.env` (nigdy w `.py`, `.sh`, `.js`)
- Fallback jako placeholder: `CHANGE_ME` (nie prawdziwe hasło!)
**Pełne zasady:** `docs/CREDENTIALS.md`
- Panel bezpieczeństwa: `/admin/security`
### Deployment
- Przed wdrożeniem: `python -m py_compile app.py`
- SSH do NORDABIZ-01: `ssh maciejpi@10.22.68.249` (ZAWSZE jako maciejpi!)
- Ścieżka aplikacji: `/var/www/nordabiznes`
- Restart: `sudo systemctl restart nordabiznes`
- **ZAWSZE** aktualizuj historię zmian (`release_notes` w app.py)
- SSH: `ssh maciejpi@10.22.68.249` (ZAWSZE jako maciejpi!)
- Ścieżka: `/var/www/nordabiznes` | Restart: `sudo systemctl restart nordabiznes`
- **ZAWSZE** aktualizuj `release_notes` w app.py
### Szablony Jinja2 - WAŻNE!
- Blok `{% block extra_js %}` w `base.html` jest już wewnątrz `<script>`
- **NIE DODAWAJ** własnych tagów `<script>` w `extra_js`!
- Prawidłowo: `{% block extra_js %}function foo() {...}{% endblock %}`
- `{% block extra_js %}` w `base.html` jest **wewnątrz** `<script>`**NIE DODAWAJ** własnych tagów `<script>`!
### Uprawnienia PostgreSQL
- Po utworzeniu tabel: `GRANT ALL ON TABLE ... TO nordabiz_app`
- Po utworzeniu sekwencji: `GRANT USAGE, SELECT ON SEQUENCE ... TO nordabiz_app`
## Infrastruktura testowa (NOWE - 2026-02-02)
### Uruchamianie testów lokalnie
## Infrastruktura testowa
```bash
# Wszystkie testy
pytest tests/ -v
# Tylko unit testy
pytest tests/unit/ -v
# Tylko integration testy
pytest tests/integration/ -v
# Testy z coverage
pytest tests/ --cov=. --cov-report=html
pytest tests/ -v # Wszystkie testy
pytest tests/unit/ -v # Unit testy (bez bazy)
pytest tests/integration/ -v # Integration testy (z bazą)
pytest tests/ --cov=. --cov-report=html # Testy z coverage
```
### Struktura testów
CI/CD: `.github/workflows/test.yml` (unit → e2e na staging → smoke na prod)
```
tests/
├── conftest.py # Fixtures (app, client, auth_client, admin_client)
├── pytest.ini # Konfiguracja pytest
├── unit/ # Unit testy (bez bazy)
├── integration/ # Integration testy (z bazą)
├── security/ # OWASP security tests
├── smoke/ # Smoke testy produkcyjne
└── e2e/ # Playwright E2E (na staging)
```
### CI/CD (GitHub Actions)
Workflow: `.github/workflows/test.yml`
| Job | Trigger | Środowisko |
|-----|---------|------------|
| unit-tests | push/PR | GitHub runner |
| e2e-tests | po unit | staging.nordabiznes.pl |
| smoke-tests | master only | nordabiznes.pl |
### GitHub Secrets
| Secret | Użycie |
|--------|--------|
| STAGING_URL | E2E tests base URL |
| TEST_USER_EMAIL | Logowanie w testach |
| TEST_USER_PASSWORD | Logowanie w testach |
| TEST_DATABASE_URL | Integration tests |
### Testowanie na produkcji
**Konta testowe (PROD):**
| Konto | Email | Hasło | Rola |
|-------|-------|-------|------|
| Test User | `test@nordabiznes.pl` | `&Rc2LdbSw&jiGR0ek@Bz` | Użytkownik |
| Test Admin | `testadmin@nordabiznes.pl` | `cSfQbbwegwv1v3Q2Dm0Q` | Admin |
**Konta testowe (PROD):** Brak (usunięte 2026-02-04)
## API Endpoints (podstawowe)
@ -351,291 +194,27 @@ Workflow: `.github/workflows/test.yml`
Pełna lista: `docs/architecture/10-api-endpoints.md`
## SearchService
## Moduły (szczegóły w docs/)
Unified search dla chatbota i `/search`. Szczegóły: `docs/DEVELOPMENT.md#searchservice`
- **SearchService:** Unified search dla chatbota i `/search``docs/DEVELOPMENT.md#searchservice`
- **Chatbot AI:** Limit 8 firm, 10 wiadomości historii — `docs/DEVELOPMENT.md#chatbot-ai`
- **News Monitoring:** Panel `/admin/news`, statusy: pending/approved/rejected
- **ZOPK News:** Panel `/admin/zopk/news`, auto-approve score >= 3 — szczegóły w `docs/CLAUDE-REFERENCE.md`
- **Social Media:** Panel `/admin/social-media` (FB, IG, LinkedIn, YT, TikTok, Twitter)
- **Audyt SEO:** Panel `/admin/seo`, API Google PageSpeed Insights
- **Website Updater:** Cron miesięczny, `scripts/website_content_updater.py` — szczegóły w `docs/CLAUDE-REFERENCE.md`
## Chatbot AI
## NordaGPT - Konfiguracja AI
- **Limit firm:** 8
- **Historia:** 10 wiadomości
Szczegóły: `docs/DEVELOPMENT.md#chatbot-ai`
- **Model:** `gemini-3-flash-preview` (alias `3-flash`) — thinking mode, 7x lepszy reasoning
- **SDK:** `google-genai>=1.0.0`
- **Inicjalizacja:** `app.py:286``gemini_service.init_gemini_service(model='3-flash')`
- **Zmiana modelu:** Edytuj alias w `app.py:286`. Katalog modeli: `docs/CLAUDE-REFERENCE.md`
## Powiązane zasoby
- **Źródło danych:** https://norda-biznes.info/czlonkowie
- **Backup:** Proxmox Backup Server (VM snapshots)
- **Backup:** Proxmox Backup Server — szczegóły DR: `docs/DR-PLAYBOOK.md` i `docs/CLAUDE-REFERENCE.md`
- **DNS wewnętrzny:** nordabiznes.inpi.local
## Disaster Recovery
**Pełna dokumentacja:** `docs/DR-PLAYBOOK.md`
### Metryki SLA
| Metryka | Wartość |
|---------|---------|
| **RTO** | 30-60 min |
| **RPO** | 1 godzina |
### Lokalizacje backupów
| Lokalizacja | Ścieżka | Retencja |
|-------------|---------|----------|
| Hourly (lokalnie) | `/var/backups/nordabiz/hourly/` | 24h |
| Daily (lokalnie) | `/var/backups/nordabiz/daily/` | 30 dni |
| Offsite (PBS) | `10.22.68.127:/backup/nordabiz/` | 30 dni |
| VM Snapshots | Proxmox | 3 snapshoty |
### Szybkie przywracanie
```bash
# Lista dostępnych backupów
ssh maciejpi@10.22.68.249 "ls -lt /var/backups/nordabiz/hourly/ | head -5"
# 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"
# Weryfikacja
curl -I https://nordabiznes.pl/health
```
### Cron backupy (automatyczne)
```bash
# Backup co godzinę
0 * * * * postgres pg_dump -Fc nordabiz > /var/backups/nordabiz/hourly/nordabiz_$(date +\%Y\%m\%d_\%H).dump
# Backup dzienny
0 2 * * * postgres pg_dump -Fc nordabiz > /var/backups/nordabiz/daily/nordabiz_$(date +\%Y\%m\%d).dump
# Sync do PBS
0 4 * * * root rsync -avz /var/backups/nordabiz/daily/ maciejpi@10.22.68.127:/backup/nordabiz/daily/
```
Data wdrożenia: 2026-02-02
## Szablon profilu firmy
### Docelowa struktura (po optymalizacji)
```
1. Header (nazwa, kategoria, badge, krótki opis)
2. Pasek kontaktowy (www, email, telefon, lokalizacja)
3. O firmie (połączone opisy)
4. Usługi i kompetencje (połączone tagi)
5. Wyróżniki (połączone z wartościami)
6. Dane kontaktowe (pełne karty)
7. Informacje prawne i biznesowe
8. Social Media (wszystkie 6 platform)
9. Strona WWW (analiza techniczna)
```
**Szablon:** `templates/company_detail.html`
## News Monitoring
System monitoringu wzmianek o firmach w mediach. Panel: `/admin/news`
**Statusy:** `pending`, `approved`, `rejected`
Szczegóły: `docs/DEVELOPMENT.md#news-monitoring`
## ZOP Kaszubia News (ZOPK)
System monitoringu newsów projektu **Zielony Okręg Przemysłowy Kaszubia**.
Panel: `/admin/zopk/news`
### Tematy istotne
- Zielony Okręg Przemysłowy Kaszubia
- Elektrownia jądrowa (Lubiatowo-Kopalino)
- Offshore wind Bałtyk (Baltic Power, Baltica)
- Via Pomerania, Droga Czerwona
- Kongsberg (inwestycje w Rumi)
- Izba Przedsiębiorców NORDA
### Tematy do odrzucenia
- Turystyka na Kaszubach
- Polityka ogólnopolska
- Inne regiony Polski
- Wypadki, clickbait
### Reguły auto-approve (WAŻNE!)
**Próg: score >= 3**
| Score | Status |
|-------|--------|
| 1-2 | `pending` |
| 3-5 | `auto_approved` |
**Plik:** `zopk_news_service.py` (linie 890, 1124, 1145)
## Social Media
Panel audytu: `/admin/social-media`
Platformy: Facebook, Instagram, LinkedIn, YouTube, TikTok, Twitter
Szczegóły: `docs/DEVELOPMENT.md#social-media`
## Audyt SEO
Panel: `/admin/seo`
API: Google PageSpeed Insights
Szczegóły: `docs/DEVELOPMENT.md#audyt-seo`
## Dodatkowe rejestry (TODO - przyszłe integracje)
| Rejestr | Opis | API |
|---------|------|-----|
| **CRBR** | Centralny Rejestr Beneficjentów Rzeczywistych | https://crbr.podatki.gov.pl |
| **SUDOP** | System Udostępniania Danych o Pomocy Publicznej | https://sudop.uokik.gov.pl |
Rejestry te mogą dostarczyć dodatkowe dane o firmach (beneficjenci rzeczywiści, otrzymana pomoc publiczna).
## Plan rozwoju
Roadmap, priorytety i strategia monetyzacji: `docs/ROADMAP.md`
## Cykliczna aktualizacja danych ze stron www firm
### Mechanizm
Skrypt `scripts/website_content_updater.py` automatycznie:
1. Pobiera treść strony www każdej firmy członkowskiej
2. Używa **Gemini 3 Flash** (darmowy plan) do ekstrakcji:
- `services_extracted` - lista usług oferowanych przez firmę
- `main_keywords` - główne słowa kluczowe
- `content_summary` - krótkie podsumowanie działalności
3. Zapisuje do tabeli `company_website_analysis`
### Uruchamianie ręczne
```bash
# Wszystkie firmy
cd /var/www/nordabiznes
/var/www/nordabiznes/venv/bin/python3 scripts/website_content_updater.py
# Konkretna firma
/var/www/nordabiznes/venv/bin/python3 scripts/website_content_updater.py --company-id 26
# Tylko firmy nieaktualizowane 30+ dni
/var/www/nordabiznes/venv/bin/python3 scripts/website_content_updater.py --stale-days 30
# Podgląd bez zapisywania (dry-run)
/var/www/nordabiznes/venv/bin/python3 scripts/website_content_updater.py --dry-run
```
### Cron (automatyczne uruchamianie)
Uruchamiany **1-ego każdego miesiąca o 3:00** dla firm nieaktualizowanych 30+ dni:
```bash
# Dodaj do crontab na serwerze produkcyjnym:
sudo crontab -e
# Wpis:
0 3 1 * * cd /var/www/nordabiznes && /var/www/nordabiznes/venv/bin/python3 scripts/website_content_updater.py --stale-days 30 >> /var/log/nordabiznes/website_updater.log 2>&1
```
### Logi
Logi zapisywane do: `/var/log/nordabiznes/website_updater.log`
### Rate limiting
- **2 sekundy** przerwy między firmami (respektowanie limitów free tier Gemini)
- ~150 firm = ~5 minut na pełną aktualizację
Data wdrożenia: 2026-02-01
## NordaGPT - Konfiguracja AI
### Aktualny model (stan: 2026-01-29)
- **Model:** `gemini-3-flash-preview` (Gemini 3 Flash Preview)
- **SDK:** `google-genai>=1.0.0` (nowy SDK z thinking mode)
- **Inicjalizacja:** `app.py:286` - `gemini_service.init_gemini_service(model='3-flash')`
- **Silnik chatu:** `nordabiz_chat.py` używa globalnego `gemini_service`
### Thinking Mode (NOWE!)
Użytkownicy mogą wybierać poziom rozumowania AI w UI chatu (dropdown obok badge "Gemini 3").
| Poziom | Opis | Zastosowanie |
|--------|------|--------------|
| **Błyskawiczny** (minimal) | Najszybsze odpowiedzi | Proste pytania: "kto?", "gdzie?" |
| **Szybki** (low) | Zrównoważony | Większość pytań o firmy i usługi |
| **Głęboki** (high) | Maksymalna analiza | Złożone pytania, rekomendacje, strategia |
**Zmiana poziomu:**
- UI: Dropdown w headerze chatu
- API: `POST /api/chat/settings` z `{"thinking_level": "high"}`
- Zapisywane w sesji użytkownika
### Dostępne modele w `gemini_service.py`
| Alias | Model ID | Opis |
|-------|----------|------|
| `flash` | `gemini-2.5-flash` | Ogólnego przeznaczenia |
| `flash-lite` | `gemini-2.5-flash-lite` | Ultra tani ($0.10/$0.40 per 1M) |
| `pro` | `gemini-2.5-pro` | Najlepszy reasoning |
| `flash-2.0` | `gemini-2.0-flash` | Poprzednia generacja (wycofywany 31.03.2026) |
| `3-flash` | `gemini-3-flash-preview` | **AKTUALNY** - 7x lepszy reasoning, thinking mode |
| `3-pro` | `gemini-3-pro-preview` | Premium - 2M context |
### Zmiana modelu
```python
# W app.py linia ~286:
gemini_service.init_gemini_service(model='3-flash') # Zmień alias tutaj
```
### UI Badge
W `templates/chat.html` badge w headerze: `<span class="chat-header-badge">Gemini 3</span>`
## Prezentacja dla członków Izby (AKTYWNY PROJEKT)
### Cel projektu
Stworzenie materiałów wideo prezentujących portal NordaBiz dla członków Izby NORDA.
### Produkty końcowe
1. **Podcast NotebookLM** (2-3 min) - rozmowa AI o portalu
2. **Zajawka Remotion** (30s) - scenariusz "Problem → Rozwiązanie"
3. **Tutorial wideo** (2-3 min) - nagrania portalu + dialogi Zofia/Marek
4. **Integracja z portalem** - Akademia + widget na dashboardzie
### Dokument źródłowy dla NotebookLM
`docs/notebooklm-source.md` - markdown do wgrania do notebooklm.google.com
### Scenariusz zajawki 30s (Remotion)
```
[0-8s] "Szukasz partnera do projektu?"
[8-16s] "Nie wiesz, kto w Izbie ma potrzebne kompetencje?"
[16-24s] "NordaGPT zna 150 firm i pomoże Ci znaleźć"
[24-30s] "nordabiznes.pl - Twoja sieć kontaktów"
```
### Głosy edge-tts
- Marek: `pl-PL-MarekNeural` (męski)
- Zofia: `pl-PL-ZofiaNeural` (żeński)
### GIFy do nagrania (Chrome MCP)
1. Strona główna zalogowanego
2. Katalog firm + filtrowanie
3. Profil firmy
4. Chat NordaGPT - pytanie
5. Chat NordaGPT - odpowiedź
6. Forum
7. Kalendarz
8. Tablica B2B
### Pliki Remotion
- Lokalizacja: `/Users/maciejpi/claude/projects/active/remotion/my-video/`
- Komponenty: `NordaBizZajawka.tsx`, `NordaBizTutorial.tsx`
- Audio: `public/audio/`, `public/voice/tutorial/`
- Nagrania: `public/recordings/*.gif`
### Konto testowe (PROD)
- Email: `test@nordabiznes.pl`
- Hasło: `&Rc2LdbSw&jiGR0ek@Bz`
Data projektu: 2026-01-29
- **Prezentacja Izby:** Remotion wideo — szczegóły w `docs/CLAUDE-REFERENCE.md`
- **Plan rozwoju:** `docs/ROADMAP.md`

View File

@ -323,6 +323,97 @@ def admin_company_delete(company_id):
db.close()
@bp.route('/companies/<int:company_id>/hard-delete', methods=['POST'])
@login_required
@role_required(SystemRole.ADMIN)
def admin_company_hard_delete(company_id):
"""Permanently delete an archived company and all related data."""
from sqlalchemy import text
db = SessionLocal()
try:
company = db.query(Company).filter(Company.id == company_id).first()
if not company:
return jsonify({'success': False, 'error': 'Firma nie istnieje'}), 404
if company.status != 'archived':
return jsonify({
'success': False,
'error': 'Tylko zarchiwizowane firmy mogą być trwale usunięte. Najpierw zarchiwizuj firmę.'
}), 400
company_name = company.name
db.expunge(company)
# 1) Nullable FKs → SET NULL
nullable_fk_updates = [
"UPDATE users SET company_id = NULL WHERE company_id = :cid",
"UPDATE companies SET parent_company_id = NULL WHERE parent_company_id = :cid",
"UPDATE companies SET it_provider_company_id = NULL WHERE it_provider_company_id = :cid",
"UPDATE norda_events SET speaker_company_id = NULL WHERE speaker_company_id = :cid",
"UPDATE classifieds SET company_id = NULL WHERE company_id = :cid",
"UPDATE membership_applications SET company_id = NULL WHERE company_id = :cid",
"UPDATE zopk_knowledge_entities SET company_id = NULL WHERE company_id = :cid",
"UPDATE membership_fee_config SET company_id = NULL WHERE company_id = :cid",
"UPDATE ai_usage_logs SET company_id = NULL WHERE company_id = :cid",
]
# 2) NOT NULL FKs without CASCADE → DELETE records
not_null_fk_deletes = [
"DELETE FROM company_services WHERE company_id = :cid",
"DELETE FROM company_competencies WHERE company_id = :cid",
"DELETE FROM certifications WHERE company_id = :cid",
"DELETE FROM awards WHERE company_id = :cid",
"DELETE FROM company_events WHERE company_id = :cid",
"DELETE FROM company_digital_maturity WHERE company_id = :cid",
"DELETE FROM company_website_analysis WHERE company_id = :cid",
"DELETE FROM company_quality_tracking WHERE company_id = :cid",
"DELETE FROM company_website_content WHERE company_id = :cid",
"DELETE FROM company_ai_insights WHERE company_id = :cid",
"DELETE FROM ai_enrichment_proposals WHERE company_id = :cid",
"DELETE FROM maturity_assessments WHERE company_id = :cid",
]
# 3) CASCADE tables (auto-handled by DB, but explicit for safety)
cascade_fk_deletes = [
"DELETE FROM user_company_permissions WHERE company_id = :cid",
"DELETE FROM company_contacts WHERE company_id = :cid",
"DELETE FROM company_social_media WHERE company_id = :cid",
"DELETE FROM company_recommendations WHERE company_id = :cid",
"DELETE FROM gbp_audits WHERE company_id = :cid",
"DELETE FROM it_audits WHERE company_id = :cid",
"DELETE FROM it_collaboration_matches WHERE company_a_id = :cid OR company_b_id = :cid",
"DELETE FROM membership_fees WHERE company_id = :cid",
"DELETE FROM zopk_company_links WHERE company_id = :cid",
"DELETE FROM company_people WHERE company_id = :cid",
"DELETE FROM krs_audits WHERE company_id = :cid",
"DELETE FROM company_pkd WHERE company_id = :cid",
"DELETE FROM company_financial_reports WHERE company_id = :cid",
]
for sql in nullable_fk_updates + not_null_fk_deletes + cascade_fk_deletes:
try:
db.execute(text(sql), {"cid": company_id})
except Exception:
pass # Table may not exist yet
db.execute(text("DELETE FROM companies WHERE id = :cid"), {"cid": company_id})
db.commit()
logger.info(f"Admin {current_user.email} permanently deleted company {company_name} (ID: {company_id})")
return jsonify({
'success': True,
'message': f'Firma "{company_name}" została trwale usunięta'
})
except Exception as e:
db.rollback()
logger.error(f"Error permanently deleting company {company_id}: {e}")
return jsonify({'success': False, 'error': f'Błąd: {str(e)}'}), 500
finally:
db.close()
@bp.route('/companies/<int:company_id>/assign-user', methods=['POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)

View File

@ -621,6 +621,9 @@ def release_notes():
'<strong>Środowisko staging</strong> - wizualne wskaźniki testowe (oznaczenie staging w UI)',
'Pinning wersji zależności w requirements.txt',
'Aktualizacja beautifulsoup4, cryptography, SQLAlchemy, Werkzeug',
'<strong>Strefa RADA</strong> - uproszczona do systemu posiedzeń (usunięto sekcję dokumentów i konwersję LibreOffice)',
'<strong>Moduł Korzyści</strong> - kolumna prowizji widoczna tylko dla właściciela',
'<strong>Trwałe usuwanie firm</strong> - workflow: aktywna → archiwum → trwałe usunięcie (tylko ADMIN)',
],
'fix': [
'<strong>CSRF tokeny w formularzach publikacji</strong> - naprawiono brak tokenów w formularzach programu/protokołu',
@ -628,6 +631,7 @@ def release_notes():
'Przycisk RSVP dla wydarzeń z ograniczonym dostępem',
'Poprawna nazwa endpointu membership.apply',
'Obsługa brakujących bibliotek systemowych weasyprint (OSError)',
'<strong>Usuwanie użytkowników</strong> - naprawiono błąd FK cascade przy relacjach zależnych',
],
},
{

View File

@ -555,11 +555,19 @@
<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
</svg>
</button>
{% if company.status == 'archived' and current_user.is_admin %}
<button class="btn-icon danger" onclick="hardDeleteCompany({{ company.id }}, '{{ company.name|e }}')" title="Trwale usuń">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
</button>
{% elif company.status != 'archived' %}
<button class="btn-icon danger" onclick="deleteCompany({{ company.id }}, '{{ company.name|e }}')" title="Archiwizuj">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</button>
{% endif %}
</div>
</td>
</tr>
@ -945,8 +953,11 @@
// Delete (Archive) Company
function deleteCompany(companyId, companyName) {
document.getElementById('confirmIcon').className = 'modal-icon warning';
document.getElementById('confirmTitle').textContent = 'Archiwizuj firmę';
document.getElementById('confirmDescription').textContent = `Czy na pewno chcesz zarchiwizować firmę "${companyName}"? Firma nie będzie widoczna w katalogu.`;
document.getElementById('confirmAction').textContent = 'Potwierdź';
document.getElementById('confirmAction').className = 'btn btn-danger';
confirmCallback = async () => {
try {
const response = await fetch(`/admin/companies/${companyId}/delete`, {
@ -971,6 +982,41 @@
document.getElementById('confirmModal').classList.add('active');
}
// Hard Delete (Permanent) - only for archived companies
function hardDeleteCompany(companyId, companyName) {
document.getElementById('confirmIcon').className = 'modal-icon danger';
document.getElementById('confirmTitle').textContent = 'Trwałe usunięcie firmy';
document.getElementById('confirmDescription').innerHTML =
`Czy na pewno chcesz <strong>trwale usunąć</strong> firmę "${companyName}"?<br><br>` +
'<strong style="color: var(--error);">Ta operacja jest NIEODWRACALNA!</strong><br>' +
'Wszystkie dane firmy zostaną permanentnie usunięte.';
document.getElementById('confirmAction').textContent = 'Trwale usuń';
document.getElementById('confirmAction').className = 'btn btn-danger';
confirmCallback = async () => {
try {
const response = await fetch(`/admin/companies/${companyId}/hard-delete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}
});
const data = await response.json();
if (data.success) {
const row = document.querySelector(`tr[data-company-id="${companyId}"]`);
if (row) row.remove();
showToast(data.message, 'success');
} else {
showToast(data.error || 'Wystąpił błąd', 'error');
}
} catch (error) {
showToast('Błąd połączenia', 'error');
}
};
document.getElementById('confirmModal').classList.add('active');
}
function closeConfirmModal() {
document.getElementById('confirmModal').classList.remove('active');
confirmCallback = null;