diff --git a/.claude/DEPLOYMENT_STATE.md b/.claude/DEPLOYMENT_STATE.md index 4051cff..1545768 100644 --- a/.claude/DEPLOYMENT_STATE.md +++ b/.claude/DEPLOYMENT_STATE.md @@ -2,7 +2,7 @@ ## Status: WDROŻONE ✅ -Aplikacja Flask została pomyślnie wdrożona na NORDABIZ-01. +Aplikacja Flask została pomyślnie wdrożona na OVH VPS inpi-vps-waw01. ## Ukończone kroki @@ -16,12 +16,12 @@ Aplikacja Flask została pomyślnie wdrożona na NORDABIZ-01. ## Dostęp -- **LAN:** http://10.22.68.249 ✅ +- **LAN:** http://57.128.200.27 ✅ - **WAN:** https://nordabiznes.pl (maintenance mode w NPM) ## Ważne informacje -- **VM:** NORDABIZ-01 (ID 249, IP 10.22.68.249) +- **VM:** OVH VPS inpi-vps-waw01 (ID 249, IP 57.128.200.27) - **Baza:** PostgreSQL `nordabiz` (80 firm) - **Port aplikacji:** 5000 - **Venv:** /var/www/nordabiznes/venv/ diff --git a/.claude/commands/add-company.md b/.claude/commands/add-company.md index 6ed7640..a2db9b9 100644 --- a/.claude/commands/add-company.md +++ b/.claude/commands/add-company.md @@ -110,7 +110,7 @@ docker exec nordabiz-postgres psql -U nordabiz_app -d nordabiz -c "SELECT slug, ## Uwagi: - DEV: PostgreSQL via Docker na localhost:5433 -- PROD: PostgreSQL na 10.22.68.249:5432 +- PROD: PostgreSQL na 57.128.200.27:5432 - Dla produkcji: po przetestowaniu lokalnie, wdróż przez `/deploy` - Nowe firmy powinny pochodzić z oficjalnej listy członków Norda Biznes - Zawsze weryfikuj NIP przed dodaniem diff --git a/.claude/commands/backup.md b/.claude/commands/backup.md index bd2f9ee..b05906c 100644 --- a/.claude/commands/backup.md +++ b/.claude/commands/backup.md @@ -12,7 +12,7 @@ Opcjonalny argument określa typ backupu: ## System automatycznych backupów -### Harmonogram (cron na NORDABIZ-01) +### Harmonogram (cron na OVH VPS inpi-vps-waw01) | Typ | Częstotliwość | Godzina | Retencja | Lokalizacja | |-----|---------------|---------|----------|-------------| @@ -25,16 +25,16 @@ Opcjonalny argument określa typ backupu: ```bash # Ostatnie backupy hourly -ssh maciejpi@10.22.68.249 "ls -lt /var/backups/nordabiz/hourly/ | head -5" +ssh maciejpi@57.128.200.27 "ls -lt /var/backups/nordabiz/hourly/ | head -5" # Ostatnie backupy daily -ssh maciejpi@10.22.68.249 "ls -lt /var/backups/nordabiz/daily/ | head -5" +ssh maciejpi@57.128.200.27 "ls -lt /var/backups/nordabiz/daily/ | head -5" # Sprawdź offsite (PBS) ssh maciejpi@10.22.68.127 "ls -lt /backup/nordabiz/daily/ | head -5" # Rozmiar backupów -ssh maciejpi@10.22.68.249 "du -sh /var/backups/nordabiz/*" +ssh maciejpi@57.128.200.27 "du -sh /var/backups/nordabiz/*" ``` ## Kroki do wykonania: @@ -48,26 +48,26 @@ docker exec nordabiz-postgres pg_dump -U nordabiz_app nordabiz > "backups/dev_$( ### 2. Backup produkcyjnej bazy PostgreSQL Eksport do lokalnego: ```bash -ssh maciejpi@10.22.68.249 "sudo -u postgres pg_dump nordabiz" > "backups/prod_$(date +%Y%m%d_%H%M%S).sql" +ssh maciejpi@57.128.200.27 "sudo -u postgres pg_dump nordabiz" > "backups/prod_$(date +%Y%m%d_%H%M%S).sql" ``` Lub backup na serwerze: ```bash -ssh maciejpi@10.22.68.249 "sudo -u postgres pg_dump -Fc nordabiz > /tmp/backup_$(date +%Y%m%d_%H%M%S).dump" +ssh maciejpi@57.128.200.27 "sudo -u postgres pg_dump -Fc nordabiz > /tmp/backup_$(date +%Y%m%d_%H%M%S).dump" ``` ### 3. Backup plików konfiguracyjnych ```bash mkdir -p backups/config_$(date +%Y%m%d) cp .env backups/config_$(date +%Y%m%d)/dev.env -ssh maciejpi@10.22.68.249 "cat /var/www/nordabiznes/.env" > backups/config_$(date +%Y%m%d)/prod.env -ssh maciejpi@10.22.68.249 "cat /etc/nginx/sites-available/nordabiznes" > backups/config_$(date +%Y%m%d)/nginx.conf +ssh maciejpi@57.128.200.27 "cat /var/www/nordabiznes/.env" > backups/config_$(date +%Y%m%d)/prod.env +ssh maciejpi@57.128.200.27 "cat /etc/nginx/sites-available/nordabiznes" > backups/config_$(date +%Y%m%d)/nginx.conf ``` ### 4. Snapshot VM w Proxmox Użyj skill `proxmox-manager`: ``` -Utwórz snapshot VM NORDABIZ-01 (ID: 249) z opisem "Backup przed [operacja]" +Utwórz snapshot VM OVH VPS inpi-vps-waw01 (OVH VPS) z opisem "Backup przed [operacja]" ``` Lub ręcznie: @@ -83,14 +83,14 @@ ls -la backups/*.sql 2>/dev/null Snapshoty VM (użyj skill proxmox-manager): ``` -Pokaż snapshoty VM 249 +Pokaż snapshoty OVH VPS ``` ### 6. Przywracanie z backupu #### Szybkie przywracanie (skrypt DR) ```bash -ssh maciejpi@10.22.68.249 "sudo /var/www/nordabiznes/scripts/dr-restore.sh /var/backups/nordabiz/hourly/nordabiz_YYYYMMDD_HH.dump" +ssh maciejpi@57.128.200.27 "sudo /var/www/nordabiznes/scripts/dr-restore.sh /var/backups/nordabiz/hourly/nordabiz_YYYYMMDD_HH.dump" ``` #### DEV (Docker PostgreSQL): @@ -100,13 +100,13 @@ docker exec -i nordabiz-postgres psql -U nordabiz_app -d nordabiz < backups/dev_ #### PROD (PostgreSQL): ```bash -cat backups/prod_YYYYMMDD_HHMMSS.sql | ssh maciejpi@10.22.68.249 "sudo -u postgres psql nordabiz" +cat backups/prod_YYYYMMDD_HHMMSS.sql | ssh maciejpi@57.128.200.27 "sudo -u postgres psql nordabiz" ``` #### Rollback VM: Użyj skill `proxmox-manager`: ``` -Przywróć VM 249 ze snapshotu backup_YYYYMMDD +Przywróć OVH VPS ze snapshotu backup_YYYYMMDD ``` ## Konfiguracja cron (na serwerze PROD) @@ -162,6 +162,6 @@ sudo /var/www/nordabiznes/scripts/dr-restore.sh /path/to/backup.dump - Snapshoty VM są najszybsze do rollbacku - PostgreSQL dump jest przenośny między środowiskami - DEV używa Docker PostgreSQL na localhost:5433 -- PROD używa PostgreSQL na 10.22.68.249:5432 +- PROD używa PostgreSQL na 57.128.200.27:5432 Data aktualizacji: 2026-02-02 diff --git a/.claude/commands/chat-stats.md b/.claude/commands/chat-stats.md index a08c82e..bf59e03 100644 --- a/.claude/commands/chat-stats.md +++ b/.claude/commands/chat-stats.md @@ -99,4 +99,4 @@ Podsumuj w czytelnej formie: - Monitoruj tokeny dla kontroli limitów - Wysokie latency może wskazywać na problemy z API - DEV: `docker exec nordabiz-postgres psql -U nordabiz_app -d nordabiz` -- PROD: `ssh maciejpi@10.22.68.249 "sudo -u postgres psql -d nordabiz"` +- PROD: `ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz"` diff --git a/.claude/commands/data-report.md b/.claude/commands/data-report.md index 22cb592..124588e 100644 --- a/.claude/commands/data-report.md +++ b/.claude/commands/data-report.md @@ -127,4 +127,4 @@ Wygeneruj raport w formacie markdown z tabelami: - Priorytet: email > telefon > www > opis - Weryfikuj dane przez oficjalne źródła (CEIDG, KRS) - DEV: `docker exec nordabiz-postgres psql -U nordabiz_app -d nordabiz` -- PROD: `ssh maciejpi@10.22.68.249 "sudo -u postgres psql -d nordabiz"` +- PROD: `ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz"` diff --git a/.claude/commands/deploy.md b/.claude/commands/deploy.md index 1674846..5497db7 100644 --- a/.claude/commands/deploy.md +++ b/.claude/commands/deploy.md @@ -1,6 +1,6 @@ # Deploy NordaBiz to Production -Wykonaj deployment projektu NordaBiz na serwer produkcyjny NORDABIZ-01. +Wykonaj deployment projektu NordaBiz na serwer produkcyjny OVH VPS inpi-vps-waw01. ## Kroki do wykonania: @@ -10,7 +10,7 @@ Wykonaj deployment projektu NordaBiz na serwer produkcyjny NORDABIZ-01. - Sprawdź czy lokalna aplikacja działa: `curl http://localhost:5000/health` lub `curl http://localhost:5001/health` ### 2. Połączenie z serwerem -- SSH do NORDABIZ-01: `ssh root@10.22.68.249` +- SSH do OVH VPS inpi-vps-waw01: `ssh maciejpi@57.128.200.27` - Przejdź do katalogu: `cd /var/www/nordabiznes` ### 3. Deployment diff --git a/.claude/commands/logs.md b/.claude/commands/logs.md index 5830802..ec1cfce 100644 --- a/.claude/commands/logs.md +++ b/.claude/commands/logs.md @@ -15,54 +15,54 @@ Opcjonalny argument określa typ logów lub liczbę linii, np.: Połącz się z serwerem i pobierz logi: ```bash -ssh root@10.22.68.249 "journalctl -u nordabiznes -n 50 --no-pager" +ssh maciejpi@57.128.200.27 "journalctl -u nordabiznes -n 50 --no-pager" ``` Dla więcej linii: ```bash -ssh root@10.22.68.249 "journalctl -u nordabiznes -n 100 --no-pager" +ssh maciejpi@57.128.200.27 "journalctl -u nordabiznes -n 100 --no-pager" ``` Tylko błędy: ```bash -ssh root@10.22.68.249 "journalctl -u nordabiznes -p err -n 50 --no-pager" +ssh maciejpi@57.128.200.27 "journalctl -u nordabiznes -p err -n 50 --no-pager" ``` ### 2. Logi Nginx (access) ```bash -ssh root@10.22.68.249 "tail -50 /var/log/nginx/access.log" +ssh maciejpi@57.128.200.27 "tail -50 /var/log/nginx/access.log" ``` ### 3. Logi Nginx (error) ```bash -ssh root@10.22.68.249 "tail -50 /var/log/nginx/error.log" +ssh maciejpi@57.128.200.27 "tail -50 /var/log/nginx/error.log" ``` ### 4. Logi w czasie rzeczywistym (follow) ```bash -ssh root@10.22.68.249 "journalctl -u nordabiznes -f" +ssh maciejpi@57.128.200.27 "journalctl -u nordabiznes -f" ``` (Ctrl+C aby przerwać) ### 5. Logi z określonego czasu Ostatnia godzina: ```bash -ssh root@10.22.68.249 "journalctl -u nordabiznes --since '1 hour ago' --no-pager" +ssh maciejpi@57.128.200.27 "journalctl -u nordabiznes --since '1 hour ago' --no-pager" ``` Dzisiaj: ```bash -ssh root@10.22.68.249 "journalctl -u nordabiznes --since today --no-pager" +ssh maciejpi@57.128.200.27 "journalctl -u nordabiznes --since today --no-pager" ``` ### 6. Szukanie wzorca ```bash -ssh root@10.22.68.249 "journalctl -u nordabiznes --no-pager | grep -i 'error\|exception\|failed'" +ssh maciejpi@57.128.200.27 "journalctl -u nordabiznes --no-pager | grep -i 'error\|exception\|failed'" ``` ### 7. Status usługi ```bash -ssh root@10.22.68.249 "systemctl status nordabiznes" +ssh maciejpi@57.128.200.27 "systemctl status nordabiznes" ``` ## Analiza logów: @@ -74,7 +74,7 @@ Po pobraniu logów przeanalizuj je pod kątem: - Nieautoryzowanych prób dostępu ## Uwagi: -- Serwer: NORDABIZ-01 (VM 249, IP 10.22.68.249) +- Serwer: OVH VPS inpi-vps-waw01 (OVH VPS, IP 57.128.200.27) - Usługa systemd: `nordabiznes` - Logi rotują automatycznie - Dla alertów rozważ integrację z Zabbix (skill: monitoring-manager) diff --git a/.claude/commands/status.md b/.claude/commands/status.md index 84e9d66..4cfb974 100644 --- a/.claude/commands/status.md +++ b/.claude/commands/status.md @@ -18,7 +18,7 @@ Sprawdź aktualny status projektu NordaBiz - lokalnie i na produkcji. - Test SSL: `curl -vI https://nordabiznes.pl 2>&1 | grep -E "(SSL|expire|subject)"` ### 3. Status VM (użyj skill proxmox-manager) -- VM: NORDABIZ-01 (ID: 249) +- VM: OVH VPS inpi-vps-waw01 (OVH VPS) - Sprawdź: CPU, RAM, uptime, snapshoty ### 4. Statystyki bazy danych (DEV via Docker) @@ -33,7 +33,7 @@ SELECT 'Wiadomości chat: ' || COUNT(*) FROM ai_chat_messages; ### 5. Statystyki bazy danych (PROD) ```bash -ssh maciejpi@10.22.68.249 "sudo -u postgres psql -d nordabiz -c \" +ssh maciejpi@57.128.200.27 "sudo -u postgres psql -d nordabiz -c \" SELECT 'Firmy: ' || COUNT(*) FROM companies; SELECT 'Użytkownicy: ' || COUNT(*) FROM users; SELECT 'Konwersacje chat: ' || COUNT(*) FROM ai_chat_conversations; @@ -52,11 +52,11 @@ Podsumuj status w formie tabeli: | Lokalna aplikacja | OK/ERROR | port | | Docker PostgreSQL | OK/ERROR | localhost:5433 | | Produkcja | OK/ERROR | response time | -| VM NORDABIZ-01 | OK/ERROR | uptime | +| VM OVH VPS inpi-vps-waw01 | OK/ERROR | uptime | | Baza danych DEV | OK/ERROR | liczba rekordów | | Baza danych PROD | OK/ERROR | liczba rekordów | | Git | CLEAN/DIRTY | branch | ## Uwagi: - DEV: PostgreSQL via Docker na localhost:5433 -- PROD: PostgreSQL na 10.22.68.249:5432 +- PROD: PostgreSQL na 57.128.200.27:5432 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1380a10 --- /dev/null +++ b/AGENTS.md @@ -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 ` +``` + +- [ ] **Step 2: Commit** + +```bash +git add templates/messages/conversations.html +git commit -m "feat(messages): add conversation view template" +``` + +--- + +## Task 10: Frontend — JavaScript + +**Files:** +- Create: `static/js/conversations.js` + +- [ ] **Step 1: Create conversations.js** + +Main JS file handling all client-side logic. Modules (as functions/objects, no build step): + +**ConversationList** — render conversation items, handle selection, search filtering, update on new messages, unread badges, muted/archived indicators + +**ChatView** — render messages for selected conversation, cursor-based scroll-up loading, scroll to bottom on new message, date separators, read receipt indicators (ptaszki for 1:1, avatars for groups) + +**MessageActions** — context menu on hover/long-press: reply, react, forward, pin, edit, delete. Reply-to quote UI. Edit mode. Delete confirmation. + +**Reactions** — emoji picker (6 emoji), toggle reaction via API, render pill badges, update via SSE + +**Composer** — Quill editor init, send on Enter (Shift+Enter = newline), attachment drag&drop (reuse existing pattern from compose.html), typing indicator (debounce 2s, POST to /api/conversations//typing) + +**SSEClient** — connect to /api/messages/stream, handle events (new_message, message_read, typing, reaction, message_edited, message_deleted, message_pinned, presence), heartbeat every 30s, auto-reconnect on disconnect + +**Presence** — batch fetch /api/users/presence for visible conversation members, update online dots and "last seen" text, refresh every 60s + +**Search** — filter conversation list client-side by name, search within conversation via API + +**Pins** — fetch/render pinned messages bar, pin/unpin actions + +**LinkPreview** — render link_preview JSON as card under message content + +- [ ] **Step 2: Commit** + +```bash +git add static/js/conversations.js +git commit -m "feat(messages): add conversation view JavaScript" +``` + +--- + +## Task 11: Old Routes — Redirect + Backward Compat + +**Files:** +- Modify: `blueprints/messages/routes.py` + +- [ ] **Step 1: Redirect old inbox/sent to new view** + +At the top of `messages_inbox` and `messages_sent` functions, add redirect: + +```python +@bp.route('/wiadomosci/stare') +@login_required +@member_required +def messages_inbox(): + # Old inbox — keep for direct links but redirect + ...existing code... + +# New primary route +@bp.route('/wiadomosci') +@login_required +@member_required +def conversations_view(): + return redirect(url_for('messages.conversations')) +``` + +Change old `/wiadomosci` route to `/wiadomosci/stare` so existing bookmarks still work but new links go to conversations view. + +- [ ] **Step 2: Update nav links** + +In `templates/base.html`, update the messages nav link to point to new route. + +- [ ] **Step 3: Commit** + +```bash +git add blueprints/messages/routes.py templates/base.html +git commit -m "feat(messages): redirect old inbox to new conversation view" +``` + +--- + +## Task 12: Data Migration Script + +**Files:** +- Create: `scripts/migrate_messages.py` + +- [ ] **Step 1: Create migration script** + +Script that: +1. Groups private_messages by unique (sender_id, recipient_id) pairs → creates Conversation per pair (is_group=False) +2. Maps message_group → Conversation (is_group=True) with name, owner +3. Maps message_group_member → ConversationMember with role, last_read_at +4. Maps private_messages → ConvMessage preserving content, sender_id, created_at +5. Maps parent_id → reply_to_id using old→new ID mapping +6. Maps group_message → ConvMessage +7. Updates message_attachments.conv_message_id using old→new mapping +8. Sets conversation.last_message_id and updated_at +9. Computes last_read_at for 1:1 members from PrivateMessage.read_at +10. Validation: count old messages == count new messages, print summary + +Usage: `DATABASE_URL=... python3 scripts/migrate_messages.py [--dry-run]` + +- [ ] **Step 2: Test on dev database** + +```bash +# Start Docker DB +docker compose up -d + +# Run migration (dry run) +DATABASE_URL=postgresql://nordabiz:nordabiz@localhost:5433/nordabiz python3 scripts/migrate_messages.py --dry-run + +# Run migration (real) +DATABASE_URL=postgresql://nordabiz:nordabiz@localhost:5433/nordabiz python3 scripts/migrate_messages.py +``` + +- [ ] **Step 3: Commit** + +```bash +git add scripts/migrate_messages.py +git commit -m "feat(messages): add data migration script (old → unified model)" +``` + +--- + +## Task 13: Nginx SSE Configuration + +**Files:** +- No code files — server configuration via SSH + +- [ ] **Step 1: Add SSE proxy config on staging** + +SSH to R11-REVPROXY-01 and add custom nginx config for the SSE endpoint: + +```nginx +location /api/messages/stream { + proxy_pass http://10.22.68.248:5000; + proxy_set_header Connection ''; + proxy_http_version 1.1; + chunked_transfer_encoding off; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 86400s; +} +``` + +This can be added via NPM's "Advanced" tab for the staging proxy host (ID 44). + +- [ ] **Step 2: Test SSE on staging** + +```bash +curl -N -H "Cookie: session=..." https://staging.nordabiznes.pl/api/messages/stream +``` + +Should receive `data: {"event": "connected"}` immediately. + +- [ ] **Step 3: Document** + +Add SSE config note to `docs/architecture/08-critical-configurations.md`. + +--- + +## Task 14: Deploy + Production Migration + +- [ ] **Step 1: Install Redis on production VM** + +```bash +ssh maciejpi@57.128.200.27 "sudo apt install redis-server -y && sudo systemctl enable redis-server" +``` + +- [ ] **Step 2: Deploy code to staging** + +```bash +git push origin master && git push inpi master +ssh maciejpi@10.22.68.248 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes" +``` + +- [ ] **Step 3: Run migration on staging** + +```bash +# Schema +ssh maciejpi@10.22.68.248 "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/091_messaging_redesign.sql" + +# Data migration +ssh maciejpi@10.22.68.248 "cd /var/www/nordabiznes && DATABASE_URL=\$(grep DATABASE_URL .env | cut -d'=' -f2) /var/www/nordabiznes/venv/bin/python3 scripts/migrate_messages.py" +``` + +- [ ] **Step 4: Test on staging** + +Verify at `https://staging.nordabiznes.pl/wiadomosci`: +- Conversation list renders with migrated data +- Opening a conversation shows message history +- Sending a message works +- SSE events arrive in real-time +- Reactions, pins, typing indicator work +- Mobile view works + +- [ ] **Step 5: Deploy to production (after staging verification)** + +```bash +# Same steps as staging but on 57.128.200.27 +# Plus: add SSE nginx config to production proxy host (ID 27) +``` + +- [ ] **Step 6: Update release notes** + +Add to `_get_releases()` in `blueprints/public/routes.py`. + +--- + +## Task 15: Unit Tests + +**Files:** +- Create: `tests/unit/test_conversation_models.py` +- Create: `tests/unit/test_conversation_api.py` +- Create: `tests/unit/test_link_preview.py` + +- [ ] **Step 1: Model tests** + +Test Conversation, ConversationMember, ConvMessage creation, relationships, display_name property, member_count. + +- [ ] **Step 2: Link preview tests** + +Test URL extraction, OG parsing, internal domain skipping, timeout handling. + +- [ ] **Step 3: API tests** + +Test conversation creation (1:1 dedup), message sending, reaction toggle, pin/unpin, mute/archive settings, membership checks. + +- [ ] **Step 4: Run all tests** + +```bash +pytest tests/unit/test_conversation_models.py tests/unit/test_conversation_api.py tests/unit/test_link_preview.py -v +``` + +- [ ] **Step 5: Commit** + +```bash +git add tests/unit/test_conversation_*.py tests/unit/test_link_preview.py +git commit -m "test(messages): add unit tests for conversation system" +``` diff --git a/docs/superpowers/plans/2026-03-28-nordagpt-identity-memory.md b/docs/superpowers/plans/2026-03-28-nordagpt-identity-memory.md index 6541ad9..36cb3a1 100644 --- a/docs/superpowers/plans/2026-03-28-nordagpt-identity-memory.md +++ b/docs/superpowers/plans/2026-03-28-nordagpt-identity-memory.md @@ -257,7 +257,7 @@ Type "Co wiesz o mnie?" — verify AI lists your profile data. - [ ] **Step 4: Deploy to production** ```bash -ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes" +ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes" curl -sI https://nordabiznes.pl/health | head -3 ``` @@ -918,7 +918,7 @@ ssh maciejpi@10.22.68.248 "journalctl -u nordabiznes -n 30 --no-pager | grep 'Ro - [ ] **Step 3: Deploy to production** ```bash -ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes" +ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes" curl -sI https://nordabiznes.pl/health | head -3 ``` @@ -1343,7 +1343,7 @@ git commit -m "feat(nordagpt): streaming UI — word-by-word response with think SSE requires Nginx to NOT buffer the response. The streaming endpoint sets `X-Accel-Buffering: no` header. Verify NPM custom config allows this: ```bash -ssh maciejpi@10.22.68.249 "cat /etc/nginx/sites-enabled/nordabiznes.conf 2>/dev/null || echo 'Using NPM proxy'" +ssh maciejpi@57.128.200.27 "cat /etc/nginx/sites-enabled/nordabiznes.conf 2>/dev/null || echo 'Using NPM proxy'" ``` If using NPM, the `X-Accel-Buffering: no` header should be sufficient. If not, add to NPM custom Nginx config for nordabiznes.pl: @@ -1364,7 +1364,7 @@ Test on staging: open chat, send message, verify text appears word-by-word. - [ ] **Step 3: Deploy to production** ```bash -ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes" +ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes" curl -sI https://nordabiznes.pl/health | head -3 ``` @@ -1901,10 +1901,10 @@ ssh maciejpi@10.22.68.248 "sudo systemctl restart nordabiznes" - [ ] **Step 4: Deploy to production** ```bash -ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && sudo -u www-data git pull" -ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && DATABASE_URL=\$(grep DATABASE_URL .env | cut -d'=' -f2) /var/www/nordabiznes/venv/bin/python3 scripts/run_migration.py database/migrations/092_ai_user_memory.sql" -ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && DATABASE_URL=\$(grep DATABASE_URL .env | cut -d'=' -f2) /var/www/nordabiznes/venv/bin/python3 scripts/run_migration.py database/migrations/093_ai_conversation_summary.sql" -ssh maciejpi@10.22.68.249 "sudo systemctl restart nordabiznes" +ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && sudo -u www-data git pull" +ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && DATABASE_URL=\$(grep DATABASE_URL .env | cut -d'=' -f2) /var/www/nordabiznes/venv/bin/python3 scripts/run_migration.py database/migrations/092_ai_user_memory.sql" +ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && DATABASE_URL=\$(grep DATABASE_URL .env | cut -d'=' -f2) /var/www/nordabiznes/venv/bin/python3 scripts/run_migration.py database/migrations/093_ai_conversation_summary.sql" +ssh maciejpi@57.128.200.27 "sudo systemctl restart nordabiznes" curl -sI https://nordabiznes.pl/health | head -3 ``` diff --git a/docs/superpowers/plans/2026-03-31-event-guests.md b/docs/superpowers/plans/2026-03-31-event-guests.md index 550e676..cb01c18 100644 --- a/docs/superpowers/plans/2026-03-31-event-guests.md +++ b/docs/superpowers/plans/2026-03-31-event-guests.md @@ -10,7 +10,7 @@ **Spec:** `docs/superpowers/specs/2026-03-31-event-guests-design.md` -**Deployment:** Staging first (`10.22.68.248`), user tests, then production (`10.22.68.249`). +**Deployment:** Staging first (`10.22.68.248`), user tests, then production (`57.128.200.27`). --- @@ -749,19 +749,19 @@ Expected: `HTTP/2 200` - [ ] **Step 1: Deploy to production** ```bash -ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && sudo -u www-data git pull" +ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && sudo -u www-data git pull" ``` - [ ] **Step 2: Run migration on production** ```bash -ssh maciejpi@10.22.68.249 "cd /var/www/nordabiznes && /var/www/nordabiznes/venv/bin/python3 scripts/run_migration.py database/migrations/096_event_guests.sql" +ssh maciejpi@57.128.200.27 "cd /var/www/nordabiznes && /var/www/nordabiznes/venv/bin/python3 scripts/run_migration.py database/migrations/096_event_guests.sql" ``` - [ ] **Step 3: Reload service** ```bash -ssh maciejpi@10.22.68.249 "sudo systemctl reload nordabiznes" +ssh maciejpi@57.128.200.27 "sudo systemctl reload nordabiznes" ``` - [ ] **Step 4: Verify production** diff --git a/docs/superpowers/specs/2026-03-15-uptime-monitoring-design.md b/docs/superpowers/specs/2026-03-15-uptime-monitoring-design.md index f56ec15..213e793 100644 --- a/docs/superpowers/specs/2026-03-15-uptime-monitoring-design.md +++ b/docs/superpowers/specs/2026-03-15-uptime-monitoring-design.md @@ -17,7 +17,7 @@ Podejście B: UptimeRobot (zewnętrzny monitoring) + wewnętrzny health logger z ## Architektura ``` -UptimeRobot.com (free) NORDABIZ-01 (10.22.68.249) +UptimeRobot.com (free) NORDABIZ-01 (57.128.200.27) │ sprawdza co 5 min │ wewnętrzny logger co 5 min │ HTTPS → nordabiznes.pl │ app/db/cpu/ram/disk → PostgreSQL │ │ diff --git a/docs/superpowers/specs/2026-03-27-messaging-redesign-design.md b/docs/superpowers/specs/2026-03-27-messaging-redesign-design.md new file mode 100644 index 0000000..68765b9 --- /dev/null +++ b/docs/superpowers/specs/2026-03-27-messaging-redesign-design.md @@ -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//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/ | Szczegóły + członkowie | +| PATCH | /api/conversations/ | Edytuj nazwę/opis | +| DELETE | /api/conversations/ | Usuń (owner) | +| POST | /api/conversations//members | Dodaj członka | +| DELETE | /api/conversations//members/ | Usuń członka | +| PATCH | /api/conversations//settings | Mute/archive | + +### Wiadomości +| Method | URL | Opis | +|--------|-----|------| +| GET | /api/conversations//messages | Paginacja cursor-based | +| POST | /api/conversations//messages | Wyślij | +| PATCH | /api/messages/ | Edytuj (swoje, max 24h) | +| DELETE | /api/messages/ | Soft delete (swoje) | +| POST | /api/messages//forward | Przekaż | +| POST | /api/conversations//read | Oznacz przeczytane | +| POST | /api/conversations//typing | Typing indicator | + +### Reakcje i przypięcia +| Method | URL | Opis | +|--------|-----|------| +| POST | /api/messages//reactions | Dodaj | +| DELETE | /api/messages//reactions/ | Usuń | +| POST | /api/messages//pin | Przypnij | +| DELETE | /api/messages//pin | Odepnij | +| GET | /api/conversations//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: + <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 diff --git a/docs/zabbix_setup.md b/docs/zabbix_setup.md index 843d0b0..df7b193 100644 --- a/docs/zabbix_setup.md +++ b/docs/zabbix_setup.md @@ -1,12 +1,11 @@ -# Zabbix Monitoring Setup - NORDABIZ-01 +# Zabbix Monitoring Setup - OVH VPS (inpi-vps-waw01) ## Informacje o serwerze | Parametr | Wartosc | |----------|---------| -| **Nazwa hosta** | NORDABIZ-01 | -| **VM ID** | 249 | -| **IP** | 10.22.68.249 | +| **Nazwa hosta** | inpi-vps-waw01 | +| **IP** | 57.128.200.27 | | **OS** | Ubuntu 22.04 LTS | | **Aplikacja** | Flask (NordaBiznes Partner) | | **Baza danych** | PostgreSQL 15 (localhost:5432) | @@ -29,7 +28,7 @@ Polacz sie z serwerem i sprawdz czy agent jest zainstalowany: ```bash -ssh maciejpi@10.22.68.249 +ssh maciejpi@57.128.200.27 # Sprawdz status agenta (Zabbix Agent 2 - nowsza wersja) systemctl status zabbix-agent2 @@ -100,7 +99,7 @@ Server=10.22.68.126 ServerActive=10.22.68.126 # Nazwa hosta - MUSI byc identyczna jak w Zabbix Server -Hostname=NORDABIZ-01 +Hostname=inpi-vps-waw01 # Port nasluchiwania (domyslny) ListenPort=10050 @@ -165,7 +164,7 @@ sudo nano /etc/zabbix/zabbix_agent2.d/nordabiznes.conf ```ini # ============================================ # NordaBiznes Partner - Custom Zabbix Monitoring -# Server: NORDABIZ-01 (10.22.68.249) +# Server: OVH VPS inpi-vps-waw01 (57.128.200.27) # ============================================ # --- FLASK APPLICATION --- @@ -283,11 +282,11 @@ zabbix_agent2 -t nordabiznes.disk_usage_mb | Pole | Wartosc | |------|---------| -| Host name | NORDABIZ-01 | -| Visible name | NordaBiznes Partner (10.22.68.249) | +| Host name | inpi-vps-waw01 | +| Visible name | NordaBiznes Partner (57.128.200.27) | | Templates | Linux by Zabbix agent, PostgreSQL by Zabbix agent 2 | | Host groups | Linux servers, Web servers, Databases | -| Interfaces | Agent: 10.22.68.249:10050 | +| Interfaces | Agent: 57.128.200.27:10050 | ### 5.2 Utworzenie dedykowanego template'u @@ -319,14 +318,14 @@ Utworz nowy template dla NordaBiznes: | Name | Expression | Severity | |------|------------|----------| -| NordaBiznes app is down | last(/NORDABIZ-01/nordabiznes.health)=0 | High | -| NordaBiznes service stopped | last(/NORDABIZ-01/nordabiznes.service_status)=0 | High | -| PostgreSQL is down | last(/NORDABIZ-01/postgresql.status)=0 | Disaster | -| High response time (>2s) | last(/NORDABIZ-01/nordabiznes.response_time)>2000 | Warning | -| Low Gunicorn workers | last(/NORDABIZ-01/nordabiznes.workers)<2 | Warning | -| High DB connections (>80) | last(/NORDABIZ-01/postgresql.connections)>80 | Warning | -| High memory usage (>500MB) | last(/NORDABIZ-01/nordabiznes.memory_mb)>500 | Warning | -| Disk usage >5GB | last(/NORDABIZ-01/nordabiznes.disk_usage_mb)>5120 | Warning | +| NordaBiznes app is down | last(/inpi-vps-waw01/nordabiznes.health)=0 | High | +| NordaBiznes service stopped | last(/inpi-vps-waw01/nordabiznes.service_status)=0 | High | +| PostgreSQL is down | last(/inpi-vps-waw01/postgresql.status)=0 | Disaster | +| High response time (>2s) | last(/inpi-vps-waw01/nordabiznes.response_time)>2000 | Warning | +| Low Gunicorn workers | last(/inpi-vps-waw01/nordabiznes.workers)<2 | Warning | +| High DB connections (>80) | last(/inpi-vps-waw01/postgresql.connections)>80 | Warning | +| High memory usage (>500MB) | last(/inpi-vps-waw01/nordabiznes.memory_mb)>500 | Warning | +| Disk usage >5GB | last(/inpi-vps-waw01/nordabiznes.disk_usage_mb)>5120 | Warning | --- @@ -336,20 +335,20 @@ Utworz nowy template dla NordaBiznes: ```bash # Na serwerze Zabbix (10.22.68.126) -zabbix_get -s 10.22.68.249 -k agent.ping +zabbix_get -s 57.128.200.27 -k agent.ping # Oczekiwany wynik: 1 -zabbix_get -s 10.22.68.249 -k nordabiznes.health +zabbix_get -s 57.128.200.27 -k nordabiznes.health # Oczekiwany wynik: 1 -zabbix_get -s 10.22.68.249 -k postgresql.status +zabbix_get -s 57.128.200.27 -k postgresql.status # Oczekiwany wynik: 1 ``` -### 6.2 Test lokalnie na NORDABIZ-01 +### 6.2 Test lokalnie na inpi-vps-waw01 ```bash -# Na serwerze NORDABIZ-01 +# Na serwerze inpi-vps-waw01 zabbix_agent2 -t agent.ping zabbix_agent2 -t nordabiznes.health zabbix_agent2 -t nordabiznes.service_status @@ -384,7 +383,7 @@ Uzycie: `ssl.days_until_expiry[nordabiznes.pl]` 4. **Graph: Memory Usage** - nordabiznes.memory_mb (trend) 5. **Graph: DB Connections** - postgresql.connections (trend) 6. **Counter: Chat Messages Today** - nordabiznes.chat_messages_today -7. **Top Hosts: Active Problems** - filtry dla NORDABIZ-01 +7. **Top Hosts: Active Problems** - filtry dla inpi-vps-waw01 --- diff --git a/mockups/messages_chat_view.html b/mockups/messages_chat_view.html new file mode 100644 index 0000000..19850fc --- /dev/null +++ b/mockups/messages_chat_view.html @@ -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) + + + + + +
+ + +
+ +
+ + +
+
+

