# PEJ Section Implementation Plan > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Create a dedicated PEJ (nuclear energy) section on nordabiznes.pl that filters ZOPK data, shows Local Content company list, and reorganizes navigation. **Architecture:** Hybrid "lens" approach — PEJ consumes existing ZOPK data (news, milestones, companies, facts, entities) filtered by nuclear project IDs. No new database tables. New routes in existing `public`/`admin` blueprints. Navigation reorganized: "Projekty ▾" dropdown replaces standalone "Kaszubia", admin nav reduced from 13 to 10 items. **Tech Stack:** Flask 3.0, SQLAlchemy 2.0, Jinja2, PostgreSQL, existing ZOPK models **Spec:** `docs/superpowers/specs/2026-03-16-pej-section-design.md` --- ## File Structure | Action | File | Responsibility | |--------|------|----------------| | Create | `blueprints/public/routes_pej.py` | 3 public routes: index, local-content, news | | Create | `blueprints/admin/routes_pej.py` | 1 admin route: CSV export | | Create | `templates/pej/index.html` | PEJ landing page | | Create | `templates/pej/local_content.html` | Company list with filters | | Create | `templates/pej/news.html` | Nuclear news list | | Create | `blueprints/pej_constants.py` | Shared constants: NUCLEAR_PROJECT_SLUGS, LINK_TYPE_LABELS, get_nuclear_project_ids() | | Create | `tests/unit/test_pej_helpers.py` | Unit test for helper function | | Modify | `database.py:4690-4696` | Add `'pej'` to Announcement.CATEGORIES/LABELS | | Modify | `blueprints/public/__init__.py:13` | Import routes_pej | | Modify | `blueprints/admin/__init__.py:20-23` | Import routes_pej | | Modify | `blueprints/__init__.py` | Add endpoint aliases for pej_index, pej_local_content, pej_news | | Modify | `templates/base.html:1461-1504` | Reorganize main NAV | | Modify | `templates/base.html:1855` | Add "Narzędzia" to admin bar | --- ## Chunk 1: Backend (Routes, Data, Model) ### Task 1: Announcement category + helper function **Files:** - Modify: `database.py:4690-4696` - Create: `tests/unit/test_pej_helpers.py` - [ ] **Step 1: Add `pej` to Announcement.CATEGORIES** In `database.py`, line 4690, change: ```python CATEGORIES = ['internal', 'external', 'event', 'opportunity', 'partnership'] ``` to: ```python CATEGORIES = ['internal', 'external', 'event', 'opportunity', 'partnership', 'pej'] ``` In `database.py`, after the last entry in `CATEGORY_LABELS` dict (~line 4696), add: ```python 'pej': 'PEJ / Energetyka jądrowa', ``` - [ ] **Step 2: Verify syntax** Run: `python -m py_compile database.py` Expected: No output (clean compile) - [ ] **Step 3: Commit** ```bash git add database.py git commit -m "feat(pej): add 'pej' category to Announcement model" ``` --- ### Task 2: PEJ public routes **Files:** - Create: `blueprints/public/routes_pej.py` - Modify: `blueprints/public/__init__.py:13` - [ ] **Step 1: Create shared constants module** Create `blueprints/pej_constants.py` — shared between public and admin PEJ routes: ```python """Shared constants and helpers for PEJ section.""" from database import db_session, ZOPKProject # Explicit slug list — easy to extend with SMR projects later NUCLEAR_PROJECT_SLUGS = ['nuclear-plant'] LINK_TYPE_LABELS = { 'potential_supplier': 'Potencjalny dostawca', 'partner': 'Partner', 'investor': 'Inwestor', 'beneficiary': 'Beneficjent' } def get_nuclear_project_ids(): """Return IDs of nuclear projects from ZOPK.""" projects = db_session.query(ZOPKProject.id).filter( ZOPKProject.slug.in_(NUCLEAR_PROJECT_SLUGS), ZOPKProject.project_type == 'energy' ).all() return [p.id for p in projects] ``` - [ ] **Step 2: Create routes_pej.py** Create `blueprints/public/routes_pej.py`. Pattern follows `routes_zopk.py` — imports `bp` from package, uses `@bp.route`. **WAŻNE — nazwy pól modeli:** - `Company.category` to relacja (ORM object), NIE string → filtruj po `Company.category_id`, wyświetlaj `company.category.name` - `Company.address_city` (NIE `city`) - `Company.pkd_code` (NIE `pkd_main`) - `Company.services_offered` (NIE `services` — to relacja) - `Announcement.categories.contains(['pej'])` (NIE `.any('pej')`) ```python """PEJ (nuclear energy) section routes — filtered lens on ZOPK data.""" import math from flask import render_template, request, abort from flask_login import login_required from sqlalchemy import func from . import bp from database import ( db_session, ZOPKNews, ZOPKMilestone, ZOPKCompanyLink, Company, Announcement, Category ) from blueprints.pej_constants import get_nuclear_project_ids, LINK_TYPE_LABELS @bp.route('/pej') @login_required def pej_index(): """PEJ landing page — hero, stats, news, timeline, top companies, announcements.""" nuclear_ids = get_nuclear_project_ids() if not nuclear_ids: abort(404) # Stats companies_count = db_session.query(func.count(ZOPKCompanyLink.id)).filter( ZOPKCompanyLink.project_id.in_(nuclear_ids), ZOPKCompanyLink.relevance_score >= 25 ).scalar() or 0 news_count = db_session.query(func.count(ZOPKNews.id)).filter( ZOPKNews.project_id.in_(nuclear_ids), ZOPKNews.status.in_(['approved', 'auto_approved']) ).scalar() or 0 milestones_count = db_session.query(func.count(ZOPKMilestone.id)).filter( ZOPKMilestone.category == 'nuclear' ).scalar() or 0 # Latest news (4) news = db_session.query(ZOPKNews).filter( ZOPKNews.project_id.in_(nuclear_ids), ZOPKNews.status.in_(['approved', 'auto_approved']) ).order_by(ZOPKNews.published_at.desc()).limit(4).all() # Nuclear milestones milestones = db_session.query(ZOPKMilestone).filter( ZOPKMilestone.category == 'nuclear' ).order_by(ZOPKMilestone.target_date.asc()).all() # Top 6 companies by relevance top_companies = db_session.query(ZOPKCompanyLink, Company).join( Company, ZOPKCompanyLink.company_id == Company.id ).filter( ZOPKCompanyLink.project_id.in_(nuclear_ids), ZOPKCompanyLink.relevance_score >= 25, Company.status == 'active' ).order_by(ZOPKCompanyLink.relevance_score.desc()).limit(6).all() # PEJ announcements announcements = db_session.query(Announcement).filter( Announcement.categories.contains(['pej']), Announcement.status == 'approved' ).order_by(Announcement.created_at.desc()).limit(3).all() return render_template('pej/index.html', companies_count=companies_count, news_count=news_count, milestones_count=milestones_count, news=news, milestones=milestones, top_companies=top_companies, announcements=announcements, link_type_labels=LINK_TYPE_LABELS ) @bp.route('/pej/local-content') @login_required def pej_local_content(): """Full list of Norda companies matched to nuclear projects.""" nuclear_ids = get_nuclear_project_ids() if not nuclear_ids: abort(404) page = request.args.get('page', 1, type=int) per_page = 20 category_filter = request.args.get('category', '', type=int) # category_id (int) link_type_filter = request.args.get('link_type', '') search_query = request.args.get('q', '') query = db_session.query(ZOPKCompanyLink, Company).join( Company, ZOPKCompanyLink.company_id == Company.id ).filter( ZOPKCompanyLink.project_id.in_(nuclear_ids), ZOPKCompanyLink.relevance_score >= 25, Company.status == 'active' ) if category_filter: query = query.filter(Company.category_id == category_filter) if link_type_filter: query = query.filter(ZOPKCompanyLink.link_type == link_type_filter) if search_query: query = query.filter(Company.name.ilike(f'%{search_query}%')) total = query.count() results = query.order_by( ZOPKCompanyLink.relevance_score.desc() ).offset((page - 1) * per_page).limit(per_page).all() # Get distinct categories for filter dropdown (as Category objects) category_ids = db_session.query(Company.category_id).join( ZOPKCompanyLink, Company.id == ZOPKCompanyLink.company_id ).filter( ZOPKCompanyLink.project_id.in_(nuclear_ids), ZOPKCompanyLink.relevance_score >= 25, Company.status == 'active', Company.category_id.isnot(None) ).distinct().all() category_ids = [c[0] for c in category_ids if c[0]] categories = db_session.query(Category).filter( Category.id.in_(category_ids) ).order_by(Category.name).all() if category_ids else [] link_types = db_session.query(ZOPKCompanyLink.link_type).filter( ZOPKCompanyLink.project_id.in_(nuclear_ids), ZOPKCompanyLink.relevance_score >= 25 ).distinct().all() link_types = sorted([lt[0] for lt in link_types if lt[0]]) total_pages = math.ceil(total / per_page) if total > 0 else 1 return render_template('pej/local_content.html', results=results, total=total, page=page, per_page=per_page, total_pages=total_pages, categories=categories, link_types=link_types, link_type_labels=LINK_TYPE_LABELS, category_filter=category_filter, link_type_filter=link_type_filter, search_query=search_query ) @bp.route('/pej/aktualnosci') @login_required def pej_news(): """Nuclear news list with pagination.""" nuclear_ids = get_nuclear_project_ids() if not nuclear_ids: abort(404) page = request.args.get('page', 1, type=int) per_page = 20 query = db_session.query(ZOPKNews).filter( ZOPKNews.project_id.in_(nuclear_ids), ZOPKNews.status.in_(['approved', 'auto_approved']) ).order_by(ZOPKNews.published_at.desc()) total = query.count() news = query.offset((page - 1) * per_page).limit(per_page).all() total_pages = math.ceil(total / per_page) if total > 0 else 1 return render_template('pej/news.html', news=news, page=page, total=total, total_pages=total_pages ) ``` - [ ] **Step 3: Register routes in public blueprint** In `blueprints/public/__init__.py`, after line 13 (the `routes_zopk` import), add: ```python from . import routes_pej # noqa: E402, F401 ``` - [ ] **Step 4: Add endpoint aliases** In `blueprints/__init__.py`, find the `ENDPOINT_ALIASES` dict (or equivalent pattern used for `zopk_index`, `company_detail` etc.) and add: ```python 'pej_index': 'public.pej_index', 'pej_local_content': 'public.pej_local_content', 'pej_news': 'public.pej_news', ``` This allows templates to use `url_for('pej_index')` without blueprint prefix, consistent with existing patterns like `url_for('zopk_index')`. - [ ] **Step 5: Verify syntax** Run: `python -m py_compile blueprints/public/routes_pej.py && python -m py_compile blueprints/pej_constants.py` Expected: No output (clean compile) - [ ] **Step 6: Commit** ```bash git add blueprints/pej_constants.py blueprints/public/routes_pej.py blueprints/public/__init__.py blueprints/__init__.py git commit -m "feat(pej): add public routes — index, local-content, news" ``` --- ### Task 3: PEJ admin route — CSV export **Files:** - Create: `blueprints/admin/routes_pej.py` - Modify: `blueprints/admin/__init__.py` - [ ] **Step 1: Create admin routes_pej.py** ```python """PEJ admin routes — CSV export of companies for PEJ local content.""" import csv import io from datetime import date from flask import Response from flask_login import login_required, current_user from . import bp from database import db_session, ZOPKCompanyLink, Company from blueprints.pej_constants import get_nuclear_project_ids, LINK_TYPE_LABELS @bp.route('/pej/export') @login_required def pej_export_csv(): """Export PEJ-matched companies as CSV.""" if not current_user.can_access_admin_panel(): return "Brak uprawnień", 403 nuclear_ids = get_nuclear_project_ids() results = db_session.query(ZOPKCompanyLink, Company).join( Company, ZOPKCompanyLink.company_id == Company.id ).filter( ZOPKCompanyLink.project_id.in_(nuclear_ids), ZOPKCompanyLink.relevance_score >= 25, Company.status == 'active' ).order_by(ZOPKCompanyLink.relevance_score.desc()).all() output = io.StringIO() # UTF-8 BOM for Excel output.write('\ufeff') writer = csv.writer(output, delimiter=';', quoting=csv.QUOTE_ALL) writer.writerow([ 'Nazwa firmy', 'Email', 'Telefon', 'Branża', 'PKD (główny)', 'Usługi', 'Typ współpracy PEJ', 'Opis współpracy', 'Score', 'Miasto' ]) for link, company in results: writer.writerow([ company.name or '', company.email or '', company.phone or '', company.category.name if company.category else '', company.pkd_code or '', company.services_offered or '', LINK_TYPE_LABELS.get(link.link_type, link.link_type or ''), link.collaboration_description or '', link.relevance_score or 0, company.address_city or '' ]) filename = f'pej-local-content-{date.today().isoformat()}.csv' return Response( output.getvalue(), mimetype='text/csv; charset=utf-8', headers={'Content-Disposition': f'attachment; filename="{filename}"'} ) ``` - [ ] **Step 2: Register in admin blueprint** In `blueprints/admin/__init__.py`, after line 23 (the ZOPK imports block), add: ```python from . import routes_pej # noqa: E402, F401 ``` - [ ] **Step 3: Verify syntax** Run: `python -m py_compile blueprints/admin/routes_pej.py` Expected: No output (clean compile) - [ ] **Step 4: Commit** ```bash git add blueprints/admin/routes_pej.py blueprints/admin/__init__.py git commit -m "feat(pej): add admin CSV export route" ``` --- ## Chunk 2: Templates ### Task 4: PEJ landing page template **Files:** - Create: `templates/pej/index.html` - [ ] **Step 1: Create templates/pej/ directory** Run: `mkdir -p templates/pej` - [ ] **Step 2: Create index.html** Template extends `base.html`. Uses purple/indigo color scheme (#7c3aed / #4f46e5). Layout: 1. Hero section with title and description 2. Stats bar (companies, news, milestones counts) 3. Announcements section (PEJ category) 4. Two-column: news (left) + timeline (right) 5. Local Content preview (top 6 companies) ```html {% extends "base.html" %} {% block title %}PEJ — Elektrownia Jądrowa - Norda Biznes Partner{% endblock %} {% block extra_css %} {% endblock %} {% block content %}

