Combined all January 31st changes into one release. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
913 lines
37 KiB
Python
913 lines
37 KiB
Python
"""
|
|
Public Routes
|
|
=============
|
|
|
|
Public-facing routes: index, company profiles, search, events, new members,
|
|
connections map, release notes, dashboard.
|
|
"""
|
|
|
|
import logging
|
|
from datetime import datetime, timedelta
|
|
|
|
from flask import render_template, request, redirect, url_for, flash, session
|
|
from flask_login import login_required, current_user
|
|
from sqlalchemy import or_, func
|
|
|
|
from . import bp
|
|
from database import (
|
|
SessionLocal,
|
|
Company,
|
|
Category,
|
|
User,
|
|
CompanyRecommendation,
|
|
CompanyEvent,
|
|
CompanyDigitalMaturity,
|
|
CompanyWebsiteAnalysis,
|
|
CompanyQualityTracking,
|
|
CompanyWebsiteContent,
|
|
CompanyAIInsights,
|
|
CompanySocialMedia,
|
|
CompanyContact,
|
|
Person,
|
|
CompanyPerson,
|
|
GBPAudit,
|
|
ITAudit,
|
|
CompanyPKD,
|
|
NordaEvent,
|
|
EventAttendee,
|
|
AIChatConversation,
|
|
AIChatMessage,
|
|
UserSession,
|
|
SearchQuery,
|
|
)
|
|
from utils.helpers import sanitize_input
|
|
from extensions import limiter
|
|
from search_service import search_companies
|
|
|
|
# Logger
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Global constant (same as in app.py)
|
|
COMPANY_COUNT_MARKETING = 150
|
|
|
|
|
|
@bp.route('/')
|
|
def index():
|
|
"""Homepage - landing page for guests, company directory for logged in users"""
|
|
if not current_user.is_authenticated:
|
|
# Landing page for guests
|
|
db = SessionLocal()
|
|
try:
|
|
total_companies = db.query(Company).filter_by(status='active').count()
|
|
total_categories = db.query(Category).count()
|
|
return render_template(
|
|
'landing.html',
|
|
total_companies=total_companies,
|
|
total_categories=total_categories
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
# Company directory for logged in users
|
|
db = SessionLocal()
|
|
try:
|
|
from datetime import date
|
|
companies = db.query(Company).filter_by(status='active').order_by(Company.name).all()
|
|
|
|
# Get hierarchical categories (main categories with subcategories)
|
|
main_categories = db.query(Category).filter(
|
|
Category.parent_id.is_(None)
|
|
).order_by(Category.display_order, Category.name).all()
|
|
|
|
# All categories for backwards compatibility
|
|
categories = db.query(Category).order_by(Category.sort_order).all()
|
|
|
|
total_companies = len(companies)
|
|
total_categories = len([c for c in categories if db.query(Company).filter_by(category_id=c.id).count() > 0])
|
|
|
|
# Najbliższe wydarzenie (dla bannera "Kto weźmie udział?")
|
|
next_event = db.query(NordaEvent).filter(
|
|
NordaEvent.event_date >= date.today()
|
|
).order_by(NordaEvent.event_date.asc()).first()
|
|
|
|
# Sprawdź czy użytkownik jest zapisany na to wydarzenie
|
|
user_registered = False
|
|
if next_event:
|
|
user_registered = db.query(EventAttendee).filter(
|
|
EventAttendee.event_id == next_event.id,
|
|
EventAttendee.user_id == current_user.id
|
|
).first() is not None
|
|
|
|
return render_template(
|
|
'index.html',
|
|
companies=companies,
|
|
categories=categories,
|
|
main_categories=main_categories,
|
|
total_companies=total_companies,
|
|
total_categories=total_categories,
|
|
next_event=next_event,
|
|
user_registered=user_registered
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/company/<int:company_id>')
|
|
def company_detail(company_id):
|
|
"""Company detail page - requires login"""
|
|
db = SessionLocal()
|
|
try:
|
|
company = db.query(Company).filter_by(id=company_id).first()
|
|
if not company:
|
|
flash('Firma nie znaleziona.', 'error')
|
|
return redirect(url_for('index'))
|
|
|
|
# Load digital maturity data if available
|
|
maturity_data = db.query(CompanyDigitalMaturity).filter_by(company_id=company_id).first()
|
|
# Get latest website analysis sorted by audit date (consistent with seo_audit_dashboard)
|
|
website_analysis = db.query(CompanyWebsiteAnalysis).filter_by(
|
|
company_id=company_id
|
|
).order_by(CompanyWebsiteAnalysis.seo_audited_at.desc()).first()
|
|
|
|
# Load quality tracking data
|
|
quality_data = db.query(CompanyQualityTracking).filter_by(company_id=company_id).first()
|
|
|
|
# Load company events (latest 10)
|
|
events = db.query(CompanyEvent).filter_by(company_id=company_id).order_by(
|
|
CompanyEvent.event_date.desc(),
|
|
CompanyEvent.created_at.desc()
|
|
).limit(10).all()
|
|
|
|
# Load website scraping data (most recent)
|
|
website_content = db.query(CompanyWebsiteContent).filter_by(company_id=company_id).order_by(
|
|
CompanyWebsiteContent.scraped_at.desc()
|
|
).first()
|
|
|
|
# Load AI insights
|
|
ai_insights = db.query(CompanyAIInsights).filter_by(company_id=company_id).first()
|
|
|
|
# Load social media profiles
|
|
social_media = db.query(CompanySocialMedia).filter_by(company_id=company_id).all()
|
|
|
|
# Load company contacts (phones, emails with sources)
|
|
contacts = db.query(CompanyContact).filter_by(company_id=company_id).order_by(
|
|
CompanyContact.contact_type,
|
|
CompanyContact.is_primary.desc()
|
|
).all()
|
|
|
|
# Load recommendations (approved only, with recommender details)
|
|
recommendations = db.query(CompanyRecommendation).filter_by(
|
|
company_id=company_id,
|
|
status='approved'
|
|
).join(User, CompanyRecommendation.user_id == User.id).order_by(
|
|
CompanyRecommendation.created_at.desc()
|
|
).all()
|
|
|
|
# Load people connected to company (zarząd, wspólnicy, prokurenci)
|
|
people = db.query(CompanyPerson).filter_by(
|
|
company_id=company_id
|
|
).join(Person, CompanyPerson.person_id == Person.id).order_by(
|
|
CompanyPerson.role_category,
|
|
Person.nazwisko
|
|
).all()
|
|
|
|
# Load GBP audit (most recent)
|
|
gbp_audit = db.query(GBPAudit).filter_by(
|
|
company_id=company_id
|
|
).order_by(GBPAudit.audit_date.desc()).first()
|
|
|
|
# Load IT audit (most recent)
|
|
it_audit = db.query(ITAudit).filter_by(
|
|
company_id=company_id
|
|
).order_by(ITAudit.audit_date.desc()).first()
|
|
|
|
# Load PKD codes (all - primary first)
|
|
pkd_codes = db.query(CompanyPKD).filter_by(
|
|
company_id=company_id
|
|
).order_by(CompanyPKD.is_primary.desc(), CompanyPKD.pkd_code).all()
|
|
|
|
# Check if current user can enrich company data (admin or company owner)
|
|
can_enrich = False
|
|
if current_user.is_authenticated:
|
|
can_enrich = current_user.is_admin or (current_user.company_id == company.id)
|
|
|
|
return render_template('company_detail.html',
|
|
company=company,
|
|
company_id=company.id, # For analytics conversion tracking
|
|
maturity_data=maturity_data,
|
|
website_analysis=website_analysis,
|
|
quality_data=quality_data,
|
|
events=events,
|
|
website_content=website_content,
|
|
ai_insights=ai_insights,
|
|
social_media=social_media,
|
|
contacts=contacts,
|
|
recommendations=recommendations,
|
|
people=people,
|
|
gbp_audit=gbp_audit,
|
|
it_audit=it_audit,
|
|
pkd_codes=pkd_codes,
|
|
can_enrich=can_enrich
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/company/<slug>')
|
|
def company_detail_by_slug(slug):
|
|
"""Company detail page by slug - requires login"""
|
|
db = SessionLocal()
|
|
try:
|
|
company = db.query(Company).filter_by(slug=slug).first()
|
|
if not company:
|
|
flash('Firma nie znaleziona.', 'error')
|
|
return redirect(url_for('index'))
|
|
# Redirect to canonical int ID route
|
|
return redirect(url_for('company_detail', company_id=company.id))
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/osoba/<int:person_id>')
|
|
def person_detail(person_id):
|
|
"""Person detail page - shows registry data and portal data if available"""
|
|
db = SessionLocal()
|
|
try:
|
|
# Get person with their company relationships
|
|
person = db.query(Person).filter_by(id=person_id).first()
|
|
if not person:
|
|
flash('Osoba nie znaleziona.', 'error')
|
|
return redirect(url_for('index'))
|
|
|
|
# Get company roles with company details (only active companies)
|
|
company_roles = db.query(CompanyPerson).filter_by(
|
|
person_id=person_id
|
|
).join(Company, CompanyPerson.company_id == Company.id).filter(
|
|
Company.status == 'active'
|
|
).order_by(
|
|
CompanyPerson.role_category,
|
|
Company.name
|
|
).all()
|
|
|
|
# Try to find matching user account by name (for portal data)
|
|
# This is a simple match - in production might need more sophisticated matching
|
|
portal_user = None
|
|
name_parts = person.full_name().upper().split()
|
|
if len(name_parts) >= 2:
|
|
# Try to find user where first/last name matches
|
|
potential_users = db.query(User).filter(
|
|
User.name.isnot(None)
|
|
).all()
|
|
for u in potential_users:
|
|
if u.name:
|
|
user_name_parts = u.name.upper().split()
|
|
# Check if at least first and last name match
|
|
if len(user_name_parts) >= 2:
|
|
if (user_name_parts[-1] in name_parts and # Last name match
|
|
any(part in user_name_parts for part in name_parts[:-1])): # First name match
|
|
portal_user = u
|
|
break
|
|
|
|
return render_template('person_detail.html',
|
|
person=person,
|
|
company_roles=company_roles,
|
|
portal_user=portal_user
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/company/<slug>/recommend', methods=['GET', 'POST'])
|
|
def company_recommend(slug):
|
|
"""Create recommendation for a company - requires login"""
|
|
db = SessionLocal()
|
|
try:
|
|
# Get company
|
|
company = db.query(Company).filter_by(slug=slug).first()
|
|
if not company:
|
|
flash('Firma nie znaleziona.', 'error')
|
|
return redirect(url_for('index'))
|
|
|
|
# Handle POST (form submission)
|
|
if request.method == 'POST':
|
|
recommendation_text = request.form.get('recommendation_text', '').strip()
|
|
service_category = sanitize_input(request.form.get('service_category', ''), 200)
|
|
show_contact = request.form.get('show_contact') == '1'
|
|
|
|
# Validation
|
|
if not recommendation_text or len(recommendation_text) < 50:
|
|
flash('Rekomendacja musi mieć co najmniej 50 znaków.', 'error')
|
|
return render_template('company/recommend.html', company=company)
|
|
|
|
if len(recommendation_text) > 2000:
|
|
flash('Rekomendacja może mieć maksymalnie 2000 znaków.', 'error')
|
|
return render_template('company/recommend.html', company=company)
|
|
|
|
# Prevent self-recommendation
|
|
if current_user.company_id == company.id:
|
|
flash('Nie możesz polecać własnej firmy.', 'error')
|
|
return redirect(url_for('company_detail', company_id=company.id))
|
|
|
|
# Check for duplicate (user already recommended this company)
|
|
existing = db.query(CompanyRecommendation).filter_by(
|
|
user_id=current_user.id,
|
|
company_id=company.id
|
|
).first()
|
|
|
|
if existing:
|
|
flash('Już poleciłeś tę firmę. Możesz edytować swoją wcześniejszą rekomendację.', 'error')
|
|
return redirect(url_for('company_detail', company_id=company.id))
|
|
|
|
# Create recommendation
|
|
recommendation = CompanyRecommendation(
|
|
company_id=company.id,
|
|
user_id=current_user.id,
|
|
recommendation_text=recommendation_text,
|
|
service_category=service_category if service_category else None,
|
|
show_contact=show_contact,
|
|
status='pending'
|
|
)
|
|
db.add(recommendation)
|
|
db.commit()
|
|
|
|
flash('Dziękujemy! Twoja rekomendacja została przesłana i oczekuje na moderację.', 'success')
|
|
return redirect(url_for('company_detail', company_id=company.id))
|
|
|
|
# Handle GET (show form)
|
|
return render_template('company/recommend.html', company=company)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/search')
|
|
@login_required
|
|
def search():
|
|
"""Search companies and people with advanced matching - requires login"""
|
|
query = request.args.get('q', '')
|
|
category_id = request.args.get('category', type=int)
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
# Use new SearchService with synonym expansion, NIP/REGON lookup, and fuzzy matching
|
|
results = search_companies(db, query, category_id, limit=50)
|
|
|
|
# Extract companies from SearchResult objects
|
|
companies = [r.company for r in results]
|
|
|
|
# Log search to analytics (SearchQuery table)
|
|
if query:
|
|
try:
|
|
analytics_session_id = session.get('analytics_session_id')
|
|
session_db_id = None
|
|
if analytics_session_id:
|
|
user_session = db.query(UserSession).filter_by(session_id=analytics_session_id).first()
|
|
if user_session:
|
|
session_db_id = user_session.id
|
|
|
|
search_query = SearchQuery(
|
|
session_id=session_db_id,
|
|
user_id=current_user.id if current_user.is_authenticated else None,
|
|
query=query[:500],
|
|
query_normalized=query.lower().strip()[:500],
|
|
results_count=len(companies),
|
|
has_results=len(companies) > 0,
|
|
search_type='main',
|
|
filters_used={'category_id': category_id} if category_id else None
|
|
)
|
|
db.add(search_query)
|
|
db.commit()
|
|
except Exception as e:
|
|
logger.error(f"Search logging error: {e}")
|
|
db.rollback()
|
|
|
|
# For debugging/analytics - log search stats
|
|
if query:
|
|
match_types = {}
|
|
for r in results:
|
|
match_types[r.match_type] = match_types.get(r.match_type, 0) + 1
|
|
logger.info(f"Search '{query}': {len(companies)} results, types: {match_types}")
|
|
|
|
# Search people by name (partial match)
|
|
people_results = []
|
|
if query and len(query) >= 2:
|
|
q = f"%{query}%"
|
|
people_results = db.query(Person).filter(
|
|
or_(
|
|
Person.imiona.ilike(q),
|
|
Person.nazwisko.ilike(q),
|
|
func.concat(Person.imiona, ' ', Person.nazwisko).ilike(q)
|
|
)
|
|
).limit(20).all()
|
|
|
|
# For each person, get their company connections count
|
|
for person in people_results:
|
|
person.company_count = len(set(
|
|
r.company_id for r in person.company_roles
|
|
if r.company and r.company.status == 'active'
|
|
))
|
|
|
|
logger.info(f"Search '{query}': {len(people_results)} people found")
|
|
|
|
return render_template(
|
|
'search_results.html',
|
|
companies=companies,
|
|
people=people_results,
|
|
query=query,
|
|
category_id=category_id,
|
|
result_count=len(companies)
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/aktualnosci')
|
|
@login_required
|
|
def events():
|
|
"""Company events and news - latest updates from member companies"""
|
|
event_type_filter = request.args.get('type', '')
|
|
company_id = request.args.get('company', type=int)
|
|
page = request.args.get('page', 1, type=int)
|
|
per_page = 20
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
# Build query
|
|
query = db.query(CompanyEvent).join(Company)
|
|
|
|
# Apply filters
|
|
if event_type_filter:
|
|
query = query.filter(CompanyEvent.event_type == event_type_filter)
|
|
if company_id:
|
|
query = query.filter(CompanyEvent.company_id == company_id)
|
|
|
|
# Order by date (newest first)
|
|
query = query.order_by(
|
|
CompanyEvent.event_date.desc(),
|
|
CompanyEvent.created_at.desc()
|
|
)
|
|
|
|
# Pagination
|
|
total_events = query.count()
|
|
events = query.limit(per_page).offset((page - 1) * per_page).all()
|
|
|
|
# Get companies with events for filter dropdown
|
|
companies_with_events = db.query(Company).join(CompanyEvent).distinct().order_by(Company.name).all()
|
|
|
|
# Event type statistics
|
|
event_types = db.query(
|
|
CompanyEvent.event_type,
|
|
func.count(CompanyEvent.id)
|
|
).group_by(CompanyEvent.event_type).all()
|
|
|
|
return render_template(
|
|
'events.html',
|
|
events=events,
|
|
companies_with_events=companies_with_events,
|
|
event_types=event_types,
|
|
event_type_filter=event_type_filter,
|
|
company_id=company_id,
|
|
page=page,
|
|
per_page=per_page,
|
|
total_events=total_events,
|
|
total_pages=(total_events + per_page - 1) // per_page
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/nowi-czlonkowie')
|
|
@login_required
|
|
def new_members():
|
|
"""Lista nowych firm członkowskich"""
|
|
days = request.args.get('days', 90, type=int)
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
cutoff_date = datetime.now() - timedelta(days=days)
|
|
|
|
new_companies = db.query(Company).filter(
|
|
Company.status == 'active',
|
|
Company.created_at >= cutoff_date
|
|
).order_by(Company.created_at.desc()).all()
|
|
|
|
return render_template('new_members.html',
|
|
companies=new_companies,
|
|
days=days,
|
|
total=len(new_companies)
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/mapa-polaczen')
|
|
def connections_map():
|
|
"""Company-person connections visualization page"""
|
|
return render_template('connections_map.html')
|
|
|
|
|
|
@bp.route('/dashboard')
|
|
@login_required
|
|
def dashboard():
|
|
"""User dashboard"""
|
|
db = SessionLocal()
|
|
try:
|
|
# Get user's conversations
|
|
conversations = db.query(AIChatConversation).filter_by(
|
|
user_id=current_user.id
|
|
).order_by(AIChatConversation.updated_at.desc()).limit(10).all()
|
|
|
|
# Stats
|
|
total_conversations = db.query(AIChatConversation).filter_by(user_id=current_user.id).count()
|
|
total_messages = db.query(AIChatMessage).join(AIChatConversation).filter(
|
|
AIChatConversation.user_id == current_user.id
|
|
).count()
|
|
|
|
return render_template(
|
|
'dashboard.html',
|
|
conversations=conversations,
|
|
total_conversations=total_conversations,
|
|
total_messages=total_messages
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/release-notes')
|
|
def release_notes():
|
|
"""Historia zmian platformy."""
|
|
releases = [
|
|
{
|
|
'version': 'v1.22.0',
|
|
'date': '31 stycznia 2026',
|
|
'badges': ['new', 'improve', 'fix'],
|
|
'new': [
|
|
# MEGA WAŻNE - B2B Interactions
|
|
'<strong>Tablica B2B: Przycisk "Jestem zainteresowany"</strong> - sygnał zainteresowania ogłoszeniem',
|
|
'<strong>Tablica B2B: Publiczne Q&A</strong> - pytania i odpowiedzi widoczne dla wszystkich',
|
|
'<strong>Tablica B2B: Wiadomości z kontekstem</strong> - powiązanie wiadomości z ogłoszeniem',
|
|
'Tablica B2B: Lista zainteresowanych widoczna dla autora ogłoszenia',
|
|
'Tablica B2B: Badge "B2B" przy wiadomościach powiązanych z ogłoszeniem',
|
|
# Read tracking
|
|
'<strong>Forum: Avatary "widziane przez"</strong> przy każdej odpowiedzi, nie tylko temacie',
|
|
'<strong>Tablica B2B: Avatary "widziane przez"</strong> - kto widział ogłoszenie',
|
|
# Admin
|
|
'<strong>Admin: Moduł zarządzania firmami</strong> - lista, edycja, statystyki',
|
|
'<strong>Admin: Moduł zarządzania osobami</strong> - dane z KRS, powiązania z firmami',
|
|
'<strong>Admin: Dashboard statusu</strong> - SSL, deploy, bezpieczeństwo, API metrics',
|
|
'<strong>Audyt logowań</strong> - śledzenie zdarzeń login/logout w systemie',
|
|
# Forum modernization
|
|
'<strong>Forum: Reakcje emoji</strong> - możliwość reagowania na tematy i odpowiedzi (👍 ❤️)',
|
|
'<strong>Forum: Subskrypcje tematów</strong> - powiadomienia o nowych odpowiedziach',
|
|
'<strong>Forum: Edycja postów</strong> - użytkownicy mogą edytować swoje wpisy (do 24h)',
|
|
'<strong>Forum: Zgłaszanie treści</strong> - użytkownicy mogą zgłaszać nieodpowiednie wpisy',
|
|
'<strong>Forum: Oznaczanie rozwiązań</strong> - admin może oznaczyć odpowiedź jako rozwiązanie',
|
|
'Forum: Statystyki użytkownika (tematy, odpowiedzi, rozwiązania)',
|
|
'Forum: Obsługa Markdown w treści postów',
|
|
'Forum: Wzmianki @użytkownik z powiadomieniami',
|
|
# Forum admin tools
|
|
'<strong>Forum: Panel analityki</strong> - statystyki, wykresy aktywności, ranking użytkowników',
|
|
'Forum: Eksport aktywności do CSV z filtrem dat',
|
|
'Forum: Akcje zbiorcze (przypinanie, blokowanie, zmiana statusu, usuwanie)',
|
|
'Forum: Przenoszenie tematów między kategoriami',
|
|
'Forum: Łączenie wielu tematów w jeden',
|
|
'Forum: Wyszukiwarka admina (także usunięte treści)',
|
|
'Forum: Log aktywności użytkownika',
|
|
'Forum: Soft-delete z możliwością przywracania treści',
|
|
# Menu
|
|
'Menu admina: Dodano linki do Forum, Ogłoszeń i Insights AI',
|
|
],
|
|
'improve': [
|
|
'<strong>Architektura: Modularyzacja kodu</strong> - migracja do blueprintów Flask',
|
|
'Forum: Etykieta "(Ty)" przy własnym awatarze w "widziane przez"',
|
|
'Dashboard statusu: Poprawiony układ kart SSL (2 kolumny + issuer)',
|
|
'Usunięto nieużywany kod z głównego pliku aplikacji',
|
|
],
|
|
'fix': [
|
|
'<strong>NordaGPT: Naprawiono wyświetlanie paska wpisywania</strong> - pole było przycięte dla adminów',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.21.0',
|
|
'date': '30 stycznia 2026',
|
|
'badges': ['new', 'improve', 'fix'],
|
|
'new': [
|
|
# MEGA WAŻNE - Konto użytkownika
|
|
'<strong>Moje konto: Nowa sekcja ustawień</strong> - edycja danych, prywatność, bezpieczeństwo, blokady',
|
|
'<strong>Forum: Panel moderacji dla admina</strong> - usuwanie wątków i odpowiedzi, przypinanie, blokowanie',
|
|
'<strong>Tablica B2B: Panel moderacji dla admina</strong> - usuwanie i dezaktywacja ogłoszeń',
|
|
# UX
|
|
'Formularze: Ikonka oka przy polach hasła (podgląd wpisywanego hasła)',
|
|
'Forum: Ładny modal potwierdzenia zamiast systemowego okna',
|
|
'Tablica B2B: Ładny modal potwierdzenia przy moderacji',
|
|
# Feedback
|
|
'Forum: Wątek "Zgłoszenia i sugestie użytkowników" do zbierania feedbacku',
|
|
],
|
|
'improve': [
|
|
'Strona rejestracji: Poprawna nazwa "Norda Biznes Partner"',
|
|
'Strona maintenance: Przyjazna strona podczas aktualizacji (502/503/504)',
|
|
],
|
|
'fix': [
|
|
'<strong>Reset hasła: Automatyczna weryfikacja emaila</strong> - użytkownik nie musi ponownie weryfikować',
|
|
'Akademia: Usunięto placeholder video "Jak korzystać z NordaGPT"',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.20.0',
|
|
'date': '29 stycznia 2026',
|
|
'badges': ['new', 'improve', 'fix'],
|
|
'new': [
|
|
# MEGA WAŻNE - AI
|
|
'<strong>NordaGPT: Upgrade do Gemini 3 Flash Preview</strong> - najnowszy model Google AI',
|
|
'<strong>NordaGPT: Dwa modele do wyboru</strong> - Flash (darmowy) i Pro (płatny, lepszy)',
|
|
'NordaGPT: 7x lepsze rozumowanie, thinking mode, 78% na SWE-bench',
|
|
'NordaGPT: Osobne klucze API dla Free tier i Paid tier',
|
|
'NordaGPT: Wyświetlanie szacowanego kosztu miesięcznego',
|
|
# MEGA WAŻNE - PWA
|
|
'<strong>PWA: Aplikacja mobilna</strong> - możliwość instalacji na telefonie (iOS/Android)',
|
|
'PWA: Web Manifest z ikonami 192px i 512px',
|
|
'PWA: Apple Touch Icon dla urządzeń iOS',
|
|
# Aktualności
|
|
'Aktualności: Obsługa wielu kategorii dla jednego ogłoszenia',
|
|
'Aktualności: Nowe kategorie - Wewnętrzne, Zewnętrzne, Wydarzenie, Okazja biznesowa, Partnerstwo',
|
|
# Edukacja
|
|
'Edukacja: Integracja wideo z portalem (modal player)',
|
|
'Edukacja: Wideo "Wprowadzenie do Norda Biznes Partner"',
|
|
# Admin
|
|
'<strong>Admin: Powiadomienia email o nowych rejestracjach</strong> - mail przy każdej rejestracji',
|
|
],
|
|
'improve': [
|
|
'Strona główna: Nowa ikona NordaGPT',
|
|
'Stopka: Usunięcie nieaktywnych linków',
|
|
],
|
|
'fix': [
|
|
'Tablica B2B: Naprawiono błąd 500 przy dodawaniu ogłoszeń',
|
|
'Kalendarz: Naprawiono błąd 500 przy dodawaniu wydarzeń',
|
|
'Kontakty: Naprawiono nawigację w module',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.19.0',
|
|
'date': '28 stycznia 2026',
|
|
'badges': ['new', 'improve', 'security'],
|
|
'new': [
|
|
# MEGA WAŻNE - Prywatność
|
|
'<strong>Prywatność: Ukrywanie telefonu i emaila</strong> w profilu (Ustawienia → Prywatność)',
|
|
'<strong>Blokowanie użytkowników</strong> - możliwość blokowania kontaktów (Ustawienia → Blokady)',
|
|
'Prywatność: Preferencje kanałów kontaktu (email, telefon, portal)',
|
|
'Blokowanie: Bidirectional - zablokowany nie może wysłać wiadomości',
|
|
# MEGA WAŻNE - Kategorie
|
|
'<strong>Kategorie: Hierarchiczna struktura</strong> - 4 główne grupy branżowe',
|
|
'Katalog: Żółta kategoria "Do uzupełnienia" dla 27 firm',
|
|
'Kategorie: Nowe podkategorie (Budownictwo ogólne, Produkcja ogólna, Usługi finansowe)',
|
|
# Nowe sekcje
|
|
'<strong>Edukacja: Nowa sekcja</strong> Platforma Edukacyjna w menu',
|
|
'Insights: Panel dla adminów do zbierania feedbacku',
|
|
'Health: Monitorowanie nowych endpointów',
|
|
],
|
|
'improve': [
|
|
'Katalog: Tylko aktywna kategoria podświetlona',
|
|
'Kategorie: Sortowanie malejąco po liczbie firm',
|
|
],
|
|
'security': [
|
|
'<strong>RODO: Automatyczne maskowanie danych wrażliwych</strong> w czacie (PESEL, karty, IBAN)',
|
|
'<strong>Chat: Izolacja sesji</strong> - użytkownicy nie widzą pytań innych',
|
|
'Admin: Anonimizacja zapytań w panelu analityki',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.17.0',
|
|
'date': '26 stycznia 2026',
|
|
'badges': ['new'],
|
|
'new': [
|
|
'<strong>Aktualności: Nowa sekcja</strong> dla członków (Społeczność → Aktualności)',
|
|
'Aktualności: Panel administracyjny do zarządzania ogłoszeniami',
|
|
'Aktualności: Kategorie, statusy publikacji, przypinanie',
|
|
'Aktualności: Linki zewnętrzne i załączniki PDF',
|
|
'Pierwsze ogłoszenia: Baza noclegowa ARP, Konkurs Tytani Przedsiębiorczości',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.16.0',
|
|
'date': '14 stycznia 2026',
|
|
'badges': ['new', 'improve', 'fix'],
|
|
'new': [
|
|
# MEGA WAŻNE - Bezpieczeństwo
|
|
'<strong>GeoIP Blocking</strong> - blokowanie krajów wysokiego ryzyka (RU, CN, KP, IR, BY)',
|
|
'<strong>Email: Własna domena</strong> - wysyłka z noreply@nordabiznes.pl (DKIM, SPF, DMARC)',
|
|
# Raporty
|
|
'<strong>Raporty: Nowa sekcja</strong> - staż członkostwa, Social Media, struktura branżowa',
|
|
'Profil firmy: Data przystąpienia do Izby NORDA z kartą stażu',
|
|
'Integracja: API CEIDG do pobierania danych JDG',
|
|
'Bezpieczeństwo: Panel z oceną wszystkich mechanizmów ochrony',
|
|
],
|
|
'improve': [
|
|
'Dane firm: Rok założenia uzupełniony dla 71 z 111 firm (64%)',
|
|
'Import dat przystąpienia: 57 firm z historią od 1997 roku',
|
|
],
|
|
'fix': [
|
|
'Analityka: Polskie znaki i pełne nazwy użytkowników',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.15.0',
|
|
'date': '13 stycznia 2026',
|
|
'badges': ['new', 'improve', 'fix'],
|
|
'new': [
|
|
# MEGA WAŻNE - NordaGPT
|
|
'<strong>NordaGPT: Rozszerzony kontekst AI</strong> - rekomendacje, kalendarz, B2B, forum, KRS',
|
|
'<strong>NordaGPT: Klikalne linki</strong> URL i email w odpowiedziach AI',
|
|
'<strong>NordaGPT: Banner na stronie głównej</strong> z szybkim dostępem do chatu',
|
|
# Kalendarz
|
|
'<strong>Kalendarz: Widok siatki miesięcznej</strong> z Quick RSVP',
|
|
'Kalendarz: Banner wydarzenia na stronie głównej z uczestnikami',
|
|
# AI i Audyty
|
|
'<strong>AI Enrichment</strong> - wzbogacanie danych firm przez AI z web search',
|
|
'<strong>KRS Audit</strong> - parsowanie dokumentów PDF, progress bar',
|
|
'<strong>Analityka: Panel /admin/analytics</strong> - śledzenie sesji użytkowników',
|
|
# Profile
|
|
'Profil firmy: Wszystkie kody PKD, dane właściciela CEIDG',
|
|
'Profil firmy: Zielone badge dla osób zweryfikowanych w KRS',
|
|
],
|
|
'improve': [
|
|
'Lepsze formatowanie odpowiedzi AI (Markdown)',
|
|
'Banner NordaGPT minimalizowalny',
|
|
],
|
|
'fix': [
|
|
'Rate limit logowania i audytu SEO zwiększony',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.14.0',
|
|
'date': '12 stycznia 2026',
|
|
'badges': ['new', 'improve', 'fix'],
|
|
'new': [
|
|
'<strong>Audyt GBP: Pełny audyt</strong> z Google Places API dla wszystkich firm',
|
|
'Audyt GBP: Sekcja edukacyjna "Jak działa wizytówka Google?"',
|
|
'Audyty: Sekcje inline na profilu firmy (SEO, GBP, Social Media, IT)',
|
|
],
|
|
'improve': [
|
|
'Ujednolicona 5-poziomowa skala kolorów dla audytów',
|
|
'Social Media: Wynik jako procent zamiast liczby platform',
|
|
],
|
|
'fix': [
|
|
'Audyt GBP: Kategorie Google po polsku',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.13.0',
|
|
'date': '11 stycznia 2026',
|
|
'badges': ['new', 'improve'],
|
|
'new': [
|
|
# MEGA WAŻNE
|
|
'<strong>Mapa Powiązań</strong> - interaktywna wizualizacja firm i osób (D3.js)',
|
|
'<strong>Profile osób</strong> (/osoba) - dane z KRS/CEIDG i portalu',
|
|
'<strong>AI Learning</strong> - uczenie chatbota z feedbacku użytkowników',
|
|
# Inne
|
|
'Wyszukiwarka osób z częściowym dopasowaniem',
|
|
'Logo firm w wynikach wyszukiwania',
|
|
'Panel AI Usage: szczegółowy widok per użytkownik',
|
|
],
|
|
'improve': [
|
|
'Mapa: fullscreen modal, etykiety przy hover',
|
|
'System toastów zamiast natywnych dialogów',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.11.0',
|
|
'date': '10 stycznia 2026',
|
|
'badges': ['new', 'improve', 'security'],
|
|
'new': [
|
|
# MEGA WAŻNE
|
|
'<strong>Forum: Załączniki obrazów</strong> - drag & drop, Ctrl+V, do 10 plików',
|
|
'<strong>Forum: Kategorie i statusy</strong> zgłoszeń (Propozycja, Błąd, Pytanie)',
|
|
'<strong>Dokumentacja architektury</strong> - 19 plików, diagramy C4, Mermaid',
|
|
],
|
|
'improve': [
|
|
'Bezpieczny upload z walidacją magic bytes',
|
|
],
|
|
'security': [
|
|
'<strong>Usunięcie hardcoded credentials</strong> z kodu źródłowego',
|
|
'Zmiana hasła PostgreSQL na produkcji',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.9.0',
|
|
'date': '9 stycznia 2026',
|
|
'badges': ['new', 'improve'],
|
|
'new': [
|
|
'<strong>Panel Audyt GBP</strong> - przegląd profili Google Business',
|
|
'<strong>Panel Audyt Social</strong> - pokrycie Social Media',
|
|
'<strong>Tworzenie użytkowników z AI</strong> - wklejanie tekstu/screenshotów',
|
|
],
|
|
'improve': [
|
|
'Nowy pasek Admin z pogrupowanymi funkcjami',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.8.0',
|
|
'date': '8 stycznia 2026',
|
|
'badges': ['new'],
|
|
'new': [
|
|
'<strong>Panel Audyt IT</strong> - kompleksowy audyt infrastruktury IT firm',
|
|
'Eksport audytów IT do CSV',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.7.0',
|
|
'date': '6 stycznia 2026',
|
|
'badges': ['new'],
|
|
'new': [
|
|
'<strong>Panel Audyt SEO</strong> - analiza wydajności stron www firm',
|
|
'<strong>Integracja z Google PageSpeed Insights API</strong>',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.6.0',
|
|
'date': '29 grudnia 2025',
|
|
'badges': ['new'],
|
|
'new': [
|
|
'<strong>System newsów</strong> i wzmianek medialnych o firmach',
|
|
'Panel moderacji newsów dla adminów',
|
|
'<strong>Integracja z Brave Search API</strong>',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.5.0',
|
|
'date': '15 grudnia 2025',
|
|
'badges': ['new', 'improve'],
|
|
'new': [
|
|
'<strong>Panel Social Media</strong> - zarządzanie profilami społecznościowymi',
|
|
'Weryfikacja aktywności profili Social Media',
|
|
],
|
|
'improve': [
|
|
'Ulepszony profil firmy z sekcją Social Media',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.4.0',
|
|
'date': '1 grudnia 2025',
|
|
'badges': ['new'],
|
|
'new': [
|
|
'<strong>System rekomendacji</strong> między firmami',
|
|
'<strong>Panel składek członkowskich</strong>',
|
|
'<strong>Kalendarz wydarzeń</strong> Norda Biznes',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.3.0',
|
|
'date': '28 listopada 2025',
|
|
'badges': ['new', 'improve'],
|
|
'new': [
|
|
'<strong>Chatbot AI (NordaGPT)</strong> z wiedzą o wszystkich firmach',
|
|
'<strong>Wyszukiwarka firm</strong> z synonimami i fuzzy matching',
|
|
],
|
|
'improve': [
|
|
'Ulepszony SearchService z PostgreSQL FTS',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.2.0',
|
|
'date': '25 listopada 2025',
|
|
'badges': ['new'],
|
|
'new': [
|
|
'<strong>System wiadomości prywatnych</strong> między użytkownikami',
|
|
'Powiadomienia o nowych wiadomościach',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.1.0',
|
|
'date': '24 listopada 2025',
|
|
'badges': ['new', 'improve'],
|
|
'new': [
|
|
'<strong>Rejestracja i logowanie</strong> użytkowników',
|
|
'Profile użytkowników powiązane z firmami',
|
|
],
|
|
'improve': [
|
|
'Responsywny design na urządzenia mobilne',
|
|
],
|
|
},
|
|
{
|
|
'version': 'v1.0.0',
|
|
'date': '23 listopada 2025',
|
|
'badges': ['new'],
|
|
'new': [
|
|
'<strong>Oficjalny start platformy Norda Biznes Partner</strong>',
|
|
'<strong>Katalog 111 firm członkowskich</strong>',
|
|
'Wyszukiwarka firm po nazwie, kategorii, usługach',
|
|
'Profile firm z pełnymi danymi kontaktowymi',
|
|
],
|
|
},
|
|
]
|
|
|
|
# Statystyki (używa globalnej stałej COMPANY_COUNT_MARKETING)
|
|
db = SessionLocal()
|
|
try:
|
|
stats = {
|
|
'companies': COMPANY_COUNT_MARKETING,
|
|
'categories': db.query(Category).filter(Category.parent_id.isnot(None)).count(),
|
|
}
|
|
finally:
|
|
db.close()
|
|
|
|
return render_template('release_notes.html', releases=releases, stats=stats)
|