Wiadomości

+ +
+ +
+ + +
+
MK
+
+
+ Magdalena Kloska + 11:54 +
+
+ witam, weszlam w skladki i mam pytanie... + 2 +
+
+
+ + +
+
+ +
+
+
+ Modul skladek7 os. + 24.03 +
+
+ Ty: Jest gotowa taka funkcjonalnosc dla roli kierownika... +
+
+
+ + +
+
AW
+
+
+ Artur Wiertel + 20.03 +
+
+ fajnie to wyglada. +
+
+
+ + +
+
RW
+
+
+ Roman Wiercinski + 18.03 +
+
+ Ty: Dzien dobry, przesylam podsumowanie... +
+
+
+ + +
+
LG
+
+
+ Leszek Glaza + 15.03 +
+
+ Ty: Lista firm z kontaktami gotowa. +
+
+
+ +
+
+ + +
+ +
+
+
MK
+
+

Magdalena Kloska

+
Kierownik Biura · Izba Norda Biznes
+
+
+
+ + +
+
+ +
+ +
19 marca 2026
+ + +
+
+
Skladki i NIP-y nowych czlonkow
+ 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 +
+ 19:49 + +
+
+
+ +
+
+
Kalendarz — wydarzenia zewnetrzne
+ Pani Magdo, dodalismy przed chwila tez informacje o wydarzeniach zewnetrznych. One w kalendarzu sa widoczne... +
+ 12:06 + +
+
+
+ +
+
+ z teog co widze na szybko i odnotowalam to firm ted jest w3pisana dwa razy ale nip ten sam tutaj... +
13:30
+
+
+ +
+
+ Ok, pani Magdo, ogarne te tematy, jak tylko wroce do biura. +
+ 13:54 + +
+
+
+ +
25 marca 2026
+ +
+
+ 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 +
11:47
+
+
+ +
+
+ witam, weszlam w skladki i mam pytanie jesli ktos np zaplaci zaleglosc lub kolejna rate jaqk to zmienic w systemie ? +
11:54
+
+
+ +
27 marca 2026
+ +
+
+ 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. +
+ 11:07 + +
+
+
+ +
+ + +
+
+ +
+ + +
+
+
+ +
+
+ +
MOCKUP — widok konwersacyjny
+ + + + diff --git a/reports/maturity_report_pilot.md b/reports/maturity_report_pilot.md index 0435b05..0df7635 100644 --- a/reports/maturity_report_pilot.md +++ b/reports/maturity_report_pilot.md @@ -186,7 +186,7 @@ blog, contact_form, detailed_services, analytics, live_chat --- **Report generated by Norda Biznes Digital Maturity Platform** -*Data source: PostgreSQL database on NORDABIZ-01 (10.22.68.249)* +*Data source: PostgreSQL database on NORDABIZ-01 (57.128.200.27)* *Phase: ETAP 1 - Foundation* --- diff --git a/scripts/.ovh_vps_monitor_state.json b/scripts/.ovh_vps_monitor_state.json new file mode 100644 index 0000000..05bf2c4 --- /dev/null +++ b/scripts/.ovh_vps_monitor_state.json @@ -0,0 +1,6 @@ +{ + "vps-2025-model1": "available", + "vps-2025-model2": "available", + "vps-2025-model3": "available", + "vps-2025-model4": "available" +} \ No newline at end of file diff --git a/scripts/audit_company_data.py b/scripts/audit_company_data.py new file mode 100644 index 0000000..0d77f53 --- /dev/null +++ b/scripts/audit_company_data.py @@ -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}') diff --git a/scripts/create_presentation.js b/scripts/create_presentation.js new file mode 100644 index 0000000..71f8ea9 --- /dev/null +++ b/scripts/create_presentation.js @@ -0,0 +1,1112 @@ +const pptxgen = require("pptxgenjs"); +const React = require("react"); +const ReactDOMServer = require("react-dom/server"); +const sharp = require("sharp"); +const { + FaBuilding, FaRobot, FaComments, FaCalendarAlt, FaAtom, FaShieldAlt, + FaRocket, FaQuestion, FaSearch, FaUsers, FaBullhorn, FaLock, + FaChartLine, FaHandshake, FaIndustry, FaGlobe, FaEnvelope, FaBell, + FaMobileAlt, FaNewspaper, FaUserShield, FaMoneyBillWave, FaStar, + FaCheckCircle, FaLightbulb, FaMapMarkedAlt, FaFileAlt +} = require("react-icons/fa"); + +function renderIconSvg(IconComponent, color = "#000000", size = 256) { + return ReactDOMServer.renderToStaticMarkup( + React.createElement(IconComponent, { color, size: String(size) }) + ); +} + +async function iconToBase64Png(IconComponent, color, size = 256) { + const svg = renderIconSvg(IconComponent, color, size); + const pngBuffer = await sharp(Buffer.from(svg)).png().toBuffer(); + return "image/png;base64," + pngBuffer.toString("base64"); +} + +// Color palette +const C = { + darkBg: "0F172A", + navyBg: "1E293B", + primary: "0891B2", + primaryLight: "22D3EE", + accent: "06B6D4", + lightBg: "F1F5F9", + white: "FFFFFF", + cardBg: "FFFFFF", + textDark: "1E293B", + textMuted: "64748B", + textLight: "CBD5E1", + green: "10B981", + amber: "F59E0B", + red: "EF4444", + purple: "8B5CF6", + indigo: "6366F1", + tealDark: "065F46", +}; + +const FONT_TITLE = "Georgia"; +const FONT_BODY = "Calibri"; + +const makeShadow = () => ({ type: "outer", blur: 6, offset: 2, angle: 135, color: "000000", opacity: 0.12 }); + +function addSlideNumber(slide, num, total) { + slide.addText(`${num} / ${total}`, { + x: 8.8, y: 5.2, w: 1, h: 0.3, + fontSize: 9, fontFace: FONT_BODY, color: C.textLight, align: "right" + }); +} + +function addSectionHeader(slide, icon, title, subtitle, num, total) { + // Dark left panel + slide.addShape(slide._slideLayout ? "rect" : "rect", {}); + // We'll just build the full slide manually +} + +async function createPresentation() { + const pres = new pptxgen(); + pres.layout = "LAYOUT_16x9"; + pres.author = "Maciej Pienczyn"; + pres.title = "NordaBiznes.pl — Prezentacja portalu"; + + const TOTAL = 14; + + // Pre-render icons + const icons = {}; + const iconMap = { + building: [FaBuilding, C.white], + robot: [FaRobot, C.white], + comments: [FaComments, C.white], + calendar: [FaCalendarAlt, C.white], + atom: [FaAtom, C.white], + shield: [FaShieldAlt, C.white], + rocket: [FaRocket, C.white], + question: [FaQuestion, C.white], + search: [FaSearch, C.primary], + users: [FaUsers, C.primary], + bullhorn: [FaBullhorn, C.primary], + lock: [FaLock, C.primary], + chart: [FaChartLine, C.primary], + handshake: [FaHandshake, C.primary], + industry: [FaIndustry, C.primary], + globe: [FaGlobe, C.primary], + envelope: [FaEnvelope, C.primary], + bell: [FaBell, C.primary], + mobile: [FaMobileAlt, C.primary], + newspaper: [FaNewspaper, C.primary], + userShield: [FaUserShield, C.primary], + money: [FaMoneyBillWave, C.primary], + star: [FaStar, C.amber], + check: [FaCheckCircle, C.green], + lightbulb: [FaLightbulb, C.amber], + map: [FaMapMarkedAlt, C.primary], + file: [FaFileAlt, C.primary], + // Dark bg versions + buildingDark: [FaBuilding, C.primaryLight], + robotDark: [FaRobot, C.primaryLight], + commentsDark: [FaComments, C.primaryLight], + calendarDark: [FaCalendarAlt, C.primaryLight], + atomDark: [FaAtom, C.primaryLight], + industryDark: [FaIndustry, C.primaryLight], + shieldDark: [FaShieldAlt, C.primaryLight], + rocketDark: [FaRocket, C.primaryLight], + checkWhite: [FaCheckCircle, C.white], + globeWhite: [FaGlobe, C.white], + searchWhite: [FaSearch, C.white], + lightbulbWhite: [FaLightbulb, C.white], + handshakeWhite: [FaHandshake, C.white], + }; + + for (const [key, [comp, color]] of Object.entries(iconMap)) { + icons[key] = await iconToBase64Png(comp, `#${color}`, 256); + } + + // Helper: colored icon circle + function addIconCircle(slide, iconKey, x, y, size, bgColor) { + slide.addShape(pres.shapes.OVAL, { + x, y, w: size, h: size, + fill: { color: bgColor } + }); + const pad = size * 0.25; + slide.addImage({ + data: icons[iconKey], + x: x + pad, y: y + pad, + w: size - pad * 2, h: size - pad * 2 + }); + } + + // Helper: content card with left accent + function addCard(slide, x, y, w, h, accentColor) { + slide.addShape(pres.shapes.RECTANGLE, { + x, y, w, h, + fill: { color: C.white }, + shadow: makeShadow() + }); + slide.addShape(pres.shapes.RECTANGLE, { + x, y, w: 0.06, h, + fill: { color: accentColor || C.primary } + }); + } + + // Logo paths + const nordaLogoPath = "/Users/maciejpi/claude/projects/active/nordabiz/static/img/logo-email.png"; + const inpiLogoPath = "/tmp/inpi-logo.png"; + + // ============================================= + // SLIDE 1: TYTUŁ + // ============================================= + let s = pres.addSlide(); + s.background = { color: C.darkBg }; + + // Accent line top + s.addShape(pres.shapes.RECTANGLE, { + x: 0, y: 0, w: 10, h: 0.06, + fill: { color: C.primary } + }); + + // Norda compass logo — right side, large + s.addImage({ + path: nordaLogoPath, + x: 7.5, y: 1.2, w: 2.0, h: 2.0, + transparency: 15 + }); + + s.addText("NordaBiznes.pl", { + x: 0.8, y: 1.2, w: 6.5, h: 1.2, + fontSize: 48, fontFace: FONT_TITLE, color: C.white, bold: true, margin: 0 + }); + s.addText("Platforma członków Izby NORDA", { + x: 0.8, y: 2.3, w: 6.5, h: 0.6, + fontSize: 22, fontFace: FONT_BODY, color: C.primaryLight, margin: 0 + }); + + s.addShape(pres.shapes.RECTANGLE, { + x: 0.8, y: 3.2, w: 2, h: 0.04, + fill: { color: C.primary } + }); + + s.addText("Prezentacja dla członków Izby", { + x: 0.8, y: 3.6, w: 5, h: 0.4, + fontSize: 14, fontFace: FONT_BODY, color: C.textLight, margin: 0 + }); + s.addText("9 kwietnia 2026 | Urząd Miasta Wejherowo", { + x: 0.8, y: 4.0, w: 5, h: 0.4, + fontSize: 14, fontFace: FONT_BODY, color: C.textLight, margin: 0 + }); + s.addText("Maciej Pienczyn | InPi sp. z o.o.", { + x: 0.8, y: 4.6, w: 5, h: 0.4, + fontSize: 14, fontFace: FONT_BODY, color: C.textLight, margin: 0 + }); + + // ============================================= + // SLIDE 2: AGENDA + // ============================================= + s = pres.addSlide(); + s.background = { color: C.lightBg }; + + s.addText("Co dziś pokażemy", { + x: 0.8, y: 0.4, w: 8.4, h: 0.7, + fontSize: 32, fontFace: FONT_TITLE, color: C.textDark, bold: true, margin: 0 + }); + s.addShape(pres.shapes.RECTANGLE, { + x: 0.8, y: 1.05, w: 1.2, h: 0.04, fill: { color: C.primary } + }); + + const agendaItems = [ + ["building", "Twoja firma w Izbie", "Profil, wizytówka, wyszukiwanie"], + ["robot", "NordaGPT", "Asystent AI, który zna wszystkich"], + ["comments", "Komunikacja", "Wiadomości, forum, ogłoszenia B2B"], + ["calendar", "Kalendarz", "Wydarzenia, zapisy, przypomnienia"], + ["atom", "PEJ i Kaszubia", "Elektrownia jądrowa, inwestycje"], + ["shield", "Prywatność", "Kto widzi co, bezpieczeństwo"], + ]; + + const startY = 1.4; + for (let i = 0; i < agendaItems.length; i++) { + const [icon, title, desc] = agendaItems[i]; + const row = i; + const yPos = startY + row * 0.65; + + addIconCircle(s, icon, 0.8, yPos + 0.05, 0.45, C.primary); + + s.addText(title, { + x: 1.45, y: yPos, w: 3, h: 0.35, + fontSize: 15, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0, valign: "middle" + }); + s.addText(desc, { + x: 1.45, y: yPos + 0.3, w: 5, h: 0.3, + fontSize: 12, fontFace: FONT_BODY, color: C.textMuted, margin: 0 + }); + } + + // Right side - stats + addCard(s, 6.5, 1.4, 3, 3.5, C.primary); + s.addText("Portal w liczbach", { + x: 6.8, y: 1.6, w: 2.5, h: 0.35, + fontSize: 14, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0 + }); + + const stats = [ + ["150+", "firm członkowskich"], + ["38", "uczestników spotkania"], + ["57", "wydań od startu"], + ["17", "kategorii branżowych"], + ["6", "poziomów dostępu"], + ]; + for (let i = 0; i < stats.length; i++) { + s.addText(stats[i][0], { + x: 6.8, y: 2.15 + i * 0.52, w: 0.8, h: 0.4, + fontSize: 20, fontFace: FONT_TITLE, color: C.primary, bold: true, margin: 0, valign: "middle" + }); + s.addText(stats[i][1], { + x: 7.7, y: 2.15 + i * 0.52, w: 1.6, h: 0.4, + fontSize: 12, fontFace: FONT_BODY, color: C.textMuted, margin: 0, valign: "middle" + }); + } + + addSlideNumber(s, 2, TOTAL); + + // ============================================= + // SLIDE 3: PO CO POWSTAŁ PORTAL + // ============================================= + s = pres.addSlide(); + s.background = { color: C.darkBg }; + + s.addShape(pres.shapes.RECTANGLE, { + x: 0, y: 0, w: 10, h: 0.06, fill: { color: C.primary } + }); + + s.addText("Po co powstał NordaBiznes?", { + x: 0.8, y: 0.4, w: 8.4, h: 0.7, + fontSize: 32, fontFace: FONT_TITLE, color: C.white, bold: true, margin: 0 + }); + + const whyItems = [ + [icons.handshakeWhite, "Łączyć firmy", "Znajdź partnera biznesowego w Izbie\nbez szukania numeru, maila czy pośredników"], + [icons.searchWhite, "Pokazywać", "Twoja firma widoczna dla wszystkich\nczłonków — wizytówka, usługi, dane kontaktowe"], + [icons.globeWhite, "Informować", "Aktualności, wydarzenia, PEJ,\ninwestycje na Kaszubach — wszystko w jednym"], + [icons.lightbulbWhite, "Wspierać AI", "NordaGPT zna firmy w Izbie\ni pomaga znaleźć właściwego partnera"], + ]; + + for (let i = 0; i < whyItems.length; i++) { + const col = i % 2; + const row = Math.floor(i / 2); + const x = 0.8 + col * 4.5; + const y = 1.5 + row * 1.8; + + s.addShape(pres.shapes.RECTANGLE, { + x, y, w: 4, h: 1.5, + fill: { color: C.navyBg }, + shadow: makeShadow() + }); + s.addShape(pres.shapes.RECTANGLE, { + x, y, w: 0.06, h: 1.5, + fill: { color: C.primary } + }); + + s.addImage({ data: whyItems[i][0], x: x + 0.3, y: y + 0.25, w: 0.4, h: 0.4 }); + s.addText(whyItems[i][1], { + x: x + 0.9, y: y + 0.15, w: 2.8, h: 0.4, + fontSize: 16, fontFace: FONT_BODY, color: C.white, bold: true, margin: 0 + }); + s.addText(whyItems[i][2], { + x: x + 0.9, y: y + 0.55, w: 2.8, h: 0.8, + fontSize: 12, fontFace: FONT_BODY, color: C.textLight, margin: 0 + }); + } + + addSlideNumber(s, 3, TOTAL); + + // ============================================= + // SLIDE 4: PROFIL FIRMY + // ============================================= + s = pres.addSlide(); + s.background = { color: C.lightBg }; + + s.addText("Twoja firma na portalu", { + x: 0.8, y: 0.4, w: 8.4, h: 0.7, + fontSize: 32, fontFace: FONT_TITLE, color: C.textDark, bold: true, margin: 0 + }); + s.addShape(pres.shapes.RECTANGLE, { + x: 0.8, y: 1.05, w: 1.2, h: 0.04, fill: { color: C.primary } + }); + + // Left column - what's on the profile + addCard(s, 0.8, 1.4, 4.2, 3.6, C.primary); + s.addText("Co jest na profilu?", { + x: 1.1, y: 1.55, w: 3.5, h: 0.35, + fontSize: 16, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0 + }); + + const profileItems = [ + "Opis firmy, usługi, technologie", + "Dane kontaktowe (telefon, email, adres)", + "Strony internetowe (do 5)", + "Social media (FB, IG, LinkedIn, YT)", + "Godziny otwarcia (status na żywo)", + "Dane z rejestrów: KRS / CEIDG", + "Rekomendacje od innych firm", + "Zarząd, wspólnicy, kapitał", + ]; + + s.addText(profileItems.map((item, i) => ({ + text: item, + options: { bullet: true, breakLine: i < profileItems.length - 1, fontSize: 12, color: C.textDark } + })), { + x: 1.1, y: 2.0, w: 3.6, h: 2.8, + fontFace: FONT_BODY, paraSpaceAfter: 4, margin: 0 + }); + + // Right column - what you can do + addCard(s, 5.4, 1.4, 4.2, 3.6, C.green); + s.addText("Co możesz zrobić?", { + x: 5.7, y: 1.55, w: 3.5, h: 0.35, + fontSize: 16, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0 + }); + + const actionItems = [ + "Edytuj profil swojej firmy", + "Zarządzaj zespołem (role, uprawnienia)", + "Kontroluj co jest widoczne publicznie", + "Zaproś pracowników do portalu", + "Sprawdź wskaźnik kompletności", + "Pobierz dane z urzędowych rejestrów", + "Zbieraj AI o swojej firmie z internetu", + "Przeglądaj statystyki odwiedzin", + ]; + + s.addText(actionItems.map((item, i) => ({ + text: item, + options: { bullet: true, breakLine: i < actionItems.length - 1, fontSize: 12, color: C.textDark } + })), { + x: 5.7, y: 2.0, w: 3.6, h: 2.8, + fontFace: FONT_BODY, paraSpaceAfter: 4, margin: 0 + }); + + addSlideNumber(s, 4, TOTAL); + + // ============================================= + // SLIDE 5: WYSZUKIWARKA I KATEGORIE + // ============================================= + s = pres.addSlide(); + s.background = { color: C.lightBg }; + + s.addText("Szukaj i znajdź", { + x: 0.8, y: 0.4, w: 8.4, h: 0.7, + fontSize: 32, fontFace: FONT_TITLE, color: C.textDark, bold: true, margin: 0 + }); + s.addShape(pres.shapes.RECTANGLE, { + x: 0.8, y: 1.05, w: 1.2, h: 0.04, fill: { color: C.primary } + }); + + s.addText("Wyszukiwarka rozumie literówki, synonimy i skróty", { + x: 0.8, y: 1.3, w: 8, h: 0.4, + fontSize: 14, fontFace: FONT_BODY, color: C.textMuted, margin: 0 + }); + + // Category cards + const categories = [ + ["IT i technologie", "Software, hosting,\nautomatyzacja", C.indigo], + ["Budownictwo", "Projekty, wykonawstwo,\ninstalacje", C.amber], + ["Usługi", "Finanse, prawo,\nkonsulting, HR", C.green], + ["Produkcja", "Przemysł, CNC,\nmetalurgia", C.red], + ]; + + for (let i = 0; i < categories.length; i++) { + const x = 0.8 + i * 2.25; + const [name, desc, color] = categories[i]; + + s.addShape(pres.shapes.RECTANGLE, { + x, y: 1.9, w: 2.0, h: 1.4, + fill: { color: C.white }, shadow: makeShadow() + }); + s.addShape(pres.shapes.RECTANGLE, { + x, y: 1.9, w: 2.0, h: 0.06, + fill: { color } + }); + s.addText(name, { + x: x + 0.15, y: 2.1, w: 1.7, h: 0.35, + fontSize: 13, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0 + }); + s.addText(desc, { + x: x + 0.15, y: 2.5, w: 1.7, h: 0.6, + fontSize: 11, fontFace: FONT_BODY, color: C.textMuted, margin: 0 + }); + } + + // Bottom - search features + addCard(s, 0.8, 3.6, 8.4, 1.5, C.primary); + s.addText("Jak szukać?", { + x: 1.1, y: 3.75, w: 3, h: 0.35, + fontSize: 15, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0 + }); + + const searchFeatures = [ + ["Po nazwie firmy", "Wpisz fragment nazwy — znajdzie nawet z literówką"], + ["Po NIP lub REGON", "Dokładne wyszukiwanie po numerach rejestrowych"], + ["Po kategorii", "Filtruj po branży i podkategorii"], + ["Przez NordaGPT", "Opisz czego szukasz swoimi słowami"], + ]; + + for (let i = 0; i < searchFeatures.length; i++) { + const col = i % 2; + const row = Math.floor(i / 2); + const x = 1.1 + col * 4.1; + const y = 4.15 + row * 0.45; + + s.addImage({ data: icons.check, x, y: y + 0.02, w: 0.2, h: 0.2 }); + s.addText([ + { text: searchFeatures[i][0] + " ", options: { bold: true, fontSize: 11 } }, + { text: searchFeatures[i][1], options: { fontSize: 11, color: C.textMuted } } + ], { + x: x + 0.3, y, w: 3.5, h: 0.35, + fontFace: FONT_BODY, color: C.textDark, margin: 0, valign: "middle" + }); + } + + addSlideNumber(s, 5, TOTAL); + + // ============================================= + // SLIDE 6: NORDAGPT + // ============================================= + s = pres.addSlide(); + s.background = { color: C.darkBg }; + + s.addShape(pres.shapes.RECTANGLE, { + x: 0, y: 0, w: 10, h: 0.06, fill: { color: C.primary } + }); + + addIconCircle(s, "robot", 0.8, 0.35, 0.55, C.primary); + s.addText("NordaGPT — asystent AI Izby", { + x: 1.5, y: 0.35, w: 7, h: 0.6, + fontSize: 30, fontFace: FONT_TITLE, color: C.white, bold: true, margin: 0, valign: "middle" + }); + + // What NordaGPT knows + s.addText("Co wie NordaGPT?", { + x: 0.8, y: 1.2, w: 4, h: 0.4, + fontSize: 16, fontFace: FONT_BODY, color: C.primaryLight, bold: true, margin: 0 + }); + + const gptKnows = [ + "Wszystkie firmy członkowskie — usługi, dane", + "Kto jest kim w Zarządzie i Radzie Izby", + "Kalendarz wydarzeń i aktualności", + "Projekt elektrowni jądrowej (PEJ)", + "Inwestycje na Kaszubach (ZOPK)", + "Ogłoszenia B2B i tematy na forum", + ]; + + s.addText(gptKnows.map((item, i) => ({ + text: item, + options: { bullet: true, breakLine: i < gptKnows.length - 1, fontSize: 13, color: C.white } + })), { + x: 0.8, y: 1.7, w: 4.2, h: 2.5, + fontFace: FONT_BODY, paraSpaceAfter: 5, margin: 0 + }); + + // Right - example questions + s.addText("Przykładowe pytania:", { + x: 5.4, y: 1.2, w: 4, h: 0.4, + fontSize: 16, fontFace: FONT_BODY, color: C.primaryLight, bold: true, margin: 0 + }); + + const examples = [ + '"Szukam firmy od instalacji elektrycznych"', + '"Kto w Izbie zajmuje się budownictwem?"', + '"Jakie są najbliższe wydarzenia?"', + '"Co nowego w sprawie elektrowni jądrowej?"', + '"Firma od IT z doświadczeniem w offshore"', + ]; + + for (let i = 0; i < examples.length; i++) { + s.addShape(pres.shapes.RECTANGLE, { + x: 5.4, y: 1.7 + i * 0.5, w: 4.2, h: 0.4, + fill: { color: C.navyBg } + }); + s.addText(examples[i], { + x: 5.6, y: 1.7 + i * 0.5, w: 3.8, h: 0.4, + fontSize: 11, fontFace: FONT_BODY, color: C.textLight, italic: true, margin: 0, valign: "middle" + }); + } + + // Bottom note about AI + s.addShape(pres.shapes.RECTANGLE, { + x: 0.8, y: 4.5, w: 8.4, h: 0.7, + fill: { color: "172554" } + }); + s.addText([ + { text: "Odpowiedzi na żywo ", options: { fontSize: 13, color: C.white, bold: true } }, + { text: " | ", options: { fontSize: 13, color: C.primaryLight } }, + { text: "Propozycje kolejnych pytań ", options: { fontSize: 13, color: C.white, bold: true } }, + { text: " | ", options: { fontSize: 13, color: C.primaryLight } }, + { text: "Klikalne linki do firm", options: { fontSize: 13, color: C.white, bold: true } }, + ], { + x: 1.0, y: 4.5, w: 8, h: 0.7, + fontFace: FONT_BODY, align: "center", valign: "middle", margin: 0 + }); + + addSlideNumber(s, 6, TOTAL); + + // ============================================= + // SLIDE 7: KOMUNIKACJA + // ============================================= + s = pres.addSlide(); + s.background = { color: C.lightBg }; + + s.addText("Komunikacja w portalu", { + x: 0.8, y: 0.4, w: 8.4, h: 0.7, + fontSize: 32, fontFace: FONT_TITLE, color: C.textDark, bold: true, margin: 0 + }); + s.addShape(pres.shapes.RECTANGLE, { + x: 0.8, y: 1.05, w: 1.2, h: 0.04, fill: { color: C.primary } + }); + + // Three columns + const commCols = [ + { + title: "Wiadomości prywatne", + color: C.primary, + items: [ + "Napisz do każdego członka Izby", + "Bąbelki jak w WhatsApp / Teams", + "Formatowanie tekstu, linki", + "Załączniki: PDF, zdjęcia", + "Potwierdzenie odczytania", + "Wiadomości grupowe", + ] + }, + { + title: "Forum dyskusyjne", + color: C.green, + items: [ + "Tematy z kategoriami", + "Odpowiadaj, reaguj emoji", + "Śledź tematy (powiadomienia)", + "Wstawiaj zdjęcia i linki", + "Oznaczaj @użytkowników", + "Najlepsza odpowiedź", + ] + }, + { + title: "Tablica B2B", + color: C.amber, + items: [ + "Ogłoszenia z ofertami usług", + "\"Jestem zainteresowany\"", + "Publiczne pytania / odpowiedzi", + "Kto widział ogłoszenie?", + "Kategorie: Partnerstwo, Okazja...", + "Wysyłaj wiadomość z ogłoszenia", + ] + }, + ]; + + for (let c = 0; c < commCols.length; c++) { + const x = 0.8 + c * 3.1; + const col = commCols[c]; + + addCard(s, x, 1.35, 2.8, 3.7, col.color); + s.addText(col.title, { + x: x + 0.25, y: 1.5, w: 2.3, h: 0.35, + fontSize: 14, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0 + }); + + s.addText(col.items.map((item, i) => ({ + text: item, + options: { bullet: true, breakLine: i < col.items.length - 1, fontSize: 11, color: C.textDark } + })), { + x: x + 0.25, y: 2.0, w: 2.3, h: 2.8, + fontFace: FONT_BODY, paraSpaceAfter: 4, margin: 0 + }); + } + + addSlideNumber(s, 7, TOTAL); + + // ============================================= + // SLIDE 8: KALENDARZ + // ============================================= + s = pres.addSlide(); + s.background = { color: C.lightBg }; + + s.addText("Kalendarz wydarzeń", { + x: 0.8, y: 0.4, w: 8.4, h: 0.7, + fontSize: 32, fontFace: FONT_TITLE, color: C.textDark, bold: true, margin: 0 + }); + s.addShape(pres.shapes.RECTANGLE, { + x: 0.8, y: 1.05, w: 1.2, h: 0.04, fill: { color: C.primary } + }); + + // Features grid 2x3 + const calFeatures = [ + [icons.calendar, "Widok miesięczny", "Przejrzysta siatka z kolorowymi\noznaczeniami typów wydarzeń"], + [icons.check, "Zapis jednym kliknięciem", "Zielony przycisk — zapisz się\nlub zrezygnuj w każdej chwili"], + [icons.users, "Lista uczestników", "Zobacz kto idzie — klikalne\nplakietki z linkami do profili"], + [icons.bell, "Przypomnienia", "Email dzień przed wydarzeniem,\npowiadomienie w portalu"], + [icons.mobile, "Synchronizacja", "Eksport do iPhone (iCal)\ni Google Calendar"], + [icons.map, "Lokalizacja", "Adresy prowadzą do Google Maps,\nwszystko klikalne"], + ]; + + for (let i = 0; i < calFeatures.length; i++) { + const col = i % 3; + const row = Math.floor(i / 3); + const x = 0.8 + col * 3.1; + const y = 1.4 + row * 1.9; + + addCard(s, x, y, 2.8, 1.6, C.primary); + s.addImage({ data: calFeatures[i][0], x: x + 0.25, y: y + 0.2, w: 0.35, h: 0.35 }); + s.addText(calFeatures[i][1], { + x: x + 0.25, y: y + 0.65, w: 2.3, h: 0.3, + fontSize: 13, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0 + }); + s.addText(calFeatures[i][2], { + x: x + 0.25, y: y + 0.95, w: 2.3, h: 0.5, + fontSize: 11, fontFace: FONT_BODY, color: C.textMuted, margin: 0 + }); + } + + addSlideNumber(s, 8, TOTAL); + + // ============================================= + // SLIDE 9: PEJ + // ============================================= + s = pres.addSlide(); + s.background = { color: C.darkBg }; + + s.addShape(pres.shapes.RECTANGLE, { + x: 0, y: 0, w: 10, h: 0.06, fill: { color: C.amber } + }); + + addIconCircle(s, "atom", 0.8, 0.35, 0.55, C.amber); + s.addText("PEJ — Elektrownia Jądrowa", { + x: 1.5, y: 0.35, w: 7, h: 0.6, + fontSize: 30, fontFace: FONT_TITLE, color: C.white, bold: true, margin: 0, valign: "middle" + }); + + s.addText("Dedykowana przestrzeń dla firm zainteresowanych największą inwestycją w regionie", { + x: 0.8, y: 1.1, w: 8.4, h: 0.4, + fontSize: 14, fontFace: FONT_BODY, color: C.textLight, margin: 0 + }); + + // Two columns + // Left - what's there + s.addShape(pres.shapes.RECTANGLE, { + x: 0.8, y: 1.7, w: 4.2, h: 3.2, + fill: { color: C.navyBg }, shadow: makeShadow() + }); + s.addShape(pres.shapes.RECTANGLE, { + x: 0.8, y: 1.7, w: 0.06, h: 3.2, fill: { color: C.amber } + }); + + s.addText("Co znajdziesz?", { + x: 1.1, y: 1.85, w: 3.5, h: 0.35, + fontSize: 15, fontFace: FONT_BODY, color: C.amber, bold: true, margin: 0 + }); + + const pejItems = [ + "Aktualności o elektrowni jądrowej (66+ artykułów)", + "Local Content — lista firm Izby dopasowanych do projektu", + "Wskaźnik dopasowania firmy do przetargów PEJ", + "Oś czasu kamieni milowych budowy", + "Kontakty: PEJ, Bechtel, PAA, Ministerstwo", + "Link do platformy zakupowej i rejestracji dostawców", + ]; + + s.addText(pejItems.map((item, i) => ({ + text: item, + options: { bullet: true, breakLine: i < pejItems.length - 1, fontSize: 12, color: C.white } + })), { + x: 1.1, y: 2.3, w: 3.6, h: 2.4, + fontFace: FONT_BODY, paraSpaceAfter: 4, margin: 0 + }); + + // Right - Local Content + s.addShape(pres.shapes.RECTANGLE, { + x: 5.4, y: 1.7, w: 4.2, h: 3.2, + fill: { color: C.navyBg }, shadow: makeShadow() + }); + s.addShape(pres.shapes.RECTANGLE, { + x: 5.4, y: 1.7, w: 0.06, h: 3.2, fill: { color: C.green } + }); + + s.addText("Local Content — Twoja szansa", { + x: 5.7, y: 1.85, w: 3.5, h: 0.35, + fontSize: 15, fontFace: FONT_BODY, color: C.green, bold: true, margin: 0 + }); + + const lcItems = [ + "System dopasowuje Twoją firmę do projektu PEJ", + "Widoczne: typ współpracy, branża, opis", + "Filtruj po: kategorii, typie powiązania", + "Potencjalny dostawca / partner / beneficjent", + "Im pełniejszy profil, tym lepsze dopasowanie", + "NordaGPT odpowiada na pytania o PEJ", + ]; + + s.addText(lcItems.map((item, i) => ({ + text: item, + options: { bullet: true, breakLine: i < lcItems.length - 1, fontSize: 12, color: C.white } + })), { + x: 5.7, y: 2.3, w: 3.6, h: 2.4, + fontFace: FONT_BODY, paraSpaceAfter: 4, margin: 0 + }); + + addSlideNumber(s, 9, TOTAL); + + // ============================================= + // SLIDE 10: KASZUBIA / ZOPK + // ============================================= + s = pres.addSlide(); + s.background = { color: C.lightBg }; + + s.addText("Kaszubia — Inwestycje regionalne", { + x: 0.8, y: 0.4, w: 8.4, h: 0.7, + fontSize: 30, fontFace: FONT_TITLE, color: C.textDark, bold: true, margin: 0 + }); + s.addText("Zielony Okręg Przemysłowy Kaszubia — monitoring szans biznesowych", { + x: 0.8, y: 1.0, w: 8.4, h: 0.4, + fontSize: 14, fontFace: FONT_BODY, color: C.textMuted, margin: 0 + }); + + // Project cards - use darker shades for white text readability + const projects = [ + ["Energia", "Elektrownia jądrowa,\noffshore, OZE", "B45309"], + ["Infrastruktura", "Drogi, kolej,\nport Gdynia", C.indigo], + ["Przemysł", "Kongsberg, CNC,\nmetalurgia", C.red], + ["Turystyka", "Żarnowiecki Ring,\nEnergy Velo", "047857"], + ]; + + for (let i = 0; i < projects.length; i++) { + const x = 0.8 + i * 2.25; + const [name, desc, color] = projects[i]; + + s.addShape(pres.shapes.RECTANGLE, { + x, y: 1.6, w: 2.0, h: 1.2, + fill: { color }, shadow: makeShadow() + }); + s.addText(name, { + x: x + 0.15, y: 1.7, w: 1.7, h: 0.35, + fontSize: 15, fontFace: FONT_BODY, color: C.white, bold: true, margin: 0 + }); + s.addText(desc, { + x: x + 0.15, y: 2.1, w: 1.7, h: 0.6, + fontSize: 11, fontFace: FONT_BODY, color: C.white, margin: 0 + }); + } + + // Bottom - what's tracked + addCard(s, 0.8, 3.1, 8.4, 2.1, C.primary); + s.addText("Co monitorujemy?", { + x: 1.1, y: 3.25, w: 4, h: 0.35, + fontSize: 15, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0 + }); + + const zopkItems = [ + [icons.newspaper, "Aktualności prasowe", "Automatyczne zbieranie i filtrowanie wiadomości przez AI"], + [icons.chart, "Kamienie milowe", "Oś czasu z postępem kluczowych projektów w regionie"], + [icons.handshake, "Dopasowanie firm", "System wskazuje które firmy Izby pasują do przetargów"], + [icons.lightbulb, "Baza wiedzy AI", "NordaGPT odpowiada na pytania o inwestycje kaszubskie"], + ]; + + for (let i = 0; i < zopkItems.length; i++) { + const col = i % 2; + const row = Math.floor(i / 2); + const x = 1.1 + col * 4.1; + const y = 3.7 + row * 0.65; + + s.addImage({ data: zopkItems[i][0], x, y: y + 0.05, w: 0.28, h: 0.28 }); + s.addText([ + { text: zopkItems[i][1] + " ", options: { bold: true, fontSize: 12 } }, + { text: zopkItems[i][2], options: { fontSize: 11, color: C.textMuted } } + ], { + x: x + 0.4, y, w: 3.5, h: 0.5, + fontFace: FONT_BODY, color: C.textDark, margin: 0, valign: "middle" + }); + } + + addSlideNumber(s, 10, TOTAL); + + // ============================================= + // SLIDE 11: POWIADOMIENIA + // ============================================= + s = pres.addSlide(); + s.background = { color: C.lightBg }; + + s.addText("Powiadomienia i aplikacja mobilna", { + x: 0.8, y: 0.4, w: 8.4, h: 0.7, + fontSize: 30, fontFace: FONT_TITLE, color: C.textDark, bold: true, margin: 0 + }); + s.addShape(pres.shapes.RECTANGLE, { + x: 0.8, y: 1.05, w: 1.2, h: 0.04, fill: { color: C.primary } + }); + + // Notification channels + const notifCols = [ + { + title: "W portalu", + icon: "bell", + color: C.primary, + items: [ + "Dzwoneczek z licznikiem", + "Nowe wiadomości", + "Odpowiedzi na forum", + "Zapis na wydarzenie", + "Badge \"Nowe\" na treściach", + ] + }, + { + title: "Email", + icon: "envelope", + color: C.green, + items: [ + "Nowa wiadomość prywatna", + "Odpowiedź na śledzony temat", + "Przypomnienie o wydarzeniu", + "Wniosek członkowski", + "Link do rezygnacji", + ] + }, + { + title: "Telefon", + icon: "mobile", + color: C.purple, + items: [ + "Zainstaluj jak aplikację", + "Powiadomienia na iPhone i Android", + "Pełna funkcjonalność", + "Działa bez internetu", + "Ikona na pulpicie telefonu", + ] + }, + ]; + + for (let c = 0; c < notifCols.length; c++) { + const x = 0.8 + c * 3.1; + const col = notifCols[c]; + + addCard(s, x, 1.35, 2.8, 3.5, col.color); + addIconCircle(s, col.icon, x + 0.25, 1.5, 0.4, col.color); + s.addText(col.title, { + x: x + 0.8, y: 1.5, w: 1.7, h: 0.4, + fontSize: 15, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0, valign: "middle" + }); + + s.addText(col.items.map((item, i) => ({ + text: item, + options: { bullet: true, breakLine: i < col.items.length - 1, fontSize: 11, color: C.textDark } + })), { + x: x + 0.25, y: 2.1, w: 2.3, h: 2.5, + fontFace: FONT_BODY, paraSpaceAfter: 5, margin: 0 + }); + } + + addSlideNumber(s, 11, TOTAL); + + // ============================================= + // SLIDE 12: PRYWATNOŚĆ I BEZPIECZEŃSTWO + // ============================================= + s = pres.addSlide(); + s.background = { color: C.darkBg }; + + s.addShape(pres.shapes.RECTANGLE, { + x: 0, y: 0, w: 10, h: 0.06, fill: { color: C.green } + }); + + addIconCircle(s, "shield", 0.8, 0.35, 0.55, C.green); + s.addText("Prywatność i bezpieczeństwo", { + x: 1.5, y: 0.35, w: 7, h: 0.6, + fontSize: 30, fontFace: FONT_TITLE, color: C.white, bold: true, margin: 0, valign: "middle" + }); + + // Privacy features + const privacyItems = [ + ["Twoje dane pod kontrolą", "Decydujesz co jest widoczne — telefon, email,\nsekcje profilu. Ukrywasz jednym kliknięciem.", C.green], + ["6 poziomów dostępu", "Gość → Użytkownik → Członek → Pracownik →\nKierownik → Administrator. Każdy widzi tylko to, co powinien.", C.primary], + ["Tylko dla członków Izby", "NordaGPT, wiadomości, B2B, dane kontaktowe\n— dostępne wyłącznie po zalogowaniu.", C.amber], + ["Ochrona danych osobowych", "PESEL, numery kart, IBAN — automatycznie\nukrywane. Blokowanie niechcianych kontaktów.", C.purple], + ]; + + for (let i = 0; i < privacyItems.length; i++) { + const col = i % 2; + const row = Math.floor(i / 2); + const x = 0.8 + col * 4.5; + const y = 1.3 + row * 1.8; + + s.addShape(pres.shapes.RECTANGLE, { + x, y, w: 4.0, h: 1.5, + fill: { color: C.navyBg }, shadow: makeShadow() + }); + s.addShape(pres.shapes.RECTANGLE, { + x, y, w: 0.06, h: 1.5, + fill: { color: privacyItems[i][2] } + }); + + s.addText(privacyItems[i][0], { + x: x + 0.25, y: y + 0.15, w: 3.5, h: 0.35, + fontSize: 15, fontFace: FONT_BODY, color: C.white, bold: true, margin: 0 + }); + s.addText(privacyItems[i][1], { + x: x + 0.25, y: y + 0.55, w: 3.5, h: 0.8, + fontSize: 12, fontFace: FONT_BODY, color: C.textLight, margin: 0 + }); + } + + addSlideNumber(s, 12, TOTAL); + + // ============================================= + // SLIDE 13: KOSZTY I PRZYSZŁOŚĆ + // ============================================= + s = pres.addSlide(); + s.background = { color: C.lightBg }; + + s.addText("Koszty, ograniczenia i przyszłość", { + x: 0.8, y: 0.4, w: 8.4, h: 0.7, + fontSize: 30, fontFace: FONT_TITLE, color: C.textDark, bold: true, margin: 0 + }); + s.addShape(pres.shapes.RECTANGLE, { + x: 0.8, y: 1.05, w: 1.2, h: 0.04, fill: { color: C.primary } + }); + + // Left - what's included + addCard(s, 0.8, 1.35, 4.2, 2.0, C.green); + s.addText("Co jest w cenie członkostwa?", { + x: 1.1, y: 1.5, w: 3.5, h: 0.35, + fontSize: 14, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0 + }); + const freeItems = [ + "Profil firmy, katalog, wyszukiwarka", + "Forum, ogłoszenia B2B, aktualności", + "Kalendarz wydarzeń z zapisami", + "Wiadomości prywatne i grupowe", + "Powiadomienia email i push (PWA)", + ]; + s.addText(freeItems.map((item, i) => ({ + text: item, + options: { bullet: true, breakLine: i < freeItems.length - 1, fontSize: 12, color: C.textDark } + })), { + x: 1.1, y: 1.9, w: 3.6, h: 1.3, + fontFace: FONT_BODY, paraSpaceAfter: 3, margin: 0 + }); + + // Right - AI limits + addCard(s, 5.4, 1.35, 4.2, 2.0, C.amber); + s.addText("Ograniczenia AI (NordaGPT)", { + x: 5.7, y: 1.5, w: 3.5, h: 0.35, + fontSize: 14, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0 + }); + const aiLimits = [ + "Silnik AI: Google Gemini (darmowy tier)", + "Limit dzienny na użytkownika", + "Limit zbiorczy na cały portal", + "Odpowiedzi w 2 trybach: szybki / zaawansowany", + "W przyszłości: płatne plany = więcej AI", + ]; + s.addText(aiLimits.map((item, i) => ({ + text: item, + options: { bullet: true, breakLine: i < aiLimits.length - 1, fontSize: 12, color: C.textDark } + })), { + x: 5.7, y: 1.9, w: 3.6, h: 1.3, + fontFace: FONT_BODY, paraSpaceAfter: 3, margin: 0 + }); + + // Bottom - future plans + addCard(s, 0.8, 3.6, 8.4, 1.7, C.indigo); + s.addText("Co planujemy?", { + x: 1.1, y: 3.75, w: 4, h: 0.35, + fontSize: 15, fontFace: FONT_BODY, color: C.textDark, bold: true, margin: 0 + }); + + const futureItems = [ + [icons.robot, "NordaGPT 2.0", "Zaawansowany doradca — analiza rynku, rekomendacje, raporty"], + [icons.handshake, "Marketplace usług", "Giełda kompetencji — członkowie oferują usługi sobie nawzajem"], + [icons.chart, "Raporty branżowe", "Automatyczna analiza trendów i szans rynkowych przez AI"], + [icons.globe, "Integracje", "WhatsApp Business, SMS — komunikacja bez granic"], + ]; + + for (let i = 0; i < futureItems.length; i++) { + const col = i % 2; + const row = Math.floor(i / 2); + const x = 1.1 + col * 4.1; + const y = 4.2 + row * 0.5; + + s.addImage({ data: futureItems[i][0], x, y: y + 0.05, w: 0.25, h: 0.25 }); + s.addText([ + { text: futureItems[i][1] + " ", options: { bold: true, fontSize: 12 } }, + { text: futureItems[i][2], options: { fontSize: 11, color: C.textMuted } } + ], { + x: x + 0.35, y, w: 3.5, h: 0.4, + fontFace: FONT_BODY, color: C.textDark, margin: 0, valign: "middle" + }); + } + + addSlideNumber(s, 13, TOTAL); + + // ============================================= + // SLIDE 14: PYTANIA + // ============================================= + s = pres.addSlide(); + s.background = { color: C.darkBg }; + + s.addShape(pres.shapes.RECTANGLE, { + x: 0, y: 0, w: 10, h: 0.06, fill: { color: C.primary } + }); + + s.addText("Pytania?", { + x: 0.8, y: 1.0, w: 8.4, h: 1.0, + fontSize: 48, fontFace: FONT_TITLE, color: C.white, bold: true, align: "center", margin: 0 + }); + + s.addShape(pres.shapes.RECTANGLE, { + x: 4.2, y: 2.2, w: 1.6, h: 0.04, fill: { color: C.primary } + }); + + s.addText("Zaloguj się i sprawdź", { + x: 0.8, y: 2.6, w: 8.4, h: 0.6, + fontSize: 22, fontFace: FONT_BODY, color: C.primaryLight, align: "center", margin: 0 + }); + + // Norda compass next to URL + s.addImage({ + path: nordaLogoPath, + x: 3.4, y: 3.05, w: 0.7, h: 0.7 + }); + s.addText("nordabiznes.pl", { + x: 4.2, y: 3.1, w: 4, h: 0.6, + fontSize: 28, fontFace: FONT_TITLE, color: C.white, bold: true, margin: 0, valign: "middle" + }); + + // Bottom contact bar + s.addShape(pres.shapes.RECTANGLE, { + x: 1.5, y: 4.1, w: 7, h: 1.0, + fill: { color: C.navyBg } + }); + s.addText([ + { text: "Maciej Pienczyn", options: { bold: true, fontSize: 13, color: C.white } }, + { text: " | maciej.pienczyn@inpi.pl | Izba Gospodarcza NORDA", options: { fontSize: 12, color: C.textLight } } + ], { + x: 1.5, y: 4.1, w: 7, h: 0.55, + fontFace: FONT_BODY, align: "center", valign: "middle", margin: 0 + }); + + // INPI logo + "Stworzone przez" — bottom of contact bar + s.addImage({ + path: inpiLogoPath, + x: 3.65, y: 4.62, w: 0.55, h: 0.31 + }); + s.addText("Stworzone przez", { + x: 2.2, y: 4.62, w: 1.4, h: 0.31, + fontSize: 9, fontFace: FONT_BODY, color: C.textMuted, align: "right", valign: "middle", margin: 0 + }); + s.addText("sp. z o.o.", { + x: 4.25, y: 4.62, w: 1, h: 0.31, + fontSize: 9, fontFace: FONT_BODY, color: C.textMuted, valign: "middle", margin: 0 + }); + + // Save + const outputPath = "/Users/maciejpi/Desktop/NordaBiznes_Prezentacja_2026-04-09.pptx"; + await pres.writeFile({ fileName: outputPath }); + console.log("Presentation saved to:", outputPath); +} + +createPresentation().catch(err => { + console.error("Error:", err); + process.exit(1); +}); diff --git a/scripts/import_kaszubia_2030.py b/scripts/import_kaszubia_2030.py index 3d0a5fb..9c93be9 100644 --- a/scripts/import_kaszubia_2030.py +++ b/scripts/import_kaszubia_2030.py @@ -14,7 +14,7 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from database import Company, Category -# DEV: localhost:5433, PROD: 10.22.68.249:5432 +# DEV: localhost:5433, PROD: 57.128.200.27:5432 DATABASE_URL = os.environ.get('DATABASE_URL', 'postgresql://nordabiz_app:dev_password@localhost:5433/nordabiz') diff --git a/scripts/ovh_vps_monitor.py b/scripts/ovh_vps_monitor.py new file mode 100644 index 0000000..bccc077 --- /dev/null +++ b/scripts/ovh_vps_monitor.py @@ -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() diff --git a/scripts/sync_staging_db.sh b/scripts/sync_staging_db.sh new file mode 100755 index 0000000..c63441e --- /dev/null +++ b/scripts/sync_staging_db.sh @@ -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 ===" diff --git a/templates/admin/ai_usage_company.html b/templates/admin/ai_usage_company.html index a1f8e11..010f895 100644 --- a/templates/admin/ai_usage_company.html +++ b/templates/admin/ai_usage_company.html @@ -543,7 +543,7 @@ {% endif %} - {{ log.created_at.strftime('%d.%m.%Y %H:%M') }} + {{ log.created_at|local_time('%d.%m.%Y %H:%M') }} {% endfor %} diff --git a/templates/admin/ai_usage_user.html b/templates/admin/ai_usage_user.html index 61cd043..20a51dc 100644 --- a/templates/admin/ai_usage_user.html +++ b/templates/admin/ai_usage_user.html @@ -505,7 +505,7 @@ {% endif %} - {{ log.created_at.strftime('%d.%m.%Y %H:%M') }} + {{ log.created_at|local_time('%d.%m.%Y %H:%M') }} {% endfor %} diff --git a/templates/admin/announcements.html b/templates/admin/announcements.html index de9cb89..8ce9305 100755 --- a/templates/admin/announcements.html +++ b/templates/admin/announcements.html @@ -268,7 +268,7 @@ {% endif %} {{ ann.author.name if ann.author else '-' }} - {{ ann.created_at.strftime('%Y-%m-%d %H:%M') if ann.created_at else '-' }} + {{ ann.created_at|local_time('%Y-%m-%d %H:%M') if ann.created_at else '-' }} {{ ann.views_count or 0 }} diff --git a/templates/admin/announcements_form.html b/templates/admin/announcements_form.html index 158e3ad..ff068b0 100755 --- a/templates/admin/announcements_form.html +++ b/templates/admin/announcements_form.html @@ -160,7 +160,7 @@
Status: {{ announcement.status_label }} {% if announcement.published_at %} - | Opublikowano: {{ announcement.published_at.strftime('%Y-%m-%d %H:%M') }} + | Opublikowano: {{ announcement.published_at|local_time('%Y-%m-%d %H:%M') }} {% endif %} {% if announcement.views_count %} | Wyswietlenia: {{ announcement.views_count }} @@ -242,7 +242,7 @@
+ value="{{ announcement.expires_at|local_time('%Y-%m-%dT%H:%M') if announcement and announcement.expires_at else '' }}">

Pozostaw puste aby nie wygasalo

diff --git a/templates/admin/benefits_clicks.html b/templates/admin/benefits_clicks.html index ef5d326..56da277 100644 --- a/templates/admin/benefits_clicks.html +++ b/templates/admin/benefits_clicks.html @@ -95,7 +95,7 @@ {% for click in clicks %} - {{ click.clicked_at.strftime('%Y-%m-%d %H:%M') }} + {{ click.clicked_at|local_time('%Y-%m-%d %H:%M') }} {% if click.user %} {{ click.user.email }} diff --git a/templates/admin/company_detail.html b/templates/admin/company_detail.html index 67f7884..703c788 100644 --- a/templates/admin/company_detail.html +++ b/templates/admin/company_detail.html @@ -680,7 +680,7 @@
Użytkownicy
-
{{ company.created_at.strftime('%d.%m.%Y') if company.created_at else '---' }}
+
{{ company.created_at|local_time('%d.%m.%Y') if company.created_at else '---' }}
Utworzono
@@ -798,7 +798,7 @@
{% if enrichment.registry.done %} - 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 %} Nie wykonano @@ -828,7 +828,7 @@
{% if enrichment.seo.done %} - 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 %} Nie wykonano @@ -875,7 +875,7 @@
{% if enrichment.gbp.done %} - 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 %} Nie wykonano diff --git a/templates/admin/company_requests.html b/templates/admin/company_requests.html index 8f2426d..5a12eff 100644 --- a/templates/admin/company_requests.html +++ b/templates/admin/company_requests.html @@ -270,7 +270,7 @@
Data: - {{ 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 '-' }}
Źródło: diff --git a/templates/admin/data_quality_dashboard.html b/templates/admin/data_quality_dashboard.html index 855485d..864b232 100644 --- a/templates/admin/data_quality_dashboard.html +++ b/templates/admin/data_quality_dashboard.html @@ -580,7 +580,7 @@