Elektrownia Jądrowa — Szanse dla Naszych Firm

Izba Norda Biznes aktywnie uczestniczy w projekcie PEJ. Tu znajdziesz aktualności, listę firm gotowych do współpracy i informacje o możliwościach dla członków.

{{ companies_count }}
firm gotowych
{{ news_count }}
aktualności
{{ milestones_count }}
kamieni milowych
{% if announcements %}
Komunikaty Izby
{% for ann in announcements %}
{{ ann.title }}
{{ ann.created_at.strftime('%d.%m.%Y') }}

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

{% endfor %}
{% endif %}
Najnowsze aktualności
{% if news %} {% for item in news %}

{{ item.title }}

{{ item.source_name or '' }} {% if item.published_at %} · {{ item.published_at.strftime('%d.%m.%Y') }}{% endif %}
{% endfor %} Zobacz wszystkie → {% else %}
Brak aktualności nuklearnych.
{% endif %}
Kamienie milowe
{% if milestones %} {% for m in milestones %}
{% if m.target_date %}{{ m.target_date.strftime('%m/%Y') }}{% else %}—{% endif %}
{{ m.title }} {{ m.status }}
{% endfor %} {% else %}
Brak kamieni milowych.
{% endif %}
Local Content — Firmy z Izby gotowe do współpracy
{% if top_companies %}
{% for link, company in top_companies %}
{% if company.category %}{{ company.category.name }}{% endif %} {{ link.relevance_score }}/100 {% if link.link_type %} {% if link.link_type == 'potential_supplier' %}Dostawca {% elif link.link_type == 'partner' %}Partner {% elif link.link_type == 'investor' %}Inwestor {% elif link.link_type == 'beneficiary' %}Beneficjent {% endif %} {% endif %}
{% if link.collaboration_description %}
{{ link.collaboration_description[:150] }}{% if link.collaboration_description|length > 150 %}...{% endif %}
{% endif %}
{% endfor %}
Zobacz pełną listę → {% if current_user.can_access_admin_panel() %} Eksportuj CSV {% endif %}
{% else %}
Brak dopasowanych firm.
{% endif %}
{% endblock %} ``` - [ ] **Step 3: Commit** ```bash git add templates/pej/index.html git commit -m "feat(pej): add landing page template" ``` --- ### Task 5: Local Content template **Files:** - Create: `templates/pej/local_content.html` - [ ] **Step 1: Create local_content.html** ```html {% extends "base.html" %} {% block title %}Local Content — Firmy dla PEJ - Norda Biznes Partner{% endblock %} {% block extra_css %} {% endblock %} {% block content %}