Przegląd kompletności i jakości danych {{ total }} firm w katalogu

- Stan na {{ now.strftime('%d.%m.%Y, %H:%M') }} + Stan na {{ now|local_time('%d.%m.%Y, %H:%M') }}
diff --git a/templates/admin/fees.html b/templates/admin/fees.html index 3d37b79..bf5341b 100755 --- a/templates/admin/fees.html +++ b/templates/admin/fees.html @@ -450,9 +450,9 @@ {% if cf.reminder %} {% if cf.reminder.is_read %} - + {% else %} - + {% endif %} {% endif %} diff --git a/templates/admin/forum.html b/templates/admin/forum.html index 6238349..e5f293b 100755 --- a/templates/admin/forum.html +++ b/templates/admin/forum.html @@ -587,7 +587,7 @@ {{ status_labels.get(topic.status, 'Nowy') }} - {{ topic.created_at.strftime('%d.%m.%Y') }} + {{ topic.created_at|local_time('%d.%m.%Y') }}
diff --git a/templates/admin_seo_dashboard.html b/templates/admin_seo_dashboard.html index e9b2abe..afe453e 100644 --- a/templates/admin_seo_dashboard.html +++ b/templates/admin_seo_dashboard.html @@ -857,8 +857,8 @@ {% if company.seo_audited_at %} {% set days_ago = (now - company.seo_audited_at).days %} - - {{ company.seo_audited_at.strftime('%d.%m.%Y') }} + + {{ company.seo_audited_at|local_time('%d.%m.%Y') }} {% else %} Nigdy diff --git a/templates/announcements/detail.html b/templates/announcements/detail.html index 1e3b3b5..ddb3aca 100644 --- a/templates/announcements/detail.html +++ b/templates/announcements/detail.html @@ -386,7 +386,7 @@ {% endfor %} - {{ 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 '' }} {% if announcement.author %} @@ -491,7 +491,7 @@ {{ other.title }}
- {{ 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 '' }}
{% endfor %} diff --git a/templates/announcements/list.html b/templates/announcements/list.html index 1caa798..af4e28a 100755 --- a/templates/announcements/list.html +++ b/templates/announcements/list.html @@ -330,7 +330,7 @@
{{ classified.views_count }} wyswietl. - {{ classified.created_at.strftime('%d.%m.%Y') }} + {{ classified.created_at|local_time('%d.%m.%Y') }}
diff --git a/templates/classifieds/view.html b/templates/classifieds/view.html index 5050d83..30411d2 100755 --- a/templates/classifieds/view.html +++ b/templates/classifieds/view.html @@ -746,9 +746,9 @@
{{ classified.views_count }} wyswietlen - Dodano: {{ classified.created_at.strftime('%d.%m.%Y %H:%M') }} + Dodano: {{ classified.created_at|local_time('%d.%m.%Y %H:%M') }} {% if classified.expires_at %} - Wygasa: {{ classified.expires_at.strftime('%d.%m.%Y') }} + Wygasa: {{ classified.expires_at|local_time('%d.%m.%Y') }} {% endif %}
@@ -807,7 +807,7 @@ {% if q.author.company %} - {{ q.author.company.name }}{% endif %} {% if not q.answer %}Oczekuje na odpowiedz{% endif %} -
{{ q.created_at.strftime('%d.%m.%Y %H:%M') }}
+
{{ q.created_at|local_time('%d.%m.%Y %H:%M') }}
{% if classified.author_id == current_user.id %}
diff --git a/templates/company_detail.html b/templates/company_detail.html index 22634ca..61b6f48 100755 --- a/templates/company_detail.html +++ b/templates/company_detail.html @@ -1613,7 +1613,7 @@ {% if 'ekrs' in first_cp.source %}ekrs.ms.gov.pl (KRS){% elif 'dane.biznes' in first_cp.source %}dane.biznes.gov.pl (CEIDG){% else %}{{ first_cp.source }}{% endif %} {% if first_cp.fetched_at %} - · Pobrano: {{ first_cp.fetched_at.strftime('%Y-%m-%d') }} + · Pobrano: {{ first_cp.fetched_at|local_time('%Y-%m-%d') }} {% endif %}
{% endif %} @@ -1816,11 +1816,11 @@
{% if company.ceidg_fetched_at %} - Pobrano: {{ company.ceidg_fetched_at.strftime('%d.%m.%Y %H:%M') }} • + Pobrano: {{ company.ceidg_fetched_at|local_time('%d.%m.%Y %H:%M') }} • {% elif company.krs_fetched_at %} - Pobrano: {{ company.krs_fetched_at.strftime('%d.%m.%Y %H:%M') }} • + Pobrano: {{ company.krs_fetched_at|local_time('%d.%m.%Y %H:%M') }} • {% elif company.last_verified_at %} - Zweryfikowano: {{ company.last_verified_at.strftime('%d.%m.%Y %H:%M') }} • + Zweryfikowano: {{ company.last_verified_at|local_time('%d.%m.%Y %H:%M') }} • {% endif %} {% if company.ceidg_id %} ID: {{ company.ceidg_id[:8] }}... @@ -2285,7 +2285,7 @@ {% endif %} {% if company.krs_fetched_at %}
- Pobrano z API: {{ company.krs_fetched_at.strftime('%d.%m.%Y %H:%M') }} + Pobrano z API: {{ company.krs_fetched_at|local_time('%d.%m.%Y %H:%M') }}
{% endif %}
@@ -2451,7 +2451,7 @@ {% if company.krs_last_audit_at %} - Audyt KRS: {{ company.krs_last_audit_at.strftime('%d.%m.%Y') }} + Audyt KRS: {{ company.krs_last_audit_at|local_time('%d.%m.%Y') }} {% endif %} @@ -2667,7 +2667,7 @@ Źródło: {{ source_names.get(contact.source, contact.source) }}
{% if contact.source_date %} - ({{ contact.source_date.strftime('%d.%m.%Y') }}) + ({{ contact.source_date|local_time('%d.%m.%Y') }}) {% endif %} {% endif %} @@ -2695,7 +2695,7 @@
Źródło: Strona WWW (auto) {% if website_content.scraped_at %} - ({{ website_content.scraped_at.strftime('%d.%m.%Y') }}) + ({{ website_content.scraped_at|local_time('%d.%m.%Y') }}) {% endif %}
@@ -2760,7 +2760,7 @@ Źródło: {{ source_names.get(contact.source, contact.source) }} {% if contact.source_date %} - ({{ contact.source_date.strftime('%d.%m.%Y') }}) + ({{ contact.source_date|local_time('%d.%m.%Y') }}) {% endif %} {% endif %} @@ -2787,7 +2787,7 @@
Źródło: Strona WWW (auto) {% if website_content.scraped_at %} - ({{ website_content.scraped_at.strftime('%d.%m.%Y') }}) + ({{ website_content.scraped_at|local_time('%d.%m.%Y') }}) {% endif %}
@@ -3139,7 +3139,7 @@ {% endif %}
- {{ rec.created_at.strftime('%d.%m.%Y') }} + {{ rec.created_at|local_time('%d.%m.%Y') }}
@@ -3259,7 +3259,7 @@
Wystawca: {{ wa.ssl_issuer }}
{% endif %} {% if wa.ssl_expires_at %} -
Ważny do {{ wa.ssl_expires_at.strftime('%d.%m.%Y') }}
+
Ważny do {{ wa.ssl_expires_at|local_time('%d.%m.%Y') }}
{% endif %} {% endif %} @@ -3609,7 +3609,7 @@

Analiza SEO - ({{ website_analysis.seo_audited_at.strftime('%d.%m.%Y') }}) + ({{ website_analysis.seo_audited_at|local_time('%d.%m.%Y') }})

@@ -3944,7 +3944,7 @@

Audyt Google Business Profile - ({{ gbp_audit.audit_date.strftime('%d.%m.%Y') }}) + ({{ gbp_audit.audit_date|local_time('%d.%m.%Y') }})

@@ -4236,7 +4236,7 @@

Audyt IT - ({{ it_audit.audit_date.strftime('%d.%m.%Y') }}) + ({{ it_audit.audit_date|local_time('%d.%m.%Y') }})

diff --git a/templates/contacts/detail.html b/templates/contacts/detail.html index d5f2886..4ed7e92 100644 --- a/templates/contacts/detail.html +++ b/templates/contacts/detail.html @@ -649,12 +649,12 @@ {% endif %}
- Dodano: {{ contact.created_at.strftime('%d.%m.%Y %H:%M') if contact.created_at else 'nieznana' }} + Dodano: {{ contact.created_at|local_time('%d.%m.%Y %H:%M') if contact.created_at else 'nieznana' }} {% if contact.creator %} przez {{ contact.creator.name or contact.creator.email }} {% endif %} {% if contact.updated_at and contact.updated_at != contact.created_at %} - | Zaktualizowano: {{ contact.updated_at.strftime('%d.%m.%Y %H:%M') }} + | Zaktualizowano: {{ contact.updated_at|local_time('%d.%m.%Y %H:%M') }} {% endif %}
diff --git a/templates/dashboard.html b/templates/dashboard.html index 9bfe6a8..88375c8 100755 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -1019,7 +1019,7 @@
{{ ann.title[:50] }}{% if ann.title|length > 50 %}...{% endif %}
{{ ann.category_label }} - {% if ann.published_at %} · {{ ann.published_at.strftime('%d.%m.%Y') }}{% endif %} + {% if ann.published_at %} · {{ ann.published_at|local_time('%d.%m.%Y') }}{% endif %}
@@ -1063,7 +1063,7 @@
{{ topic.title[:45] }}{% if topic.title|length > 45 %}...{% endif %}
{{ topic.CATEGORY_LABELS.get(topic.category, topic.category) }} - · {{ topic.updated_at.strftime('%d.%m.%Y') }} + · {{ topic.updated_at|local_time('%d.%m.%Y') }}
{{ topic.views_count }} views @@ -1097,7 +1097,7 @@
{{ cl.title[:45] }}{% if cl.title|length > 45 %}...{% endif %}
{% if cl.company %}{{ cl.company.name[:25] }}{% endif %} - · {{ cl.created_at.strftime('%d.%m.%Y') }} + · {{ cl.created_at|local_time('%d.%m.%Y') }}
{{ 'Szukam' if cl.listing_type == 'szukam' else 'Oferuję' }} @@ -1137,7 +1137,7 @@ {% if company.category %} · {{ company.category.name }}{% endif %} - {{ company.created_at.strftime('%d.%m') if company.created_at else '' }} + {{ company.created_at|local_time('%d.%m') if company.created_at else '' }}
{% endfor %} {% else %} @@ -1166,7 +1166,7 @@
{{ conv.title }}
-
{{ conv.updated_at.strftime('%d.%m.%Y %H:%M') }}
+
{{ conv.updated_at|local_time('%d.%m.%Y %H:%M') }}
{{ conv.message_count }} msg
diff --git a/templates/forum/index.html b/templates/forum/index.html index b6b67cf..4e25154 100755 --- a/templates/forum/index.html +++ b/templates/forum/index.html @@ -434,7 +434,7 @@ - {{ topic.created_at.strftime('%d.%m.%Y %H:%M') }} + {{ topic.created_at|local_time('%d.%m.%Y %H:%M') }} diff --git a/templates/forum/topic.html b/templates/forum/topic.html index 9f8c3c2..885b70a 100755 --- a/templates/forum/topic.html +++ b/templates/forum/topic.html @@ -1076,9 +1076,9 @@ - {{ topic.created_at.strftime('%d.%m.%Y %H:%M') }} + {{ topic.created_at|local_time('%d.%m.%Y %H:%M') }} {% if topic.edited_at %} - (edytowano {{ topic.edited_at.strftime('%d.%m.%Y %H:%M') }}) + (edytowano {{ topic.edited_at|local_time('%d.%m.%Y %H:%M') }}) {% endif %} @@ -1190,7 +1190,7 @@ {% endif %} - {{ reply.created_at.strftime('%d.%m.%Y %H:%M') }} + {{ reply.created_at|local_time('%d.%m.%Y %H:%M') }} {% if reply.edited_at %} (edytowano) {% endif %} diff --git a/templates/gbp_audit.html b/templates/gbp_audit.html index dfe4ac3..9630253 100644 --- a/templates/gbp_audit.html +++ b/templates/gbp_audit.html @@ -971,7 +971,7 @@