Local Content — Firmy Izby Norda dla PEJ

← Powrót do PEJ
{% if current_user.can_access_admin_panel() %} Eksportuj CSV {% endif %}
{% if category_filter or link_type_filter or search_query %} Wyczyść filtry {% endif %}
Znaleziono {{ total }} firm
{% for link, company in results %}
{% if company.category %}{{ company.category.name }}{% endif %} {% if company.address_city %} · {{ company.address_city }}{% endif %} {% if company.email %} · {{ company.email }}{% endif %}
{% if link.collaboration_description %}
{{ link.collaboration_description }}
{% endif %}
{{ link.relevance_score }}/100
{% if link.link_type %}
{{ link_type_labels.get(link.link_type, link.link_type) }}
{% endif %}
{% else %}
Brak firm spełniających kryteria.
{% endfor %} {% if total_pages > 1 %}
{% if page > 1 %} « Poprzednia {% endif %} {% for p in range(1, total_pages + 1) %} {% if p == page %} {{ p }} {% elif p <= 3 or p > total_pages - 2 or (p >= page - 1 and p <= page + 1) %} {{ p }} {% elif p == 4 or p == total_pages - 2 %} ... {% endif %} {% endfor %} {% if page < total_pages %} Nastepna » {% endif %}
{% endif %} {% endblock %} ``` - [ ] **Step 2: Commit** ```bash git add templates/pej/local_content.html git commit -m "feat(pej): add Local Content company list template" ``` --- ### Task 6: PEJ news template **Files:** - Create: `templates/pej/news.html` - [ ] **Step 1: Create news.html** Follows the same pattern as `zopk/news_list.html` but simplified (no project filter needed — all content is already nuclear). ```html {% extends "base.html" %} {% block title %}Aktualności PEJ - Norda Biznes Partner{% endblock %} {% block extra_css %} {% endblock %} {% block content %}