Dostępne są pełne dane: wyświetlenia, wyszukiwania, kliknięcia i interakcje klientów. {% if gbp_connection.google_email %}Konto: {{ gbp_connection.google_email }}.{% endif %} - {% if gbp_connection.created_at %}Połączone od {{ gbp_connection.created_at.strftime('%d.%m.%Y') }}.{% endif %} + {% if gbp_connection.created_at %}Połączone od {{ gbp_connection.created_at|local_time('%d.%m.%Y') }}.{% endif %}

Zarządzaj @@ -982,7 +982,7 @@
Połączenie z konsolą GBP wygasło

- Autoryzacja Google wygasła{% if gbp_connection.expires_at %} {{ gbp_connection.expires_at.strftime('%d.%m.%Y') }}{% endif %}. + Autoryzacja Google wygasła{% if gbp_connection.expires_at %} {{ gbp_connection.expires_at|local_time('%d.%m.%Y') }}{% endif %}. {% if gbp_connection.google_email %}Ostatnio użyte konto: {{ gbp_connection.google_email }}.{% endif %} Połącz ponownie tym samym kontem, aby przywrócić pełne statystyki.

@@ -1049,7 +1049,7 @@ - Ostatni audyt: {{ audit.audit_date.strftime('%d.%m.%Y %H:%M') if audit.audit_date else 'Brak danych' }} + Ostatni audyt: {{ audit.audit_date|local_time('%d.%m.%Y %H:%M') if audit.audit_date else 'Brak danych' }}
{% if audit.review_count %}
@@ -1506,7 +1506,7 @@ {% endif %}
{% if review.publish_time %} - {{ review.publish_time.strftime('%d.%m.%Y') }} + {{ review.publish_time|local_time('%d.%m.%Y') }} {% endif %} {% if review.text %} @@ -1517,7 +1517,7 @@
Odpowiedz wlasciciela: {% if review.owner_response_time %} - {{ review.owner_response_time.strftime('%d.%m.%Y') }} + {{ review.owner_response_time|local_time('%d.%m.%Y') }} {% endif %}

{{ review.owner_response_text[:200] }}{% if review.owner_response_text|length > 200 %}...{% endif %}

@@ -2229,7 +2229,7 @@ Wersja: {{ audit.audit_version }} {% endif %} {% if audit.audit_date %} - Data: {{ audit.audit_date.strftime('%d.%m.%Y %H:%M') }} + Data: {{ audit.audit_date|local_time('%d.%m.%Y %H:%M') }} {% endif %} {% if audit.audit_errors %} diff --git a/templates/index.html b/templates/index.html index 28178a3..7a9f2ec 100755 --- a/templates/index.html +++ b/templates/index.html @@ -1222,7 +1222,7 @@

{% if fact.source_news %}
- {{ fact.source_news.source_name or fact.source_news.source_domain }} • {{ fact.source_news.published_at.strftime('%d.%m.%Y') if fact.source_news.published_at else '' }} + {{ fact.source_news.source_name or fact.source_news.source_domain }} • {{ fact.source_news.published_at|local_time('%d.%m.%Y') if fact.source_news.published_at else '' }} Czytaj →
{% endif %} diff --git a/templates/it_audit.html b/templates/it_audit.html index a8efec3..5805b09 100644 --- a/templates/it_audit.html +++ b/templates/it_audit.html @@ -470,7 +470,7 @@ - Ostatni audyt: {{ audit_data.audit_date.strftime('%d.%m.%Y') if audit_data.audit_date else 'Brak danych' }} + Ostatni audyt: {{ audit_data.audit_date|local_time('%d.%m.%Y') if audit_data.audit_date else 'Brak danych' }} {% if audit_data.audit_source %}
diff --git a/templates/konto/bezpieczenstwo.html b/templates/konto/bezpieczenstwo.html index db8c189..325b088 100644 --- a/templates/konto/bezpieczenstwo.html +++ b/templates/konto/bezpieczenstwo.html @@ -286,8 +286,8 @@ Historia logowań -