Aktualności — Elektrownia Jądrowa

← Powrót do PEJ

{{ total }} artykułów

{% for item in news %}

{{ item.title }}

{{ item.source_name or 'Źródło nieznane' }} {% if item.published_at %} · {{ item.published_at.strftime('%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 %}
{% if item.summary %}
{{ item.summary[:200] }}{% if item.summary|length > 200 %}...{% endif %}
{% endif %}
{% else %}
Brak aktualności nuklearnych.
{% endfor %} {% if total_pages > 1 %}
{% if page > 1 %} « Poprzednia {% endif %} {% for p in range(1, total_pages + 1) %} {% if p == page %} {{ p }} {% elif p <= 3 or p > total_pages - 2 or (p >= page - 1 and p <= page + 1) %} {{ p }} {% endif %} {% endfor %} {% if page < total_pages %} Następna » {% endif %}
{% endif %} {% endblock %} ``` - [ ] **Step 2: Commit** ```bash git add templates/pej/news.html git commit -m "feat(pej): add nuclear news list template" ``` --- ## Chunk 3: Navigation Reorganization ### Task 7: Main NAV — "Projekty ▾" dropdown + cleanup **Files:** - Modify: `templates/base.html:1461-1504` This is the most delicate change — modifying shared navigation. Changes: 1. Remove "Social (beta)" from main NAV (line 1461-1464) — already in admin bar 2. Remove "Korzyści" from main NAV (line 1487-1489) — already in admin bar 3. Replace the entire Kaszubia/Więcej block (lines 1491-1504) with a unified "Projekty ▾" dropdown for ALL users - [ ] **Step 1: Remove "Social (beta)" from main NAV** Delete lines 1461-1464 (the `{% if MANAGER %}` block with Social link): ```html {% if current_user.is_authenticated and current_user.has_role(SystemRole.MANAGER) %}
  • Social beta
  • {% endif %} ``` - [ ] **Step 2: Remove "Korzyści" from main NAV** Delete lines 1487-1489 (the `{% if can_access_admin_panel %}` block with Korzyści): ```html {% if current_user.can_access_admin_panel() %}
  • Korzyści
  • {% endif %} ``` - [ ] **Step 3: Replace Kaszubia/Więcej with "Projekty ▾" dropdown** Replace the entire block from lines 1491-1504 (the `{% if maciej %}` Więcej / `{% else %}` Kaszubia block) with: ```html ``` - [ ] **Step 4: Verify the result** Final admin main NAV should be 10 items: ``` Firmy | NordaGPT | Kalendarz | B2B | Forum | Wiadomości | Aktualności | Edukacja | Rada | Projekty▾ ``` - [ ] **Step 5: Commit** ```bash git add templates/base.html git commit -m "refactor(nav): replace Kaszubia/Więcej with Projekty dropdown, remove duplicates" ``` --- ### Task 8: Admin bar — "Narzędzia" dropdown **Files:** - Modify: `templates/base.html:1855` Move Kontakty zewnętrzne, Raporty, Mapa Powiązań from main NAV (removed in Task 7) to admin bar. - [ ] **Step 1: Add "Narzędzia" dropdown to admin bar** Insert after line 1855 (closing `` of "Treści" dropdown), before the admin bar container closing ``: ```html {% if is_audit_owner %}
    Kontakty zewnętrzne Raporty Mapa Powiązań
    {% endif %} ``` - [ ] **Step 2: Commit** ```bash git add templates/base.html git commit -m "refactor(nav): add Narzędzia dropdown to admin bar" ``` --- ## Chunk 4: Verification ### Task 9: Local development testing - [ ] **Step 1: Start the application** Run: `python3 app.py` Expected: Server starts on port 5000/5001 without import errors. - [ ] **Step 2: Verify PEJ routes** 1. Navigate to `http://localhost:5000/pej` (logged in) — should show landing page with purple hero 2. Navigate to `http://localhost:5000/pej/local-content` — should show company list 3. Navigate to `http://localhost:5000/pej/aktualnosci` — should show nuclear news 4. Navigate to `http://localhost:5000/admin/pej/export` (as admin) — should download CSV - [ ] **Step 3: Verify navigation** 1. As admin: main NAV has 10 items, "Projekty ▾" dropdown shows Kaszubia + PEJ 2. As admin: "Social" and "Korzyści" NOT in main NAV 3. As admin: admin bar has "Narzędzia" dropdown with Kontakty, Raporty, Mapa 4. As regular user: "Projekty ▾" dropdown shows Kaszubia + PEJ - [ ] **Step 4: Mobile responsiveness** Check all pages at 375px viewport width. - [ ] **Step 5: Commit verification notes** If any fixes were needed, commit them as separate fix commits. --- ### Task 10: Deploy to staging Follow standard deployment procedure from CLAUDE.md: - [ ] **Step 1: Push to both remotes** ```bash git push origin master && git push inpi master ``` - [ ] **Step 2: Deploy to staging** ```bash ssh maciejpi@10.22.68.248 "cd /var/www/nordabiznes && sudo -u www-data git pull && sudo systemctl restart nordabiznes" ``` - [ ] **Step 3: Verify on staging** ```bash curl -sI https://staging.nordabiznes.pl/health | head -3 ``` Then manually test `/pej`, `/pej/local-content`, `/pej/aktualnosci` on staging. - [ ] **Step 4: Deploy to production (AFTER staging verification)** ```bash 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 ```