Ostatnie logowanie: {% if current_user.last_login %}{{ current_user.last_login.strftime('%d.%m.%Y %H:%M') }}{% else %}Brak danych{% endif %}

-

Konto utworzone: {{ current_user.created_at.strftime('%d.%m.%Y') }}

+

Ostatnie logowanie: {% if current_user.last_login %}{{ current_user.last_login|local_time('%d.%m.%Y %H:%M') }}{% else %}Brak danych{% endif %}

+

Konto utworzone: {{ current_user.created_at|local_time('%d.%m.%Y') }}

diff --git a/templates/konto/blokady.html b/templates/konto/blokady.html index 95495c8..477e432 100644 --- a/templates/konto/blokady.html +++ b/templates/konto/blokady.html @@ -340,7 +340,7 @@
{{ block.blocked_user.name or block.blocked_user.email.split('@')[0] }}
-
Zablokowano: {{ block.created_at.strftime('%d.%m.%Y') if block.created_at else '-' }}
+
Zablokowano: {{ block.created_at|local_time('%d.%m.%Y') if block.created_at else '-' }}
diff --git a/templates/membership/data_request.html b/templates/membership/data_request.html index c209dde..6f4456b 100644 --- a/templates/membership/data_request.html +++ b/templates/membership/data_request.html @@ -242,7 +242,7 @@ {% if pending_request %}

Masz już oczekujące zgłoszenie

-

Twoje poprzednie zgłoszenie z dnia {{ pending_request.created_at.strftime('%Y-%m-%d') }} jest w trakcie rozpatrywania.

+

Twoje poprzednie zgłoszenie z dnia {{ pending_request.created_at|local_time('%Y-%m-%d') }} jest w trakcie rozpatrywania.

Wróć na stronę główną

{% else %} diff --git a/templates/membership/status.html b/templates/membership/status.html index 57786dc..d56a84f 100644 --- a/templates/membership/status.html +++ b/templates/membership/status.html @@ -426,18 +426,18 @@
Utworzono
-
{{ app.created_at.strftime('%Y-%m-%d %H:%M') if app.created_at else '-' }}
+
{{ app.created_at|local_time('%Y-%m-%d %H:%M') if app.created_at else '-' }}
{% if app.submitted_at %}
Wysłano
-
{{ app.submitted_at.strftime('%Y-%m-%d %H:%M') }}
+
{{ app.submitted_at|local_time('%Y-%m-%d %H:%M') }}
{% endif %} {% if app.reviewed_at %}
Rozpatrzono
-
{{ app.reviewed_at.strftime('%Y-%m-%d %H:%M') }}
+
{{ app.reviewed_at|local_time('%Y-%m-%d %H:%M') }}
{% endif %}
@@ -478,7 +478,7 @@
Złożono deklarację{% if app.user %} przez: {{ app.user.name or app.user.email }}{% endif %}
-
{{ app.submitted_at.strftime('%Y-%m-%d') if app.submitted_at else '-' }}
+
{{ app.submitted_at|local_time('%Y-%m-%d') if app.submitted_at else '-' }}
{% if app.status in ['under_review', 'pending_user_approval', 'approved', 'rejected', 'changes_requested'] %} @@ -531,7 +531,7 @@ Oczekuje na Twoją akceptację
-
{{ app.proposed_changes_at.strftime('%Y-%m-%d') if app.proposed_changes_at else '' }}
+
{{ app.proposed_changes_at|local_time('%Y-%m-%d') if app.proposed_changes_at else '' }}
{% endif %} @@ -550,21 +550,21 @@ {% if app.status == 'changes_requested' %}
Wymagane poprawki{% if app.reviewed_by %} - zgłosił: {{ app.reviewed_by.name or app.reviewed_by.email }}{% endif %}
-
{{ app.reviewed_at.strftime('%Y-%m-%d') if app.reviewed_at else '' }}
+
{{ app.reviewed_at|local_time('%Y-%m-%d') if app.reviewed_at else '' }}
{% endif %} {% if app.status == 'approved' %}
Zatwierdzono{% if app.reviewed_by %} przez: {{ app.reviewed_by.name or app.reviewed_by.email }}{% endif %}
-
{{ app.reviewed_at.strftime('%Y-%m-%d') if app.reviewed_at else '' }}
+
{{ app.reviewed_at|local_time('%Y-%m-%d') if app.reviewed_at else '' }}
{% endif %} {% if app.status == 'rejected' %}
Odrzucono{% if app.reviewed_by %} przez: {{ app.reviewed_by.name or app.reviewed_by.email }}{% endif %}
-
{{ app.reviewed_at.strftime('%Y-%m-%d') if app.reviewed_at else '' }}
+
{{ app.reviewed_at|local_time('%Y-%m-%d') if app.reviewed_at else '' }}
{% endif %} diff --git a/templates/messages/group_view.html b/templates/messages/group_view.html index eb3204b..9f7d14b 100644 --- a/templates/messages/group_view.html +++ b/templates/messages/group_view.html @@ -454,7 +454,7 @@
{% if msg.sender_id == current_user.id %}Ty{% else %}{{ msg.sender.name or msg.sender.email.split('@')[0] }}{% endif %} - {{ msg.created_at.strftime('%d.%m.%Y %H:%M') }} + {{ msg.created_at|local_time('%d.%m.%Y %H:%M') }} {% if msg.sender_id == current_user.id or membership.is_owner %} diff --git a/templates/messages/inbox.html b/templates/messages/inbox.html index f297bda..a406808 100755 --- a/templates/messages/inbox.html +++ b/templates/messages/inbox.html @@ -364,7 +364,7 @@ {{ gi.group.member_count }} os.
- {{ gi.sort_date.strftime('%d.%m.%Y %H:%M') if gi.sort_date else '' }} + {{ gi.sort_date|local_time('%d.%m.%Y %H:%M') if gi.sort_date else '' }}
{% if gi.last_message %} @@ -409,7 +409,7 @@ {% endif %}
- {{ msg.created_at.strftime('%d.%m.%Y %H:%M') }} + {{ msg.created_at|local_time('%d.%m.%Y %H:%M') }}
{{ msg.content|striptags|truncate(100) }} diff --git a/templates/messages/sent.html b/templates/messages/sent.html index a01a5a3..8ca0156 100755 --- a/templates/messages/sent.html +++ b/templates/messages/sent.html @@ -348,14 +348,14 @@ {% endif %}
- {{ msg.created_at.strftime('%d.%m.%Y %H:%M') }} + {{ msg.created_at|local_time('%d.%m.%Y %H:%M') }}
{{ msg.content[:100] }}{% if msg.content|length > 100 %}…{% endif %} Do: {{ msg.recipient.name or msg.recipient.email.split('@')[0] }}
{% if msg.is_read %} - + Przeczytana diff --git a/templates/messages/view.html b/templates/messages/view.html index bd9c14e..23033c2 100755 --- a/templates/messages/view.html +++ b/templates/messages/view.html @@ -430,10 +430,10 @@
- {{ msg.created_at.strftime('%d.%m.%Y %H:%M') }} + {{ msg.created_at|local_time('%d.%m.%Y %H:%M') }} {% if msg.sender_id == current_user.id %} {% if msg.is_read %} - + Przeczytana @@ -550,13 +550,13 @@
- {{ message.created_at.strftime('%d.%m.%Y o %H:%M') }} + {{ message.created_at|local_time('%d.%m.%Y o %H:%M') }} {% if message.sender_id == current_user.id %} {% if message.is_read %} - + - Przeczytana{{ ' ' + message.read_at.strftime('%d.%m o %H:%M') if message.read_at else '' }} + Przeczytana{{ ' ' + message.read_at|local_time('%d.%m o %H:%M') if message.read_at else '' }} {% else %} diff --git a/templates/new_members.html b/templates/new_members.html index ab473fb..d151462 100755 --- a/templates/new_members.html +++ b/templates/new_members.html @@ -189,7 +189,7 @@
{% endif %}
- Dolaczyl: {{ company.created_at.strftime('%d.%m.%Y') }} + Dolaczyl: {{ company.created_at|local_time('%d.%m.%Y') }} {% if company.address_city %} diff --git a/templates/pej/index.html b/templates/pej/index.html index 99b3825..7ddaae2 100644 --- a/templates/pej/index.html +++ b/templates/pej/index.html @@ -203,7 +203,7 @@ {% for ann in announcements %}
{{ ann.title }}
-
{{ ann.created_at.strftime('%d.%m.%Y') }}
+
{{ ann.created_at|local_time('%d.%m.%Y') }}

{{ ann.content[:300] }}{% if ann.content|length > 300 %}...{% endif %}

{% endfor %} @@ -219,7 +219,7 @@

{{ item.title }}

{{ item.source_name or item.source_domain or '' }} - {% if item.published_at %} · {{ item.published_at.strftime('%d.%m.%Y') }}{% endif %} + {% if item.published_at %} · {{ item.published_at|local_time('%d.%m.%Y') }}{% endif %}
{% endfor %} diff --git a/templates/pej/news.html b/templates/pej/news.html index d9c82ad..1a72604 100644 --- a/templates/pej/news.html +++ b/templates/pej/news.html @@ -73,7 +73,7 @@
{{ item.source_name or item.source_domain or 'Źródło nieznane' }} - {% if item.published_at %} · {{ item.published_at.strftime('%d.%m.%Y') }}{% endif %} + {% if item.published_at %} · {{ item.published_at|local_time('%d.%m.%Y') }}{% endif %} {% if item.ai_relevance_score %} · {% for i in range(item.ai_relevance_score) %}★{% endfor %}{% for i in range(5 - item.ai_relevance_score) %}☆{% endfor %} {% endif %} diff --git a/templates/reports/categories.html b/templates/reports/categories.html index 674dd9f..f59654d 100644 --- a/templates/reports/categories.html +++ b/templates/reports/categories.html @@ -352,7 +352,7 @@ - Wygenerowano: {{ generated_at.strftime('%d.%m.%Y, %H:%M:%S') }} + Wygenerowano: {{ generated_at|local_time('%d.%m.%Y, %H:%M:%S') }}
diff --git a/templates/reports/fees.html b/templates/reports/fees.html index 59dfc95..6b95285 100644 --- a/templates/reports/fees.html +++ b/templates/reports/fees.html @@ -55,7 +55,7 @@ ← Raporty

Składki członkowskie {{ year }}

- Wygenerowano: {{ generated_at.strftime('%d.%m.%Y %H:%M') }} | + Wygenerowano: {{ generated_at|local_time('%d.%m.%Y %H:%M') }} | diff --git a/templates/reports/membership.html b/templates/reports/membership.html index 6695f7f..92c9a9e 100644 --- a/templates/reports/membership.html +++ b/templates/reports/membership.html @@ -224,7 +224,7 @@ - Wygenerowano: {{ generated_at.strftime('%d.%m.%Y, %H:%M:%S') }} + Wygenerowano: {{ generated_at|local_time('%d.%m.%Y, %H:%M:%S') }}
diff --git a/templates/reports/social_media.html b/templates/reports/social_media.html index 0c9eaca..7852c5d 100644 --- a/templates/reports/social_media.html +++ b/templates/reports/social_media.html @@ -280,7 +280,7 @@ - Wygenerowano: {{ generated_at.strftime('%d.%m.%Y, %H:%M:%S') }} + Wygenerowano: {{ generated_at|local_time('%d.%m.%Y, %H:%M:%S') }}
diff --git a/templates/seo_audit.html b/templates/seo_audit.html index ed1d152..fff5066 100644 --- a/templates/seo_audit.html +++ b/templates/seo_audit.html @@ -683,7 +683,7 @@ - Ostatni audyt: {{ seo_data.audited_at.strftime('%d.%m.%Y %H:%M') if seo_data.audited_at else 'Brak danych' }} + Ostatni audyt: {{ seo_data.audited_at|local_time('%d.%m.%Y %H:%M') if seo_data.audited_at else 'Brak danych' }}
{% if seo_data.url %}
@@ -1212,7 +1212,7 @@
{% if seo_data.gsc_last_crawl %} - Ostatni crawl: {{ seo_data.gsc_last_crawl.strftime('%Y-%m-%d') if seo_data.gsc_last_crawl.strftime is defined else seo_data.gsc_last_crawl[:10] }} + Ostatni crawl: {{ seo_data.gsc_last_crawl|local_time('%Y-%m-%d') if seo_data.gsc_last_crawl.strftime is defined else seo_data.gsc_last_crawl[:10] }} {% endif %} {% if seo_data.gsc_crawled_as %} @@ -1375,7 +1375,7 @@
Ostatnia modyfikacja
-
{{ seo_data.last_modified_date.strftime('%d.%m.%Y') }}
+
{{ seo_data.last_modified_date|local_time('%d.%m.%Y') }}
{% endif %} @@ -1486,7 +1486,7 @@ {% if seo_data.has_ssl is not none %}
{{ '✓' if seo_data.has_ssl else '✗' }} - Certyfikat SSL{% if seo_data.has_ssl and seo_data.ssl_expires_at %} (ważny do {{ seo_data.ssl_expires_at.strftime('%d.%m.%Y') }}){% endif %} + Certyfikat SSL{% if seo_data.has_ssl and seo_data.ssl_expires_at %} (ważny do {{ seo_data.ssl_expires_at|local_time('%d.%m.%Y') }}){% endif %}
{% endif %} diff --git a/templates/settings/blocks.html b/templates/settings/blocks.html index 893e3b0..48f2ff2 100644 --- a/templates/settings/blocks.html +++ b/templates/settings/blocks.html @@ -250,7 +250,7 @@
{{ block.blocked_user.name or block.blocked_user.email.split('@')[0] }}
-
Zablokowano: {{ block.created_at.strftime('%d.%m.%Y') if block.created_at else '-' }}
+
Zablokowano: {{ block.created_at|local_time('%d.%m.%Y') if block.created_at else '-' }}
diff --git a/templates/social_audit.html b/templates/social_audit.html index d9f82a4..9308241 100644 --- a/templates/social_audit.html +++ b/templates/social_audit.html @@ -903,7 +903,7 @@ - {{ profile.verified_at.strftime('%d.%m.%Y') }} + {{ profile.verified_at|local_time('%d.%m.%Y') }} {% endif %} {% if profile.last_checked_at %} @@ -911,7 +911,7 @@ - Sprawdzono: {{ profile.last_checked_at.strftime('%d.%m.%Y') }} + Sprawdzono: {{ profile.last_checked_at|local_time('%d.%m.%Y') }} {% endif %} {% if profile.has_bio %} @@ -968,7 +968,7 @@ - Ostatni: {{ profile.last_post_date.strftime('%d.%m.%Y') }} + Ostatni: {{ profile.last_post_date|local_time('%d.%m.%Y') }} {% endif %} {% if profile.posting_frequency_score is not none %} diff --git a/templates/zopk/index.html b/templates/zopk/index.html index 1075c8e..1ebbd1d 100644 --- a/templates/zopk/index.html +++ b/templates/zopk/index.html @@ -1297,7 +1297,7 @@ {% endif %}
{{ news.source_name or news.source_domain }} - {{ news.published_at.strftime('%d.%m.%Y') if news.published_at else '-' }} + {{ news.published_at|local_time('%d.%m.%Y') if news.published_at else '-' }}
diff --git a/templates/zopk/news_list.html b/templates/zopk/news_list.html index 55374f3..2031123 100644 --- a/templates/zopk/news_list.html +++ b/templates/zopk/news_list.html @@ -204,7 +204,7 @@ {% endif %}
{{ news.source_name or news.source_domain }} - {{ news.published_at.strftime('%d.%m.%Y') if news.published_at else '-' }} + {{ news.published_at|local_time('%d.%m.%Y') if news.published_at else '-' }}
diff --git a/templates/zopk/project_detail.html b/templates/zopk/project_detail.html index e3dd8b5..26f29f1 100644 --- a/templates/zopk/project_detail.html +++ b/templates/zopk/project_detail.html @@ -276,7 +276,7 @@

{{ news.title }}

{{ news.source_name or news.source_domain }} • - {{ news.published_at.strftime('%d.%m.%Y') if news.published_at else '-' }} + {{ news.published_at|local_time('%d.%m.%Y') if news.published_at else '-' }}
{% endfor %} diff --git a/test_diagrams.md b/test_diagrams.md index bf228c6..a9f4925 100644 --- a/test_diagrams.md +++ b/test_diagrams.md @@ -19,7 +19,7 @@ A["User says 'hello'"] ## Diagram 10 - Line Break Example ```mermaid %% Use
for line breaks in labels -A[Flask App
10.22.68.249
Port 5000] +A[Flask App
57.128.200.27
Port 5000] ``` ## Diagram 11 - Style Guide diff --git a/tests/dr/test_dr_procedures.py b/tests/dr/test_dr_procedures.py index 2f172b6..c26f430 100644 --- a/tests/dr/test_dr_procedures.py +++ b/tests/dr/test_dr_procedures.py @@ -16,7 +16,7 @@ pytestmark = pytest.mark.dr PROJECT_ROOT = Path(__file__).parent.parent.parent STAGING_HOST = os.environ.get('STAGING_HOST', 'maciejpi@10.22.68.251') -PROD_HOST = os.environ.get('PROD_HOST', 'maciejpi@10.22.68.249') +PROD_HOST = os.environ.get('PROD_HOST', 'maciejpi@57.128.200.27') def ssh_command(host: str, cmd: str, timeout: int = 60) -> tuple[int, str, str]: diff --git a/tests/smoke/test_backup_health.py b/tests/smoke/test_backup_health.py index 841bdc9..c5ab46d 100644 --- a/tests/smoke/test_backup_health.py +++ b/tests/smoke/test_backup_health.py @@ -18,7 +18,7 @@ import pytest pytestmark = [pytest.mark.smoke, pytest.mark.dr] # Production server -PROD_HOST = os.environ.get('PROD_HOST', 'maciejpi@10.22.68.249') +PROD_HOST = os.environ.get('PROD_HOST', 'maciejpi@57.128.200.27') BACKUP_DIR = '/var/backups/nordabiz' diff --git a/verify_architecture_accuracy.py b/verify_architecture_accuracy.py index 711fe10..b065aa9 100644 --- a/verify_architecture_accuracy.py +++ b/verify_architecture_accuracy.py @@ -182,7 +182,7 @@ class ArchitectureVerifier: # Verify critical configurations are mentioned critical_items = [ - ('10.22.68.249', 'NORDABIZ-01 IP address'), + ('57.128.200.27', 'NORDABIZ-01 IP address'), ('10.22.68.250', 'R11-REVPROXY-01 IP address'), ('port 5000', 'Flask/Gunicorn port'), ('port 5432', 'PostgreSQL port'),